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

一启具、發(fā)現(xiàn)問(wèn)題

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

二拷沸、定位問(wèn)題:

源碼中可發(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在用戶(hù)關(guān)閉消息通知權(quán)限的同時(shí)被禁用了,所以我們的Toast無(wú)法顯示堵漱。

三综慎、解決方案

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

Toast的顯示路徑:

  1. 通過(guò)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示惊,那么我們就自立門(mén)戶(hù)——自己寫(xiě)一個(gè)Toast出來(lái)。
我們自己參照Toast的源碼愉镰,重寫(xiě)一份米罚,最后show的時(shí)候,不進(jìn)入TN維護(hù)的隊(duì)列丈探,我們自己用Handler+Queue來(lái)維護(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ì)列}來(lái)顯示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寫(xiě)隘竭,但是不同廠(chǎng)商系統(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ì)列還沒(méi)有激活动看,就激活隊(duì)列,依次展示隊(duì)列中的toast
      if (0 == mAtomicInteger.get()) {
         mAtomicInteger.incrementAndGet();
         mHandler.post(mActivite);
      }
   }

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

      // 2. 當(dāng)前顯示的toast是否為本次要取消的toast,如果是的話(huà)
      // 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的話(huà)奶甘,那么就表示已經(jīng)暫時(shí)完所有的toast了
         // 這個(gè)時(shí)候需要標(biāo)記隊(duì)列狀態(tài)為:非激活讀取中
         mAtomicInteger.decrementAndGet();
      } else {

         // 如果還能從隊(duì)列中獲取到toast的話(huà)篷店,那么就表示還有toast沒(méi)有展示
         // 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();
      }
   };

}
四甩十、使用方法

問(wèn)題解決后船庇,想到這是一個(gè)通用性的問(wèn)題吭产,可以搞一個(gè)庫(kù)出來(lái)共享侣监,所以就打成了aar上傳到我們的maven私服,便于復(fù)用臣淤。
compile 'xsl.common:toaster:1.0.1'
Toaster實(shí)現(xiàn)了自定義的IToast接口橄霉,IToast的接口方法基本和原來(lái)的Toast相差無(wú)幾, 所以從系統(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ì)列來(lái)維護(hù)钱慢,讓其不受NotificationManagerService影響逮京。(本文采用)
通過(guò)WindowManager自己來(lái)寫(xiě)一個(gè)通知。
通過(guò)Dialog束莫、PopupWindow來(lái)編寫(xiě)一個(gè)自定義通知懒棉。
通過(guò)直接去當(dāng)前頁(yè)面最外層content布局來(lái)添加View。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末览绿,一起剝皮案震驚了整個(gè)濱河市策严,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌妻导,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,265評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件倔韭,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡狐肢,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén)份名,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人妓美,你說(shuō)我怎么就攤上這事『埃” “怎么了辰如?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,852評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀(guān)的道長(zhǎng)贵试。 經(jīng)常有香客問(wèn)我琉兜,道長(zhǎng),這世上最難降的妖魔是什么毙玻? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,408評(píng)論 1 283
  • 正文 為了忘掉前任豌蟋,我火速辦了婚禮,結(jié)果婚禮上桑滩,老公的妹妹穿的比我還像新娘梧疲。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布幌氮。 她就那樣靜靜地躺著缭受,像睡著了一般。 火紅的嫁衣襯著肌膚如雪该互。 梳的紋絲不亂的頭發(fā)上米者,一...
    開(kāi)封第一講書(shū)人閱讀 49,772評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音宇智,去河邊找鬼塘雳。 笑死,一個(gè)胖子當(dāng)著我的面吹牛普筹,可吹牛的內(nèi)容都是我干的败明。 我是一名探鬼主播,決...
    沈念sama閱讀 38,921評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼太防,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼妻顶!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起蜒车,我...
    開(kāi)封第一講書(shū)人閱讀 37,688評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤讳嘱,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后酿愧,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體沥潭,經(jīng)...
    沈念sama閱讀 44,130評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評(píng)論 2 325
  • 正文 我和宋清朗相戀三年嬉挡,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了钝鸽。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,617評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡庞钢,死狀恐怖拔恰,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情基括,我是刑警寧澤颜懊,帶...
    沈念sama閱讀 34,276評(píng)論 4 329
  • 正文 年R本政府宣布,位于F島的核電站风皿,受9級(jí)特大地震影響河爹,放射性物質(zhì)發(fā)生泄漏桐款。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評(píng)論 3 312
  • 文/蒙蒙 一炊苫、第九天 我趴在偏房一處隱蔽的房頂上張望侨艾。 院中可真熱鬧,春花似錦拓挥、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,740評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)赁炎。三九已至,卻和暖如春徙垫,著一層夾襖步出監(jiān)牢的瞬間姻报,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,967評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工损肛, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留荣瑟,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,315評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親未檩。 傳聞我的和親對(duì)象是個(gè)殘疾皇子孙蒙,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容