【笔记】使Toast在不同版本系统下都能显示的几种方案

13 篇文章 0 订阅

系统原生的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"即可。

通过反射代理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都是不错的选择。

源码

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值