天天看點

【筆記】使Toast在不同版本系統下都能顯示的幾種方案

系統原生的Toast是用了INotificationManager類來顯示的, Android 5.0以上系統使用者隻要關閉了通知權限,在大部分手機上Toast也将不能顯示(有部分國産手機5.0以上的系統禁了通知權限仍能顯示Toast)。

/**
     * Show the view for the specified duration.
     */
    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
        }
    }
           

針對這種情況有以下幾種解決方案:

1、提醒使用者打開通知權限

2、讓系統認定為系統Toast(推薦)

3、使用Dialog或PopupWindow實作Toast(推薦)

1、提醒使用者打開通知權限

優點:有了通知權限其他都不是問題;

缺點:是否打開權限取決于使用者;

針對不同版本系統擷取通知權限是否打開的方法也不一樣,4.4之前的系統沒有通知權限預設true,4.4到7.0系統沒有直接擷取通知權限方法需要通過反射擷取,7.0以上系統可以使用NotificationManager#areNotificationsEnabled()方法直接擷取應用是否有通知權限。

/**
     * 檢查通知欄權限有沒有開啟
     */
    public static boolean isNotificationEnabled(Context context) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            return ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)).areNotificationsEnabled();
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            AppOpsManager appOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
            ApplicationInfo appInfo = context.getApplicationInfo();
            String pkg = context.getApplicationContext().getPackageName();
            int uid = appInfo.uid;
            try {
                Class<?> appOpsClass = Class.forName(AppOpsManager.class.getName());
                Method checkOpNoThrowMethod = appOpsClass.getMethod("checkOpNoThrow", Integer.TYPE, Integer.TYPE, String.class);
                Field opPostNotificationValue = appOpsClass.getDeclaredField("OP_POST_NOTIFICATION");
                int value = (Integer) opPostNotificationValue.get(Integer.class);
                return (Integer) checkOpNoThrowMethod.invoke(appOps, value, uid, pkg) == 0;
            } catch (NoSuchMethodException | NoSuchFieldException | InvocationTargetException | IllegalAccessException | RuntimeException | ClassNotFoundException ignored) {
                return true;
            }
        } else {
            return true;
        }
    }
           

雖然有些使用者是不小心關閉通知權限的,但某些使用者就是不想接受應用的通知才關閉的權限,讓他們打開很大可能也會選擇拒絕,這種方案隻能pass。

2、讓系統認定為系統Toast(推薦)

優點:對原先使用那套ToastUtil改動最小;

缺點:國内廠商對Android系統定制化嚴重,可能需要做機型相容,不過暫時隻發現華為P20;

檢視系統源碼可以看到NotificationManagerService.java裡有這樣一個判斷是否是系統Toast,判斷條件隻要兩者滿足其一就行,由此我們隻要将enqueueToast的參數pkg改成"android"即可。

【筆記】使Toast在不同版本系統下都能顯示的幾種方案

通過反射代理INotificationManager,在執行enqueueToast方法時讓系統認定為是原生Toast(但由于國内廠商對系統的定制化可能需要做其他相容性處理,目前隻知道華為P20有問題)。

隻需要在原來封裝的那套ToastUtil方法最終show的地方調用下面的show方法即可。

/**
     * 顯示
     *
     * @param context
     * @param toast
     */
    public static void show(Context context, Toast toast) {
        if (context == null || toast == null) {
            throw new RuntimeException("context 與 toast不能為null");
        }
        if (isNotificationEnabled(context)) {
            toast.show();
        } else {
            try {
                Method getServiceMethod = Toast.class.getDeclaredMethod("getService");
                getServiceMethod.setAccessible(true);
                if (iNotificationManagerObject == null) {
                    iNotificationManagerObject = getServiceMethod.invoke(null);
                    Class iNotificationManagerClazz = Class.forName("android.app.INotificationManager");
                    Object iNotificationManagerProxy = Proxy.newProxyInstance(toast.getClass().getClassLoader(), new Class[]{iNotificationManagerClazz}, new InvocationHandler() {
                        @Override
                        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                            if ("enqueueToast".equals(method.getName())//原生系統用了enqueueToast
                                    || "enqueueToastEx".equals(method.getName())//華為P20用了enqueueToastEx
                                    ) {
                                //強制變成系統Toast
                                args[0] = "android";
                            }
                            return method.invoke(iNotificationManagerObject, args);
                        }
                    });
                    Field field = Toast.class.getDeclaredField("sService");
                    field.setAccessible(true);
                    //替換Toast裡的sService
                    field.set(null, iNotificationManagerProxy);
                }
                toast.show();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            }
        }
    }
           

3、使用Dialog或PopupWindow實作Toast(推薦)

缺點:需要傳目前Activity的上下文;

優點:無需權限在頁面上顯示完全沒問題;

根據應用是否有通知權限分别顯示原生Toast和自定義實作的Toast,我這邊用了PopupWindow實作了一個Toast。

public void show() {
        initToast();
        startTimer();
        if (Util.isNotificationEnabled(activity)) {
            //有權限使用原生Toast
            if (contentView != null) {
                //視圖
                setCustomView();
                toast.setView(contentParentView);
            } else {
                //text
                toast.setGravity(gravity, offsetX, offsetY);
                toast.setText(text);
            }
            toast.show();
        } else {
            //無權限使用PopupWindow
            if (contentView != null) {
                //視圖
                setCustomView();
                popupWindow.setContentView(contentParentView);
            } else {
                //text
                ((TextView) defaultView.findViewById(R.id.tv_default_text)).setText(text);
                popupWindow.setContentView(defaultView);
            }
            if (!popupWindow.isShowing()) {
                popupWindow.showAtLocation(activity.getWindow().getDecorView(), gravity, offsetX, offsetY);
            }
        }
    }
           

用計時器來取消顯示PopupWindow

/**
     * 開始計時
     */
    private void startTimer() {
        stopTimer();
        timer = new Timer();
        timerTask = new TimerTask() {
            @Override
            public void run() {
                handler.post(runnable);
            }
        };
        timer.schedule(timerTask, duration == Toast.LENGTH_SHORT ? LENGTH_SHORT : LENGTH_LONG);
    }

    /**
     * 結束計時
     */
    private void stopTimer() {
        if (timer != null) {
            timer.cancel();
            timer = null;
        }
        if (timerTask != null) {
            timerTask.cancel();
            timerTask = null;
        }
    }
           

連續顯示View視圖土司時,如果直接設定View,

原生Toast會等上一個消失後再顯示;

而看PopupWindow#setContentView()方法可以發現如果PopupWindow已經顯示是不能再次設定contentView的;

這裡先讓Toast或PopupWindow添加一個内容容器,通過内容容器的addView和removeView實作良好的使用者體驗。

/**
     * 設定View視圖
     */
    private void setCustomView() {
        contentParentView.removeAllViews();
        if (contentView.getParent() != null) {
            ((ViewGroup) contentView.getParent()).removeView(contentView);
        }
        contentParentView.addView(contentView);
    }
           

在依賴包裡面也封裝了一個ToastViewUtil(具體使用),方法與ToastUtil一樣。

以上提供了幾種Toast在無權限下的解決方案,2、3都是不錯的選擇。

源碼