Toast系列(五):還在被關閉通知無法顯示Toast所困擾?解決方案來了

開源庫地址:https://github.com/the-pig-of-jungle/smart-show

Toast工作原理依賴於通知,關閉應用通知權限後,Toast無法顯示。在發佈SmartShow1.0.0版的時候,我注意到了這個問題,立即用自己的手機(魅族pro 6 plus)對淘寶、優酷等知名app進行測試,發現關閉通知權限後,它們的“再按一次退出程序”的Toast無法顯示。因爲Toast的工作機制如此,我並沒有把它當做一個問題看待。但是在前兩篇文章發佈時,關閉通知權限依然能夠顯示Toast的呼聲之高,讓我不得不着手解決這個問題。

有不少Toast開源庫解決了該問題,採用獨立懸浮窗,也就是另起爐竈,廢棄了原生Toast,自行實現一套彈窗提示。我們知道,不同手機品牌設備的Toast外觀及彈出動畫不盡相同,直接強行統一成一種風格,並不一定符合開發者的意願,我們應該把是否定製Toast的權利交給開發者。另外,獨立懸浮窗本身也需要申請權限,用戶會關閉通知權限,難道就沒有可能關閉懸浮窗權限麼?

完美的解決方案是什麼?當通知權限關閉時,我們需要製造一個VirtualToast來代替,對開發者來說,在使用上和原生Toast毫無差別。對app用戶來說,在體驗上(彈窗的外觀、動畫)與原生一致。

乍一看,這個問題好像很棘手,換一個角度思考問題,則柳暗花明,哈哈。

先進行技術選型,基於上面提到的原因,我們採用Dialog而不是懸浮窗。

先分析一下,關閉通知權限後,Toast的工作流程卡在了哪裏。Toast的工作流程是一個基於Binder的IPC(進程通信)過程,應用程序作爲客戶端僅僅發起Toast請求和被動接受回調。系統服務負責管理請求隊列、Token等,並通過Toast的內部類Tn來實現與應用程序的交互,即通過回調Tn的show/hide方法來顯示/隱藏Toast窗口。關閉了通知權限,導致無法與系統服務“通信”,最終無法添加Toast窗口。

不過當調用Toast的show方法時,此時添加窗口所需的View及各種窗口參數已全部準備就緒。

public class Toast{

 public void show() {
        ...
        //創建tn,筆者註釋
        TN tn = mTN;
        //窗口View,筆者註釋
        tn.mNextView = mNextView;
        ...
    }

}
private static class TN extends ITransientNotification.Stub {
       ...
       TN(String packageName, @Nullable Looper looper) {
            ...
            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;
            ...

        }
      ...
}

如此,當通知權限關閉,我們只需在調用Toast的show方法前用Dialog“截胡”,將其View作爲Dialog的ContentView。

    public void showToast() {
        applySetting();
        //如果具有通知權限,正常顯示Toast
        if (Utils.isNotificationPermitted()) {
            mToast.show();
        } else {
            //無通知權限,用Dialog代替
            VirtualToastManager.get().show(getToastType(), mToast, mWindowParams);
        }
    }

將Toast窗口的參數設置給Dialog

public void show(int toastType, Toast toast, WindowManager.LayoutParams windowParams) {
        //獲取棧頂activity
        Activity activity = ActivityStack.getTop();
        //若activity生命週期不符合條件,則什麼都不做
        if (!Utils.isUpdateActivityUIPermitted(activity)) {
            EasyLogger.d("activity is can not show virtual toast dialog ,so do nothing but return.");
            return;
        }
        ...
        //取出Dialog窗口布局參數,原樣複製Toast窗口的佈局參數
        WindowManager.LayoutParams lp = virtualToastDialog.getWindow().getAttributes();
        //窗口寬度
        lp.width = windowParams.width;
        //窗口高度
        lp.height = windowParams.height;
        //窗口動畫
        lp.windowAnimations = windowParams.windowAnimations;
        //窗口gravity
        lp.gravity = toast.getGravity();
        //窗口x,y座標
        lp.x = toast.getXOffset();
        lp.y = toast.getYOffset();

        ViewGroup content = virtualToastDialog.findViewById(android.R.id.content);
        if (toast.getView().getParent() != content) {
            if (toast.getView().getParent() != null) {
                ViewGroup parent = (ViewGroup) toast.getView().getParent();
                parent.removeView(toast.getView());
            }
            content.removeAllViews();
            virtualToastDialog.setContentView(toast.getView());
        }
        try {
            virtualToastDialog.show();
        } catch (WindowManager.BadTokenException e) {
            EasyLogger.e("bad token has happened when show virtual toast!");
            mHostActivity = null;
        }
        ...
}

去除Dialog顯示時周圍變暗的特性

virtualToastDialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);

Dialog不響應back鍵

      virtualToastDialog.getWindow().setFlags(
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);

如此這般VirtualToastDialog的外觀及動畫與目標設備的原生Toast完全一致了。就剩下間隔一段時間自動消失的控制了。

定義一個Handler,延時特定時間後,執行隱藏VirtualToastDialog的代碼。

        Runnable dismissRunnable = toastType == BaseToastManager.PLAIN_TOAST ? mDismissPlainToastRunnable
                : mDismissTypeToastRunnable;
        mDismissHandler.removeCallbacks(dismissRunnable);
        mDismissHandler.postDelayed(dismissRunnable, toast.getDuration() == Toast.LENGTH_SHORT ?
                DURATION_SHORT : DURATION_LONG);

這裏,始終複用同一個runnable任務即可,並且在發起一個新的延時隱藏時,我們要清除所有已存在的延時隱藏任務。假如,在上一個隱藏任務還差200毫秒執行的時候,主動調用了隱藏方法使Toast消失,那麼再顯示一個‘’新的‘’Toast時,200毫秒後就被自動隱藏了。所以mDismissHandler.removeCallbacks(dismissRunnable)起到歸位的作用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章