前言
既然能看到這篇博文,就說明你一定看過這個【Android】當關閉通知消息權限後無法顯示系統Toast的解決方案 。然後,我很開心的告訴你,兄弟,可能你們的心病這次就解決了~當然,沒看過上一篇博文的還是建議看下,看看以前的解決思路,看看之前的實現方式和一些遇到的問題,說不定對你也很有收穫呢。
怎麼使用
github地址:https://github.com/Blincheng/EToast2
如果大家有任何什麼問題,歡迎大家加入EToast交流羣和我一起討論和改進。
QQ羣:EToast交流羣
羣號碼:547279762
(PS:目前v2.1.0已經正式上線,經過一個多月的測試和改進,此版本相對接近完美,歡迎大家使用,
具體改進過程請移步github)
Step 1. Add the JitPack repository to your build file
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
Step 2. Add the dependency
dependencies {
compile 'com.github.Blincheng:EToast2:v2.1.0'
}
然後,就大大咧咧的用吧
EToast 一個關閉系統消息通知後依然可以顯示的Toast,此版本完全是獨立於v1.x.x的版本,實現方式上也是完全的不同,儘量的參考系統Toast的源碼去實現。
和上代EToast相比,有以下的改動:
1. Context不再依賴於Activity顯示。
2. 顯示動畫完全跟隨着系統走,也就是說和系統的Toast動畫效果完全一致
3. 多條顯示規則還是保留了V1.x的版本的規則,永遠只顯示一個Toast。
4. 由於實現原理的更改,EToast不再會被Dialog、PopupWindow等彈窗佈局覆蓋
由於在Android5.0以下無法獲取是否打開系統通知權限,所以爲了防止用戶看不到Toast,最終的邏輯優化了一下:
1. 當用戶是5.0以下的機器時,永遠只顯示EToast
2. 當用戶是5.0以上的機器是,如果打開了通知權限,則顯示系統Toast;反之則顯示Etoast
Toast.makeText(mActivity, text, EToast2.LENGTH_SHORT).show();
需要注意的是,此Toast非彼Toast,應該使用“import com.mic.etoast2.Toast”,建議在BaseActivity中如下使用:
public void showShortText(String text) {
Toast.makeText(mActivity, text, Toast.LENGTH_SHORT).show();
}
好了,沒有興趣想看看怎麼實現的,只是來拿貨的,你就可以走了/(ㄒoㄒ)/~~
然後本文會有點長~請耐心往下看,哈哈哈。
實現思路
首先思考下這個版本要做什麼事
- 首先,Context的使用限制,之前只能是Activity,這個有點憂傷,相當於直接依賴Activity,使用上的確有點不踏實哈~
- 然後,不能在頂層顯示,會被鍵盤、dialog等遮擋,如果有這種場景的話,比較尷尬。
- 內存泄漏,由於引用了當前Activity的上下文
- 顯示動畫是不是應該跟着系統走,好讓Toast和其他App一致。(反正目的就是說不管怎麼樣,一定讓Toast顯示唄)
- 貌似管理機制不是很好,能不能和Activity或者Fragment的生命週期保持一致呢?
那麼,怎麼解決
剛開始我也腦子中思考了許久,想想有什麼好的辦法可以解決以上問題呢?當然,結果肯定是有的。不過貌似也遇到了一些坎坷。剛好呢,我之前抽空看了一下Glide的源碼,因爲類似的圖片框架加載就和Activity、Fragment等的聲明週期密切相關~具體Glide的源碼我就不展開了,簡單說下:
Glide.with(this).load(url).into(imageView);
上面就是Glide的最簡單用法,whit(this)在一開始就把當前環境丟進去了哈~然後那麼久開始仿照直接寫唄。(忘記說了哈~由於之前好像說過以後的博文直接用Kotlin開發,不知道大家能不能習慣哈~)
companion object{
var style = android.R.style.Theme_Light_NoTitleBar
val LENGTH_SHORT = 0
val LENGTH_LONG = 1
var toast:EToast2 ?= null
fun makeText(context: Context?, message: CharSequence, HIDE_DELAY: Int): EToast2?{
if(toast == null)
toast = EToast2()
var etoast: EToast2 = toast!!
if(context == null)
throw IllegalArgumentException("You cannot show EToast2 on a null Context")
else if(Utils.isOnMainThread()&&context !is Application){
if(context is FragmentActivity){
etoast.initDialog(etoast.get(context),HIDE_DELAY,message)
}else if(context is Activity){
etoast.initDialog(etoast.get(context),HIDE_DELAY,message)
}
}else{
etoast.get(context)
}
return etoast
}
fun makeText(activity: Activity?,message: CharSequence,HIDE_DELAY: Int): EToast2?{
if(toast == null)
toast = EToast2()
var etoast: EToast2 = toast!!
etoast.initDialog(etoast.get(activity),HIDE_DELAY,message)
return etoast
}
fun makeText(activity: FragmentActivity?, message: CharSequence, HIDE_DELAY: Int): EToast2?{
if(toast == null)
toast = EToast2()
var etoast: EToast2 = toast!!
etoast.initDialog(etoast.get(activity),HIDE_DELAY,message)
return etoast
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
fun makeText(fragment: android.app.Fragment?, message: CharSequence, HIDE_DELAY: Int): EToast2?{
if(toast == null)
toast = EToast2()
var etoast: EToast2 = toast!!
etoast.initDialog(etoast.get(fragment),HIDE_DELAY,message)
return etoast
}
fun makeText(fragment: Fragment?, message: CharSequence, HIDE_DELAY: Int): EToast2?{
if(toast == null)
toast = EToast2()
var etoast: EToast2 = toast!!
etoast.initDialog(etoast.get(fragment),HIDE_DELAY,message)
return etoast
}
}
我們先來看下構造函數,還是通過makeText(…)只是說,這邊重載了很多個不同參數的函數,也就是說,Context不限制嘍~比如是Activity的話,EToast的生命週期就跟着Activity走,如果是ApplicationContext的話,那就跟着整個App生命週期走,Fragment也一樣。
主要看看參數爲Cotext的時候:
fun makeText(context: Context?, message: CharSequence, HIDE_DELAY: Int): EToast2?{
if(toast == null)
toast = EToast2()
var etoast: EToast2 = toast!!
if(context == null)
throw IllegalArgumentException("You cannot show EToast2 on a null Context")
else if(Utils.isOnMainThread()&&context !is Application){
if(context is FragmentActivity){
etoast.initDialog(etoast.get(context),HIDE_DELAY,message)
}else if(context is Activity){
etoast.initDialog(etoast.get(context),HIDE_DELAY,message)
}
}else{
etoast.get(context)
}
return etoast
}
其實就是和Glide學的,這邊對context進行了分類處理嘍~當然先看看是不是在主線程嘍~不然的話也沒意義是吧。然後說說重點,爲什麼Glide的所有圖片都可以那麼靈活聰明呢?爲什麼都可以跟着生命週期一起走呢?其實,當你簡單看一下源碼就知道了(以下是Glide的源碼):
public RequestManager get(FragmentActivity activity) {
if (Util.isOnBackgroundThread()) {
return get(activity.getApplicationContext());
} else {
assertNotDestroyed(activity);
FragmentManager fm = activity.getSupportFragmentManager();
return supportFragmentGet(activity, fm);
}
}
public RequestManager get(Fragment fragment) {
if (fragment.getActivity() == null) {
throw new IllegalArgumentException("You cannot start a load on a fragment before it is attached");
}
if (Util.isOnBackgroundThread()) {
return get(fragment.getActivity().getApplicationContext());
} else {
FragmentManager fm = fragment.getChildFragmentManager();
return supportFragmentGet(fragment.getActivity(), fm);
}
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public RequestManager get(Activity activity) {
if (Util.isOnBackgroundThread() || Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
return get(activity.getApplicationContext());
} else {
assertNotDestroyed(activity);
android.app.FragmentManager fm = activity.getFragmentManager();
return fragmentGet(activity, fm);
}
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
private static void assertNotDestroyed(Activity activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && activity.isDestroyed()) {
throw new IllegalArgumentException("You cannot start a load for a destroyed activity");
}
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
public RequestManager get(android.app.Fragment fragment) {
if (fragment.getActivity() == null) {
throw new IllegalArgumentException("You cannot start a load on a fragment before it is attached");
}
if (Util.isOnBackgroundThread() || Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
return get(fragment.getActivity().getApplicationContext());
} else {
android.app.FragmentManager fm = fragment.getChildFragmentManager();
return fragmentGet(fragment.getActivity(), fm);
}
}
其實不難發現,不管你在Glide.with()方法中傳入的是Activity、FragmentActivity、v4包下的Fragment、還是app包下的Fragment,最終的流程都是一樣的,那就是會向當前的Activity當中添加一個隱藏的Fragment,然後你就領悟了吧~這個時候其實就有生命週期了。然後這樣的話,就簡單了,你只需要在Fragment中的每個生命週期回調中調用你自己定義的接口做對應的事情就OK嘍 ~這樣別人用起來是不是也相當方便,當生命被銷燬的時候,自然就釋放你的Toast的資源。其實這種方式對於你來說,以後如果想要封裝一個控件,然後呢需要綁定生命週期來做一些事情或者搞事情,這種方法就很棒哈。
那麼佈局怎麼來顯示呢?EToast在之前是通過Activity最外層佈局來加載需要顯示的Toast的,當時呢,我覺得這樣的方式挺新穎的,所以就那麼做了~那麼,我們這次如果不是Activity,那怎麼辦呢?然後我想既然要用到Fragment,那麼我們能不能用DialogFragment呢?這樣是不是一舉兩得,生命週期也有了,佈局顯示也有了?
所以我做了,看代碼
class EToast2Fragment: DialogFragment(){
var HIDE_DELAY = 2000L
var mTextView:TextView ?= null
var message: CharSequence ?= ""
val TAG = "EToast2"
var callbask: CallBack?= null
var isShow:Boolean = false
var outAnimation: Animation ?= null
companion object{
val ANIMATION_DURATION = 500L
fun NewInstance(callback: CallBack): EToast2Fragment {
var fg = EToast2Fragment()
var bundle: Bundle = Bundle()
bundle.putSerializable(TAG,callback)
fg.arguments = bundle
return fg
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initAnimation()
callbask = arguments.getSerializable(TAG) as CallBack?
if(callbask?.getStyle() != null)
setStyle(android.app.DialogFragment.STYLE_NORMAL, callbask?.getStyle()!!)
else
setStyle(android.app.DialogFragment.STYLE_NORMAL, android.R.style.Theme_Light_NoTitleBar)
}
fun initAnimation(){
outAnimation = AlphaAnimation(1.0f, 0.0f)
outAnimation?.duration = ANIMATION_DURATION
outAnimation?.setAnimationListener(object : Animation.AnimationListener{
override fun onAnimationRepeat(animation: Animation?) {
}
override fun onAnimationEnd(animation: Animation?) {
dismiss()
}
override fun onAnimationStart(animation: Animation?) {
}
})
}
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View {
dialog.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
var view = inflater?.inflate(R.layout.etoast,null)
mTextView = view?.findViewById(R.id.mbMessage) as TextView?
mTextView?.text = message
return view!!
}
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
view?.setOnTouchListener{ _, event ->
activity.dispatchTouchEvent(event)
}
isShow = true
}
fun setText(message: CharSequence){
this.message = message
mTextView?.text = message
}
fun delayTime(HIDE_DELAY: Int){
if (HIDE_DELAY == EToast2.LENGTH_LONG) {
this.HIDE_DELAY = 2500
} else if(HIDE_DELAY == EToast2.LENGTH_SHORT){
this.HIDE_DELAY = 1500
}
}
override fun show(manager: FragmentManager?, tag: String?) {
if(isShow){
mTextView?.removeCallbacks(mHideRunnable)
}else{
super.show(manager, tag)
}
mTextView?.postDelayed(mHideRunnable,HIDE_DELAY)
}
private val mHideRunnable = Runnable {
mTextView?.startAnimation(outAnimation)
}
override fun onDestroy() {
super.onDestroy()
callbask?.onDestroy()
}
override fun onPause() {
super.onPause()
callbask?.onPause()
}
override fun onResume() {
super.onResume()
callbask?.onResume()
}
override fun onStart() {
super.onStart()
callbask?.onStart()
}
override fun onDismiss(dialog: DialogInterface?) {
super.onDismiss(dialog)
callbask?.onDismiss()
isShow = false
}
override fun onSaveInstanceState(outState: Bundle?) {
super.onSaveInstanceState(outState)
}
}
這就是常規的顯示一個DialogFragment,當然還有些其他的細節,比如:
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
view?.setOnTouchListener{ _, event ->
activity.dispatchTouchEvent(event)
}
isShow = true
}
這邊的就是爲了使我們的點擊穿透~當你點擊DilaogFragment 的時候,點擊事件還可以往下傳遞。然後呢,把所有的生命週期對應的方法綁定起來,也就完事兒了~當然,由於情況很多種,其實還有一個
EToast2SupportFragment
只不過是繼承的包不一樣而已了,其餘代碼是一致的。但是呢,我發現當我對Dialog設置不同顯示效果的時候,的確有些不一樣的事情,如果我隱藏標題,這個時候點擊事件向下傳遞,其實透過Dialog以後,點擊的位置迴向上偏移50個點左右,當然,爲什麼引起的,大家肯定也能想到。就是標題隱藏了,導致實際位置往上走了,但是下面的Activity或Fragment的點擊位置也就被偏移了。那麼不隱藏標題呢,可能大家的需求不一致~又有說不清的情況。其實東西最終是寫完了,目錄結構如下:
但是!但是!我們再仔細想想上面我們要解決的問題哈~好像也是都解決了吧~但是最終還是被我拋棄了~我覺得是不是太臃腫了,就一個Toast以至於這麼大費周折嗎?主要還有點擊偏移的問題,這個就不好解決了。還有dialog的style問題,那麼又產生新的問題嘍~好吧好吧,就當自己練練手了/(ㄒoㄒ)/~~(如果有感興趣的,可以私聊我要源碼哈~)
所以 最終,我決定從Toast源碼入手,看看系統是怎麼做的!我們就算不改系統的,那我們仿照系統的來做一套Toast爲什麼不可以呢?哈哈哈,那就開始分析源碼吧。
Toast源碼分析
切記看源碼不要一定盯着某一行代碼去拼命理解,然後卡在那裏…
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
Toast result = new Toast(context);
LayoutInflater inflate = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
tv.setText(text);
result.mNextView = v;
result.mDuration = duration;
return result;
}
嗯,就是這個方法了哈~這邊能看出2個信息
1. 源碼真的很簡單
2. Toast的佈局其實就是R.layout.transient_notification
然後看show()方法
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}
然後,這邊來看先要拿到一個INotificationManager ,然後service.enqueueToast(pkg, tn, mDuration);
這個很關鍵啊,是隊列!有木有,其實這樣就能理解了,爲什麼系統的Toast是那種很討厭的,一個接着一個慢慢的出來的,有時候點個10下,你App都退了,卻還能看到一隻在彈Toast。其實這東西,我們並不關注,我們主要是看看系統的Toast的是怎麼顯示在屏幕上的,是吧。那麼繼續跟蹤源碼,我們看看TN又是什麼鬼呢?
private static class TN extends ITransientNotification.Stub {
final Runnable mHide = new Runnable() {
@Override
public void run() {
handleHide();
// Don't do this in handleHide() because it is also invoked by handleShow()
mNextView = null;
}
};
private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
IBinder token = (IBinder) msg.obj;
handleShow(token);
}
};
int mGravity;
int mX, mY;
float mHorizontalMargin;
float mVerticalMargin;
View mView;
View mNextView;
int mDuration;
WindowManager mWM;
static final long SHORT_DURATION_TIMEOUT = 5000;
static final long LONG_DURATION_TIMEOUT = 1000;
TN() {
// XXX This should be changed to use a Dialog, with a Theme.Toast
// defined that sets up the layout params appropriately.
final WindowManager.LayoutParams params = mParams;
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
params.width = WindowManager.LayoutParams.WRAP_CONTENT;
params.format = PixelFormat.TRANSLUCENT;
params.windowAnimations = com.android.internal.R.style.Animation_Toast;
params.type = WindowManager.LayoutParams.TYPE_TOAST;
params.setTitle("Toast");
params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
}
/**
* schedule handleShow into the right thread
*/
@Override
public void show(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.obtainMessage(0, windowToken).sendToTarget();
}
/**
* schedule handleHide into the right thread
*/
@Override
public void hide() {
if (localLOGV) Log.v(TAG, "HIDE: " + this);
mHandler.post(mHide);
}
public void handleShow(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
+ " mNextView=" + mNextView);
if (mView != mNextView) {
// remove the old view if necessary
handleHide();
mView = mNextView;
Context context = mView.getContext().getApplicationContext();
String packageName = mView.getContext().getOpPackageName();
if (context == null) {
context = mView.getContext();
}
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
// We can resolve the Gravity here by using the Locale for getting
// the layout direction
final Configuration config = mView.getContext().getResources().getConfiguration();
final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
mParams.gravity = gravity;
if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
mParams.horizontalWeight = 1.0f;
}
if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
mParams.verticalWeight = 1.0f;
}
mParams.x = mX;
mParams.y = mY;
mParams.verticalMargin = mVerticalMargin;
mParams.horizontalMargin = mHorizontalMargin;
mParams.packageName = packageName;
mParams.hideTimeoutMilliseconds = mDuration ==
Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
mParams.token = windowToken;
if (mView.getParent() != null) {
if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
mWM.removeView(mView);
}
if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
}
}
private void trySendAccessibilityEvent() {
AccessibilityManager accessibilityManager =
AccessibilityManager.getInstance(mView.getContext());
if (!accessibilityManager.isEnabled()) {
return;
}
// treat toasts as notifications since they are used to
// announce a transient piece of information to the user
AccessibilityEvent event = AccessibilityEvent.obtain(
AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
event.setClassName(getClass().getName());
event.setPackageName(mView.getContext().getPackageName());
mView.dispatchPopulateAccessibilityEvent(event);
accessibilityManager.sendAccessibilityEvent(event);
}
public void handleHide() {
if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
if (mView != null) {
// note: checking parent() just to make sure the view has
// been added... i have seen cases where we get here when
// the view isn't yet added, so let's try not to crash.
if (mView.getParent() != null) {
if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
mWM.removeViewImmediate(mView);
}
mView = null;
}
}
}
哈哈哈~這個時候是不是真相大白了,看handleShow
這邊纔是我們的關鍵,原來啊,Toast的視圖是通過WindowManager的addView來加載的。那麼Toast的原來總結一下就是這樣:
先通過makeText()實例化出一個Toast,然後調用toast.show()方法,這時並不會馬上顯示Toast,而是會實例化一個TN變量,然後通過service.enqueueToast()將其加到服務隊列裏面去等待顯示。在TN中進行調控Toast的顯示格式以及裏面的hide()、show()方法來控制Toast的顯示和消失。然後最可悲的是這個隊列是系統維護的,我們並不能干涉,所以纔會出現我們屏蔽系統通知的時候,連Toast都沒有了哈~
真正實現EToast2
那麼既然上面做了那麼多事,我們也看了Toast的源碼,那麼就直接仿照Google的也來寫一套唄,這樣不是所的問題都解決了麼,不受系統控制,然後還可以把那個隊列去掉~優化一下顯示規則什麼的,是吧。
/**
* Author: Blincheng.
* Date: 2017/6/30.
* Description:EToast2.0
*/
public class EToast2 {
private WindowManager manger;
private Long time = 2000L;
private static View contentView;
private WindowManager.LayoutParams params;
private static Timer timer;
private Toast toast;
private static Toast oldToast;
public static final int LENGTH_SHORT = 0;
public static final int LENGTH_LONG = 1;
private static Handler handler;
private CharSequence text;
private EToast2(Context context, CharSequence text, int HIDE_DELAY){
manger = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
this.text = text;
if(HIDE_DELAY == EToast2.LENGTH_SHORT)
this.time = 2000L;
else if(HIDE_DELAY == EToast2.LENGTH_LONG)
this.time = 3500L;
if(oldToast == null){
toast = Toast.makeText(context, text, Toast.LENGTH_SHORT);
contentView = toast.getView();
params = new WindowManager.LayoutParams();
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
params.width = WindowManager.LayoutParams.WRAP_CONTENT;
params.format = PixelFormat.TRANSLUCENT;
params.windowAnimations = toast.getView().getAnimation().INFINITE;
params.type = WindowManager.LayoutParams.TYPE_TOAST;
params.setTitle("EToast2");
params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
params.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;
params.y = 200;
}
if(handler == null){
handler = new Handler(){
@Override
public void handleMessage(Message msg) {
EToast2.this.cancel();
}
};
}
}
public static EToast2 makeText(Context context, String text, int HIDE_DELAY){
EToast2 toast = new EToast2(context, text, HIDE_DELAY);
return toast;
}
public static EToast2 makeText(Context context, int resId, int HIDE_DELAY) {
return makeText(context,context.getText(resId).toString(),HIDE_DELAY);
}
public void show(){
if(oldToast == null){
oldToast = toast;
manger.addView(contentView, params);
timer = new Timer();
}else{
timer.cancel();
oldToast.setText(text);
}
timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
handler.sendEmptyMessage(1);
}
}, time);
}
public void cancel(){
manger.removeView(contentView);
timer.cancel();
oldToast.cancel();
timer = null;
toast = null;
oldToast = null;
contentView = null;
handler = null;
}
public void setText(CharSequence s){
toast.setText(s);
}
}
上面就是EToast2的全部源碼了~是不是很少,就那麼點點。看構造函數,其實我們也是借用了Google的Toast,拿到裏面的佈局嘍~這樣就是原汁原味了,至少,是吧。然後其餘設置其實和Toast沒有什麼區別,唯一不同的是沒有隊列了,然後顯示的規則是就是:
由於在Android5.0以下無法獲取是否打開系統通知權限,所以爲了防止用戶看不到Toast,最終的邏輯優化了一下:
1. 當用戶是5.0以下的機器時,永遠只顯示EToast
2. 當用戶是5.0以上的機器是,如果打開了通知權限,則顯示系統Toast;反之則顯示Etoast
3. 當一個Toast在屏幕中顯示,這時又彈出Toast的話會直接改變Toast上的文字,並且重置計時器,在規定的時間後消失。
然後Toast這邊我也做了一點優化,捨棄了一些接口,比如setText(Resid)。因爲Toast顯示的內容一般變化比較大,所以一般不會通過String寫在本地吧~真的要用,我想你也有辦法用的,是吧。(別說我偷懶)
/**
* Author: Blincheng.
* Date: 2017/6/30.
* Description:
*/
public class Toast {
private static final String CHECK_OP_NO_THROW = "checkOpNoThrow";
private static final String OP_POST_NOTIFICATION = "OP_POST_NOTIFICATION";
private static int checkNotification = 0;
private Object mToast;
private Toast(Context context, String message, int duration) {
checkNotification = isNotificationEnabled(context) ? 0 : 1;
if (checkNotification == 1) {
mToast = EToast2.makeText(context, message, duration);
} else {
mToast = android.widget.Toast.makeText(context, message, duration);
}
}
private Toast(Context context, int resId, int duration) {
if (checkNotification == -1){
checkNotification = isNotificationEnabled(context) ? 0 : 1;
}
if (checkNotification == 1 && context instanceof Activity) {
mToast = EToast2.makeText(context, resId, duration);
} else {
mToast = android.widget.Toast.makeText(context, resId, duration);
}
}
public static Toast makeText(Context context, String message, int duration) {
return new Toast(context,message,duration);
}
public static Toast makeText(Context context, int resId, int duration) {
return new Toast(context,resId,duration);
}
public void show() {
if(mToast instanceof EToast2){
((EToast2) mToast).show();
}else if(mToast instanceof android.widget.Toast){
((android.widget.Toast) mToast).show();
}
}
public void cancel(){
if(mToast instanceof EToast2){
((EToast2) mToast).cancel();
}else if(mToast instanceof android.widget.Toast){
((android.widget.Toast) mToast).cancel();
}
}
public void setText(CharSequence s){
if(mToast instanceof EToast2){
((EToast2) mToast).setText(s);
}else if(mToast instanceof android.widget.Toast){
((android.widget.Toast) mToast).setText(s);
}
}
/**
* 用來判斷是否開啓通知權限
* */
private static boolean isNotificationEnabled(Context context){
if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT){
AppOpsManager mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
ApplicationInfo appInfo = context.getApplicationInfo();
String pkg = context.getApplicationContext().getPackageName();
int uid = appInfo.uid;
Class appOpsClass = null; /* Context.APP_OPS_MANAGER */
try {
appOpsClass = Class.forName(AppOpsManager.class.getName());
Method checkOpNoThrowMethod = appOpsClass.getMethod(CHECK_OP_NO_THROW, Integer.TYPE, Integer.TYPE, String.class);
Field opPostNotificationValue = appOpsClass.getDeclaredField(OP_POST_NOTIFICATION);
int value = (int)opPostNotificationValue.get(Integer.class);
return ((int)checkOpNoThrowMethod.invoke(mAppOps,value, uid, pkg) == AppOpsManager.MODE_ALLOWED);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}else{
return false;
}
}
}
總結
最後呢,經過這麼一波三折,東西終於弄完了,這邊也要特別感謝xiaogaofudao 幫忙測試和提供一些建議,非常感謝。
v2.0.1最新版本你已經上線了哦~經過本人和其他的一些用戶使用體驗,應該是可以的,如果大家在使用中發現什麼問題,歡迎及時聯繫哦~(づ ̄ 3 ̄)づ