Android用戶關(guān)閉APP通知導(dǎo)致Toast不顯示的解決方案

一、發(fā)現(xiàn)問題

只是想關(guān)閉Notification, 但Toast意外躺槍不顯示了,我的第一想法這不科學(xué)啊,所以去看看源碼WTF?

二、定位問題:

源碼中可發(fā)現(xiàn)

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
    }
}

public void cancel() {
    mTN.hide();

    try {
        getService().cancelToast(mContext.getPackageName(), mTN);
    } catch (RemoteException e) {
        // Empty
    }
}

可以看到先是獲取一個(gè)服務(wù)INotificationManager service = getService();,顯示時(shí)調(diào)用其service.enqueueToast(pkg, tn, mDuration);

而這個(gè)INotificationManager在用戶關(guān)閉消息通知權(quán)限的同時(shí)被禁用了,所以我們的Toast無法顯示。

三、解決方案

經(jīng)過一番看源碼和在某一篇關(guān)于Toast源碼分析的博文中了解到

Toast的顯示路徑:

  1. 通過new Toast(Context context)或者makeText(...)方法實(shí)例化Toast對(duì)象
  2. 調(diào)用show()方法之后,實(shí)例會(huì)加入到一個(gè)TN變量(AIDL)的服務(wù)隊(duì)列中,而這個(gè)隊(duì)列由系統(tǒng)維護(hù)
  3. TN控制Toast的顯示和消息

解決思路就有了:
既然系統(tǒng)不允許我們調(diào)用Toast,那么我們就自立門戶——自己寫一個(gè)Toast出來。
我們自己參照Toast的源碼,重寫一份,最后show的時(shí)候,不進(jìn)入TN維護(hù)的隊(duì)列,我們自己用Handler+Queue來維護(hù)Toast的消息隊(duì)列。

public class CustomToast implements IToast {

   private static Handler mHandler = new Handler();

   /**
    * 維護(hù)toast的隊(duì)列
    */
   private static BlockingQueue<CustomToast> mQueue = new LinkedBlockingQueue<CustomToast>();

   /**
    * 原子操作:判斷當(dāng)前是否在讀取{**@linkplain **#mQueue 隊(duì)列}來顯示toast
    */
   protected static AtomicInteger mAtomicInteger = new AtomicInteger(0);

   private WindowManager mWindowManager;

   private long mDurationMillis;

   private View mView;

   private WindowManager.LayoutParams mParams;

   private Context mContext;

   public static IToast makeText(Context context, String text, long duration) {
      return new CustomToast(context).setText(text).setDuration(duration)
            .setGravity(Gravity.BOTTOM, 0, DisplayUtil.dip2px(context, 64));
   }

   public CustomToast(Context context) {

      mContext = context;
      mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
      mParams = new WindowManager.LayoutParams();
      mParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
      mParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
      mParams.format = PixelFormat.TRANSLUCENT;
      mParams.windowAnimations = android.R.style.Animation_Toast;
      mParams.type = WindowManager.LayoutParams.TYPE_TOAST;
      mParams.setTitle("Toast");
      mParams.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
                      WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
      // 默認(rèn)CustomToast在下方居中
      mParams.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
   }

   /**
    * Set the location at which the notification should appear on the screen.
    *
    * **@param **gravity
    * **@param **xOffset
    * **@param **yOffset
    */
   @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
   @Override
   public IToast setGravity(int gravity, int xOffset, int yOffset) {

      // We can resolve the Gravity here by using the Locale for getting
      // the layout direction
      final int finalGravity;
      if (Build.VERSION.SDK_INT >= 14) {
         final Configuration config = mView.getContext().getResources().getConfiguration();
         finalGravity = Gravity.getAbsoluteGravity(gravity, config.getLayoutDirection());
      } else {
         finalGravity = gravity;
      }
      mParams.gravity = finalGravity;
      if ((finalGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
         mParams.horizontalWeight = 1.0f;
      }
      if ((finalGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
         mParams.verticalWeight = 1.0f;
      }
      mParams.y = yOffset;
      mParams.x = xOffset;
      return this;
   }

   @Override
   public IToast setDuration(long durationMillis) {
      if (durationMillis < 0) {
         mDurationMillis = 0;
      }
      if (durationMillis == Toast.LENGTH_SHORT) {
         mDurationMillis = 2000;
      } else if (durationMillis == Toast.LENGTH_LONG) {
         mDurationMillis = 3500;
      } else {
         mDurationMillis = durationMillis;
      }
      return this;
   }

   /**
    * 不能和{**@link **#setText(String)}一起使用,要么{**@link **#setView(View)} 要么{**@link **#setView(View)}
    *
    * **@param **view 傳入view
    *
    * **@return **自身對(duì)象
    */
   @Override
   public IToast setView(View view) {
      mView = view;
      return this;
   }

   @Override
   public IToast setMargin(float horizontalMargin, float verticalMargin) {
      mParams.horizontalMargin = horizontalMargin;
      mParams.verticalMargin = verticalMargin;
      return this;
   }

   /**
    * 不能和{**@link **#setView(View)}一起使用,要么{**@link **#setView(View)} 要么{**@link **#setView(View)}
    *
    * **@param **text 字符串
    *
    * **@return **自身對(duì)象
    */
   @Override
   public IToast setText(String text) {

      // 模擬Toast的布局文件 com.android.internal.R.layout.transient_notification
      // 雖然可以手動(dòng)用java寫,但是不同廠商系統(tǒng),這個(gè)布局的設(shè)置好像是不同的,因此我們自己獲取原生Toast的view進(jìn)行配置

      View view = Toast.makeText(mContext, text, Toast.LENGTH_SHORT).getView();
      if (view != null) {
         TextView tv = (TextView) view.findViewById(android.R.id.message);
         tv.setText(text);
         setView(view);
      }

      return this;
   }

   @Override
   public void show() {
      // 1. 將本次需要顯示的toast加入到隊(duì)列中
      mQueue.offer(this);

      // 2. 如果隊(duì)列還沒有激活,就激活隊(duì)列,依次展示隊(duì)列中的toast
      if (0 == mAtomicInteger.get()) {
         mAtomicInteger.incrementAndGet();
         mHandler.post(mActivite);
      }
   }

   @Override
   public void cancel() {
      // 1. 如果隊(duì)列已經(jīng)處于非激活狀態(tài)或者隊(duì)列沒有toast了,就表示隊(duì)列沒有toast正在展示了,直接return
      if (0 == mAtomicInteger.get() && mQueue.isEmpty()) {
         return;
      }

      // 2. 當(dāng)前顯示的toast是否為本次要取消的toast,如果是的話
      // 2.1 先移除之前的隊(duì)列邏輯
      // 2.2 立即暫停當(dāng)前顯示的toast
      // 2.3 重新激活隊(duì)列
      if (this.equals(mQueue.peek())) {
         mHandler.removeCallbacks(mActivite);
         mHandler.post(mHide);
         mHandler.post(mActivite);
      }

      //TODO 如果一個(gè)Toast在隊(duì)列中的等候展示,當(dāng)調(diào)用了這個(gè)toast的取消時(shí),考慮是否應(yīng)該從對(duì)隊(duì)列中移除,看產(chǎn)品需求吧
   }

   private void handleShow() {
      if (mView != null) {
         if (mView.getParent() != null) {
            mWindowManager.removeView(mView);
         }
         mWindowManager.addView(mView, mParams);
      }
   }

   private void handleHide() {
      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) {
            mWindowManager.removeView(mView);
            // 同時(shí)從隊(duì)列中移除這個(gè)toast
            mQueue.poll();
         }
         mView = null;
      }
   }

   private static void activeQueue() {
      CustomToast miuiToast = mQueue.peek();
      if (miuiToast == null) {
         // 如果不能從隊(duì)列中獲取到toast的話,那么就表示已經(jīng)暫時(shí)完所有的toast了
         // 這個(gè)時(shí)候需要標(biāo)記隊(duì)列狀態(tài)為:非激活讀取中
         mAtomicInteger.decrementAndGet();
      } else {

         // 如果還能從隊(duì)列中獲取到toast的話,那么就表示還有toast沒有展示
         // 1. 展示隊(duì)首的toast
         // 2. 設(shè)置一定時(shí)間后主動(dòng)采取toast消失措施
         // 3. 設(shè)置展示完畢之后再次執(zhí)行本邏輯,以展示下一個(gè)toast
         mHandler.post(miuiToast.mShow);
         mHandler.postDelayed(miuiToast.mHide, miuiToast.mDurationMillis);
         mHandler.postDelayed(mActivite, miuiToast.mDurationMillis);
      }
   }

   private final Runnable mShow = new Runnable() {
      @Override
      public void run() {
         handleShow();
      }
   };

   private final Runnable mHide = new Runnable() {
      @Override
      public void run() {
         handleHide();
      }
   };

   private final static Runnable mActivite = new Runnable() {
      @Override
      public void run() {
         activeQueue();
      }
   };

}
四、使用方法

問題解決后,想到這是一個(gè)通用性的問題,可以搞一個(gè)庫出來共享,所以就打成了aar上傳到我們的maven私服,便于復(fù)用。
compile 'xsl.common:toaster:1.0.1'
Toaster實(shí)現(xiàn)了自定義的IToast接口,IToast的接口方法基本和原來的Toast相差無幾, 所以從系統(tǒng)的Toast轉(zhuǎn)到我們自定義的Toaster的成本極低,其實(shí)就是改個(gè)名字而已,其他用法完全一致。

//System Toast
Toast.makeText(MainActivity.this, "show System Toast", Toast.LENGTH_SHORT).show();
//Custom Toast
Toaster.makeText(this, "show Custom Toast", Toast.LENGTH_SHORT).show();
五、發(fā)散思維

還有什么別的解決思路?

自己仿照系統(tǒng)的Toast然后用自己的消息隊(duì)列來維護(hù),讓其不受NotificationManagerService影響。(本文采用)
通過WindowManager自己來寫一個(gè)通知。
通過Dialog、PopupWindow來編寫一個(gè)自定義通知。
通過直接去當(dāng)前頁面最外層content布局來添加View。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容