android新版本(8.0以上)使用Toast的那些坑

華爲、三星等機型禁用通知權限後Toast不彈出

原因

查看Toast源碼後發現,Toast顯示要通過INotificationManager類來實現,而當通知禁用後,調用此類會返回異常,所以導致通知不顯示,源碼如下:

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) {
    // 權限禁用後走這裏,這裏是空方法,所以會發生既不crash又無響應的情況
  }
}

這是一個googlebug,部分小米手機重寫了Toast代碼,所以可以正常執行,我們可以通過反射的方式來暴力繞過,也就有了如下解決方式:

解決方法
public class ToastUtils {
    private static Object iNotificationManagerObj;

    /**
     * @param context
     * @param message
     */
    public static void show(Context context, String message) {
        show(context.getApplicationContext(), message, Toast.LENGTH_SHORT);
    }

    /**
     * @param context
     * @param message
     */
    public static void show(Context context, String message, int duration) {
        if (TextUtils.isEmpty(message)) {
            return;
        }
        //後setText 兼容小米默認會顯示app名稱的問題
        Toast toast = Toast.makeText(context, null, duration);
        toast.setText(message);
        if (isNotificationEnabled(context)) {
            toast.show();
        } else {
            showSystemToast(toast);
        }
    }

    /**
     * 顯示系統Toast
     */
    private static void showSystemToast(Toast toast) {
        try {
            Method getServiceMethod = Toast.class.getDeclaredMethod("getService");
            getServiceMethod.setAccessible(true);
            //hook INotificationManager
            if (iNotificationManagerObj == null) {
                iNotificationManagerObj = getServiceMethod.invoke(null);

                Class iNotificationManagerCls = Class.forName("android.app.INotificationManager");
                Object iNotificationManagerProxy = Proxy.newProxyInstance(toast.getClass().getClassLoader(), new Class[]{iNotificationManagerCls}, new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        //強制使用系統Toast
                        if ("enqueueToast".equals(method.getName())
                                || "enqueueToastEx".equals(method.getName())) {  //華爲p20 pro上爲enqueueToastEx
                            args[0] = "android";
                        }
                        return method.invoke(iNotificationManagerObj, args);
                    }
                });
                Field sServiceFiled = Toast.class.getDeclaredField("sService");
                sServiceFiled.setAccessible(true);
                sServiceFiled.set(null, iNotificationManagerProxy);
            }
            toast.show();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 消息通知是否開啓
     *
     * @return
     */
    private static boolean isNotificationEnabled(Context context) {
        NotificationManagerCompat notificationManagerCompat = NotificationManagerCompat.from(context);
        boolean areNotificationsEnabled = notificationManagerCompat.areNotificationsEnabled();
        return areNotificationsEnabled;
    }
}

內容相同Toast短時間不能重複彈出

原因

當我們重複點擊Toast時候,會連續彈出很多Toast,視覺體驗不好,於是網上流傳着這些解決方法:

Toast mToast;

public void showToast(String text) {
  if (mToast == null) {
    mToast = Toast.makeText(MainActivity.this, text, Toast.LENGTH_SHORT);
  } else {
    mToast.setText(text);
    mToast.setDuration(Toast.LENGTH_SHORT);
  }
  mToast.show();
}

這個方法在舊版本android上沒有問題,新版本當短時間顯示同一個Toast時,會顯示不出來。

文字相同且當前Toast正在顯示時,系統會認爲是誤觸操作,從而屏蔽當前顯示Toast請求。

出現這個問題據說是爲了防止某些流氓app一直彈出一個模仿系統界面的Toast從而導致系統癱瘓。

解決方法

這是系統的限制,想要繞過這個限制只能自定義Toast了,這裏我推薦git上的大神自定義版Toast——XToast(Git地址

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