精华内容
下载资源
问答
  • 关闭toast权限后自定义解决方式

    千次阅读 2016-12-19 19:27:52
    前言 不知道大家是否遇到了当你们的App在5.0以上系统中被用户关闭消息通知后(其实用户本身只是想关闭Notification的,猜测),...直接跟踪Toast的源码,其实我们可以发现,果真Toast其实是通过NotificationManagerS

    前言

    不知道大家是否遇到了当你们的App在5.0以上系统中被用户关闭消息通知后(其实用户本身只是想关闭Notification的,猜测),系统的Toast也神奇的无法显示。当然这个问题并不复杂,有很多种解决方案,我们逐一探讨一下,然后来看看到底哪种方式会好一点。

    问题分析

    直接跟踪Toast的源码,其实我们可以发现,果真Toast其实是通过NotificationManagerService 维护一个toast队列,然后通知给Toast中的客户端 TN 调用 WindowManager 添加view。那么当用户关闭通知权限后自然也无法显示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
            }
        }
        ....
        static private INotificationManager getService() {
            if (sService != null) {
                return sService;
            }
            sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
            return sService;
        }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    解决思路

    这边就来说说我这边的几种解决方案,就是大致我能想到的,哈哈。

    自己仿照系统的Toast然后用自己的消息队列来维护,让其不受NotificationManagerService影响。 
    通过WindowManager自己来写一个通知。 
    通过Dialog、PopupWindow来编写一个自定义通知。 
    通过直接去当前页面最外层content布局来添加View。 
    仿照系统Toast自己来维护Toast消息队列

    通过WindowManager自己来写一个通知

    说起WindowManager,其实我对这个东西的第一印象就是强大,悬浮窗什么的其实都是通过WindowManager来实现的,那么我们来看看怎么实现,我就直接上代码了

    public class Toast {
        private Context mContext;
        private WindowManager wm;
        private int mDuration;
        private View mNextView;
        public static final int LENGTH_SHORT = 1500;
        public static final int LENGTH_LONG = 3000;
    
        public Toast(Context context) {
            mContext = context.getApplicationContext();
            wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
        }
    
        public static Toast makeText(Context context, CharSequence text,
                                     int duration) {
            Toast result = new Toast(context);
            View view = android.widget.Toast.makeText(context, text, android.widget.Toast.LENGTH_SHORT).getView();
            if (view != null){
                TextView tv = (TextView) view.findViewById(android.R.id.message);
                tv.setText(text);
            }
            result.mNextView = view;
            result.mDuration = duration;
            return result;
        }
    
        public static Toast makeText(Context context, int resId, int duration)
                throws Resources.NotFoundException {
            return makeText(context, context.getResources().getText(resId),duration);
        }
    
        public void show() {
            if (mNextView != null) {
                WindowManager.LayoutParams params = new WindowManager.LayoutParams();
                params.gravity = Gravity.CENTER | Gravity.CENTER_HORIZONTAL;
                params.height = WindowManager.LayoutParams.WRAP_CONTENT;
                params.width = WindowManager.LayoutParams.WRAP_CONTENT;
                params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                        | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
                        | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
                params.format = PixelFormat.TRANSLUCENT;
                params.windowAnimations = android.R.style.Animation_Toast;
                params.y = dip2px(mContext, 64);
                params.type = WindowManager.LayoutParams.TYPE_TOAST;
                wm.addView(mNextView, params);
                new Handler().postDelayed(new Runnable() {
    
                    @Override
                    public void run() {
                        if (mNextView != null) {
                            wm.removeView(mNextView);
                            mNextView = null;
                            wm = null;
                        }
                    }
                }, mDuration);
            }
        }
    
        /**
         * dip与px的转换
         *
         * @参数   @param context
         * @参数   @param dipValue
         * @返回值 int
         *
         */
        private int dip2px(Context context, float dipValue) {
            final float scale = context.getResources().getDisplayMetrics().density;
            return (int) (dipValue * scale + 0.5f);
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72

    嗯,这样写应该是没问题的,然后为啥没有效果呢??好吧,其实写了这么多,就是给自己挖坑,很明显,这个东西在现在的5.0以上机器中有一个悬浮窗权限,而且系统默认是关闭该权限的,只有用户手动打开才能显示,而且代码中也要添加如下一条权限。

    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
    • 1
    • 1

    那么问题又回来了,用户一般不会打开,这不是又白搞么。

    通过Dialog、PopupWindow来编写一个自定义通知。

    这个方案貌似也是可行的,代码就不写了,提醒一点就是一般来说Dialog和PopupWindow显示时有一个隔板,用户是无法点击其余部分控件的,所以记得加上以上属性。

    public static void setPopupWindowTouchModal(PopupWindow popupWindow,
                                                    boolean touchModal) {
            if (null == popupWindow) {
                return;
            }
            Method method;
            try {
                method = PopupWindow.class.getDeclaredMethod("setTouchModal",
                        boolean.class);
                method.setAccessible(true);
                method.invoke(popupWindow, touchModal);
    
            }
            catch (Exception e) {
                e.printStackTrace();
            }
        }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    通过直接去当前页面最外层content布局来添加View。

    说说这种方式吧,其实刚开始我也是没有想到的,因为一般很少回去直接拿Activity最外层的content布局去创建一个View并且显示在上面的。

    (ViewGroup) ((Activity) context).findViewById(android.R.id.content);
    • 1
    • 1

    其实我们是可以直接通过findViewById去直接拿到最外层布局的哦,当然context记得一定是Activity。 
    然后通过以下代码就可以直接把布局显示在当前content布局之上。

    ViewGroup container = (ViewGroup) ((Activity) context).findViewById(android.R.id.content);
    View v = ((Activity) context).getLayoutInflater().inflate(R.layout.etoast,container);
    • 1
    • 2
    • 1
    • 2

    这种方式是不是有点奇怪,好吧,我也是这么想的,不过感觉还是非常的实在的,也不复杂,东西也不多,直接上代码。

    public class EToast {
        public static final int LENGTH_SHORT = 0;
        public static final int LENGTH_LONG = 1;
        private static EToast result;
        //动画时间
        private final int ANIMATION_DURATION = 600;
        private static TextView mTextView;
        private ViewGroup container;
        private View v;
        //默认展示时间
        private int HIDE_DELAY = 2000;
        private LinearLayout mContainer;
        private AlphaAnimation mFadeOutAnimation;
        private AlphaAnimation mFadeInAnimation;
        private boolean isShow = false;
        private static Context mContext;
        private Handler mHandler = new Handler();
    
        private EToast(Context context) {
            mContext = context;
            container = (ViewGroup) ((Activity) context)
                    .findViewById(android.R.id.content);
            v = ((Activity) context).getLayoutInflater().inflate(
                    R.layout.etoast, container);
            mContainer = (LinearLayout) v.findViewById(R.id.mbContainer);
            mContainer.setVisibility(View.GONE);
            mTextView = (TextView) v.findViewById(R.id.mbMessage);
        }
    
        public static EToast makeText(Context context, String message, int HIDE_DELAY) {
            if(result == null){
                result = new EToast(context);
            }else{
                //这边主要是当切换Activity后我们应该更新当前持有的context,不然无法显示的
                if(!mContext.getClass().getName().equals(context.getClass().getName())){
                    result = new EToast(context);
                }
            }
            if(HIDE_DELAY == LENGTH_LONG){
                result.HIDE_DELAY = 2500;
            }else{
                result.HIDE_DELAY = 1500;
            }
            mTextView.setText(message);
            return result;
        };
        public static EToast makeText(Context context, int resId, int HIDE_DELAY) {
            String mes = "";
            try{
                mes = context.getResources().getString(resId);
            } catch (Resources.NotFoundException e) {
                e.printStackTrace();
            }
            return makeText(context,mes,HIDE_DELAY);
        }
        public void show() {
            if(isShow){
                //如果已经显示,则再次显示不生效
                return;
            }
            isShow = true;
            //显示动画
            mFadeInAnimation = new AlphaAnimation(0.0f, 1.0f);
            //消失动画
            mFadeOutAnimation = new AlphaAnimation(1.0f, 0.0f);
            mFadeOutAnimation.setDuration(ANIMATION_DURATION);
            mFadeOutAnimation
                    .setAnimationListener(new Animation.AnimationListener() {
                        @Override
                        public void onAnimationStart(Animation animation) {
                            //消失动画消失后记得刷新状态
                            isShow = false;
                        }
    
                        @Override
                        public void onAnimationEnd(Animation animation) {
                            //隐藏布局,没有remove主要是为了防止一个页面创建多次布局
                            mContainer.setVisibility(View.GONE);
                        }
    
                        @Override
                        public void onAnimationRepeat(Animation animation) {
                        }
                    });
            mContainer.setVisibility(View.VISIBLE);
    
            mFadeInAnimation.setDuration(ANIMATION_DURATION);
    
            mContainer.startAnimation(mFadeInAnimation);
            mHandler.postDelayed(mHideRunnable, HIDE_DELAY);
        }
    
        private final Runnable mHideRunnable = new Runnable() {
            @Override
            public void run() {
                mContainer.startAnimation(mFadeOutAnimation);
            }
        };
        public void cancel(){
            if(isShow) {
                isShow = false;
                mContainer.setVisibility(View.GONE);
                mHandler.removeCallbacks(mHideRunnable);
            }
        }
        //这个方法主要是为了解决用户在重启页面后单例还会持有上一个context,
        //并且上面的mContext.getClass().getName()其实是一样的
        //所以使用上还需在你们的BaseActivity的onDestroy()方法中调用该方法
        public static void reset(){
            result = null;
        }
        public void setText(CharSequence s){
            if(result == null) return;
            TextView mTextView = (TextView) v.findViewById(R.id.mbMessage);
            if(mTextView == null) throw new RuntimeException("This Toast was not created with Toast.makeText()");
            mTextView.setText(s);
        }
        public void setText(int resId) {
            setText(mContext.getText(resId));
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121

    简单说下吧,代码应该是很简单的,然后简单封装了和Toast相同的几个方法。嗯,其实大家也应该能发现我这边的布局其实是一直都在的,只是直接GONE掉了。所以呢,还是有待优化的地方,当然可以去想想是不是可以直接remove()掉什么的。我这边也没有用队列,我觉得在一个Toast显示的期间如果再需要显示另一个Toast,直接把当前的文本改过来就好了,没有必要搞个队列的,而且系统Toast我最厌恶的就是这个了,用户如果不停的点击,那Toast一个接一个的显示,这个我觉得是不合理的。上面的布局文件我也贴一下吧。有一点大家还是要注意下,因为我在完善的过程中其实遇到了很多种情况的BUG,所以最终需要大家再BaseActivity中的onDestory()方法中去手动调用一下EToast.reset();具体可以看源码中的解释。 
    etoast.xml

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:id="@+id/mbContainer"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingLeft="50dp"
        android:paddingRight="50dp"
        android:layout_marginBottom="50dp"
        android:gravity="bottom|center">
        <LinearLayout
            android:id="@+id/toast_linear"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/shape_eroast_bg"
            android:gravity="bottom|center"
            android:padding="5dp"
            android:orientation="vertical" >
    
            <TextView
                android:id="@+id/mbMessage"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:gravity="center"
                android:layout_margin="5dp"
                android:layout_gravity="center"
                android:textColor="#ffffffff"
                android:shadowColor="#BB000000"
                android:shadowRadius="2.75"/>
        </LinearLayout>
    </LinearLayout>
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    shape_eroast_bg.xml

    <?xml version="1.0" encoding="utf-8"?>
    <shape xmlns:android="http://schemas.android.com/apk/res/android">
        <!-- 实心 -->
        <solid
            android:color="@color/BlackTransparent" />
        <corners
            android:radius="45dp"
            />
    </shape>
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    优化

    上面的几种方式我大致也都走了一遍,其实我觉得都没啥区别,看你喜欢用哪种吧。我其实是采用了第四种,因为第一种的话我是不喜欢队列的,比如5个Toast排队还要一个一个等待显示,这样的体验我是不喜欢的。第二种就不推荐了,因为又涉及到了其他的权限。第三种我没试,实现应该是不难的,效果的话也是随你喜欢。最后我采用的是第四种,因为这种方式之前是没有用到过的,也尝试一下。

    那么来说说优化,如果直接替换掉系统的Toast,那相当的暴力,肯定妥妥的。那么我们能不能智能的去判断一下呢,如果用户没有关闭通知权限,那么久跟随系统的Toast去吧,这样好让App采用系统风格,对吧。 
    方法是有的,如下:

    /**
     * 用来判断是否开启通知权限
     * */
        private static boolean isNotificationEnabled(Context context){
    
            AppOpsManager mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
            ApplicationInfo appInfo = context.getApplicationInfo();
    
            String pkg = context.getApplicationContext().getPackageName();
    
            int uid = appInfo.uid;
    
            Class appOpsClass = null; /* Context.APP_OPS_MANAGER */
    
            try {
    
                appOpsClass = Class.forName(AppOpsManager.class.getName());
    
                Method checkOpNoThrowMethod = appOpsClass.getMethod(CHECK_OP_NO_THROW, Integer.TYPE, Integer.TYPE, String.class);
    
                Field opPostNotificationValue = appOpsClass.getDeclaredField(OP_POST_NOTIFICATION);
                int value = (int)opPostNotificationValue.get(Integer.class);
                return ((int)checkOpNoThrowMethod.invoke(mAppOps,value, uid, pkg) == AppOpsManager.MODE_ALLOWED);
    
            } catch (Exception e) {
                e.printStackTrace();
            }
            return true;
        }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    据说android24 可以使用NotificationManagerCompat.areNotificationsEnabled()来判断,具体大家可以尝试。那么如何来替换老项目中的Toast呢?
    我这边的话自定义Toast就是EToast了。为什么要E开头呢,因为公……你懂的。然后写一个Toast的工具类,如下:

    public class Toast {
        private static final String CHECK_OP_NO_THROW = "checkOpNoThrow";
        private static final String OP_POST_NOTIFICATION = "OP_POST_NOTIFICATION";
        private static int checkNotification = -1;
        private Object mToast;
        public static final int LENGTH_SHORT = 0;
        public static final int LENGTH_LONG = 1;
        private Toast(Context context, String message, int duration) {
            try{
                if (checkNotification == -1){
                    checkNotification = isNotificationEnabled(context) ? 0 : 1;
                }
    
                if (checkNotification == 1) {
                    mToast = EToast.makeText(context, message, duration);
                } else {
                    mToast = android.widget.Toast.makeText(context, message, duration);
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        private Toast(Context context, int resId, int duration) {
            if (checkNotification == -1){
                checkNotification = isNotificationEnabled(context) ? 0 : 1;
            }
    
            if (checkNotification == 1) {
                mToast = EToast.makeText(context, resId, duration);
            } else {
                mToast = android.widget.Toast.makeText(context, resId, duration);
            }
        }
    
        public static Toast makeText(Context context, String message, int duration) {
            return new Toast(context,message,duration);
        }
        public static Toast makeText(Context context, int resId, int duration) {
            return new Toast(context,resId,duration);
        }
    
        public void show() {
            if(mToast instanceof EToast){
                ((EToast) mToast).show();
            }else if(mToast instanceof android.widget.Toast){
                ((android.widget.Toast) mToast).show();
            }
        }
        public void cancel(){
            if(mToast instanceof EToast){
                ((EToast) mToast).cancel();
            }else if(mToast instanceof android.widget.Toast){
                ((android.widget.Toast) mToast).cancel();
            }
        }
        public void setText(int resId){
            if(mToast instanceof EToast){
                ((EToast) mToast).setText(resId);
            }else if(mToast instanceof android.widget.Toast){
                ((android.widget.Toast) mToast).setText(resId);
            }
        }
        public void setText(CharSequence s){
            if(mToast instanceof EToast){
                ((EToast) mToast).setText(s);
            }else if(mToast instanceof android.widget.Toast){
                ((android.widget.Toast) mToast).setText(s);
            }
        }
        /**
         * 用来判断是否开启通知权限
         * */
        private static boolean isNotificationEnabled(Context context){
    
            AppOpsManager mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
            ApplicationInfo appInfo = context.getApplicationInfo();
    
            String pkg = context.getApplicationContext().getPackageName();
    
            int uid = appInfo.uid;
    
            Class appOpsClass = null; /* Context.APP_OPS_MANAGER */
    
            try {
    
                appOpsClass = Class.forName(AppOpsManager.class.getName());
    
                Method checkOpNoThrowMethod = appOpsClass.getMethod(CHECK_OP_NO_THROW, Integer.TYPE, Integer.TYPE, String.class);
    
                Field opPostNotificationValue = appOpsClass.getDeclaredField(OP_POST_NOTIFICATION);
                int value = (int)opPostNotificationValue.get(Integer.class);
                return ((int)checkOpNoThrowMethod.invoke(mAppOps,value, uid, pkg) == AppOpsManager.MODE_ALLOWED);
    
            } catch (Exception e) {
                e.printStackTrace();
            }
            return true;
        }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99

    然后直接把你项目的import Android.widget.Toast 全局替换成import 你Toast的包名 即可。

    展开全文
  • toast

    千次阅读 2017-04-14 19:31:19
    ToastCompat欢迎各位提出改进意见及完善,欢迎star. ...有需求在关闭系统的通知权限后照样可以弹出toast 有需求可以弹出自定义view 参考资料参考toast的源码,系统维护是一个队列,同时也用到了aidl`p

    ToastCompat

    欢迎各位提出改进意见及完善,欢迎star.

    • 做这个的出发点是不管是测试还是本身的项目需求,需要搞一个toast,满足各式各样的需要,大概有这些,于是就有写了
      • 有需求的单列的toast
      • 有需求可以自定义弹出时间的toast
      • 有需求在关闭系统的通知权限后照样可以弹出toast
      • 有需求可以弹出自定义view
    • 参考资料
      • 参考toast的源码,系统维护是一个队列,同时也用到了aidl
        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
            }
        }


    • 参考okhttp源码,以前没有太在意,后来发现,不管是okhttp中的builder还是dialog中的,他们的功能都是设置参数,这种思想非常好
    • 使用方法
      • 直接拷贝源码到项目中,简单暴力直接,有点重口味了,同时源码也不多。
      • 直接引入库
        compile ‘com.gs:ToastCompat:1.0.0’

    具体调用api方法
    1.单列
    new CustomToast.Builder(this).setIsSingle(true).setText("你好").build()
    .show();

    2.自定义时间
    new CustomToast.Builder(this).setText("你好").setDuration(2000).build()
    .show();

    3.自定义View
    ImageView imageView = new ImageView(this);
    imageView.setImageResource(R.mipmap.ic_launcher);
    new CustomToast.Builder(this).setIsSingle(true).setView(imageView).setDuration(2000).build()
    .show();

    4.复合使用
    new CustomToast.Builder(this).setIsSingle(true).setText("你好").setDuration(2000).build()
    .show();

    效果展示
    Thanks
    [rongwu](https://github.com/rongwu/ToastCompat
    项目地址

    展开全文
  • 本文章已授权鸿洋微信公众号转载:Toast不显示了? 吐司弹不出来完美的解决方案:ToastUtils,接下来让我们来一步步开始分析这个问题是如何出现,解决的过程,以及解决的方法 首先我们先看一下大厂 APP 的弹吐司 ...

    本文章已授权鸿洋微信公众号转载:Toast不显示了?

    吐司弹不出来完美的解决方案:ToastUtils,接下来让我们来一步步开始分析这个问题是如何出现,解决的过程,以及解决的方法

    首先我们先看一下大厂 APP 的弹吐司

    疑问

    • 连吐司弹不出来的手机是个什么梗?

    • 是少部分机型问题还是大多数机型的问题?

    • 为什么关闭了通知栏权限弹不出来?

    • 为什么有的机型可以弹有的却不行?

    解答

    • 自从我的 ToastUtils 框架发布了之后,被问最多的一个问题,你的Toast框架关闭通知栏权限还能弹出来吗?我心想这 Toast 跟通知栏扯不上啥关系吧,但是既然有人这样问了,也只能半信半疑了,于是我便拿了我的小米8还有红米Note5进行了测试,发现并没有该问题,于是我统一回复,这个是兼容问题,极少数机型才可能出现的问题,为保证框架稳定性,不给予兼容

    • 于是还有人陆陆续续给我反馈了这个问题,反馈的人都是用华为机型出现的问题,我便开始重视起来,刚好有同事用的是华为 P9,我跟他借了一下手机,一借不要紧,一借一下午。估计同事的内心是崩溃的,因为这个问题被 100% 复现了,真的关闭通知栏权限后吐司弹不出来了

    • 于是我翻遍了 Toast 的源码,吐司底层是 WindowManager 实现的,但是这跟通知栏权限有什么关系呢?就算有关系也是和 NotificationManager 有关系,到底和通知栏权限扯上啥关系了呢?经过查看系统源码发现,吐司的创建是使用到了 WindowManager 去创建,但是显示吐司的时候使用了 INotificationManager ,看类名就知道肯定和 NotificationManager 有联系,这就是为什么关闭了通知栏权限后导致了吐司显示不出来的问题

    • 现在经过测试,大部分小米机型不会因为通知栏权限被关闭而原生的Toast弹不出来,而华为荣耀,三星等都会出现通知栏权限被关闭后导致原生Toast显示不出来,这可能是小米手机对这个吐司的显示做了特殊处理,这个问题在Github上排名前几的Toast框架都会出现,并且一些大厂的APP(除QQ微信和美团外)也会出现该问题

    吐司弹不出来的后果

    Toast是我们日常开发中最常用的类,如果我们的APP在通知栏推送的消息比较多,用户就会把我们的通知栏权限屏蔽了,但是这个会引起一个连带反应,就是应用中所有使用到 Toast 的地方都会显示不出来,彻底成为一个哑巴应用,例如以下情景:

    • 账户密码输入错误,吐司弹不出来

    • 用户网络支付失败,吐司弹不出来

    • 网络请求错误,吐司弹不出来

    • 双击退出应用,吐司弹不出来

    • 等等情况,只要用到原生 Toast 都显示不出来

    其实这是一个系统的Bug,谷歌为了让应用的 Toast 能够显示在其他应用上面,所以使用了通知栏相关的 API,但是这个 API 随着用户屏蔽通知栏而变得不可用,系统错误地认为你没有通知栏权限,从而间接导致 Toast 有 show 请求时被系统所拦截

    Toast 源码解析

    首先看一下 Toast 的构成

    再看一下 Toast 内部的 API

    里面还有一个内部类,再看一下内部的 API

    从这里我们不难推断,Toast 只是一个外观类,最终实现还是由其内部类来实现,由于这个内部类太长,这里放一下这个内部类的源码,简单过一遍就好

    private static class TN extends ITransientNotification.Stub {
        private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
    
        private static final int SHOW = 0;
        private static final int HIDE = 1;
        private static final int CANCEL = 2;
        final Handler mHandler;
    
        int mGravity;
        int mX, mY;
        float mHorizontalMargin;
        float mVerticalMargin;
    
    
        View mView;
        View mNextView;
        int mDuration;
    
        WindowManager mWM;
    
        String mPackageName;
    
        static final long SHORT_DURATION_TIMEOUT = 4000;
        static final long LONG_DURATION_TIMEOUT = 7000;
    
        TN(String packageName, @Nullable Looper looper) {
            // XXX This should be changed to use a Dialog, with a Theme.Toast
            // defined that sets up the layout params appropriately.
            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;
    
            mPackageName = packageName;
    
            if (looper == null) {
                // Use Looper.myLooper() if looper is not specified.
                looper = Looper.myLooper();
                if (looper == null) {
                    throw new RuntimeException(
                            "Can't toast on a thread that has not called Looper.prepare()");
                }
            }
            mHandler = new Handler(looper, null) {
                @Override
                public void handleMessage(Message msg) {
                    switch (msg.what) {
                        case SHOW: {
                            IBinder token = (IBinder) msg.obj;
                            handleShow(token);
                            break;
                        }
                        case HIDE: {
                            handleHide();
                            // Don't do this in handleHide() because it is also invoked by
                            // handleShow()
                            mNextView = null;
                            break;
                        }
                        case CANCEL: {
                            handleHide();
                            // Don't do this in handleHide() because it is also invoked by
                            // handleShow()
                            mNextView = null;
                            try {
                                getService().cancelToast(mPackageName, TN.this);
                            } catch (RemoteException e) {
                            }
                            break;
                        }
                    }
                }
            };
        }
    
        /**
         * schedule handleShow into the right thread
         */
        @Override
        public void show(IBinder windowToken) {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
        }
    
        /**
         * schedule handleHide into the right thread
         */
        @Override
        public void hide() {
            if (localLOGV) Log.v(TAG, "HIDE: " + this);
            mHandler.obtainMessage(HIDE).sendToTarget();
        }
    
        public void cancel() {
            if (localLOGV) Log.v(TAG, "CANCEL: " + this);
            mHandler.obtainMessage(CANCEL).sendToTarget();
        }
    
        public void handleShow(IBinder windowToken) {
            if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
                    + " mNextView=" + mNextView);
            // If a cancel/hide is pending - no need to show - at this point
            // the window token is already invalid and no need to do any work.
            if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
                return;
            }
            if (mView != mNextView) {
                // remove the old view if necessary
                handleHide();
                mView = mNextView;
                Context context = mView.getContext().getApplicationContext();
                String packageName = mView.getContext().getOpPackageName();
                if (context == null) {
                    context = mView.getContext();
                }
                mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
                // We can resolve the Gravity here by using the Locale for getting
                // the layout direction
                final Configuration config = mView.getContext().getResources().getConfiguration();
                final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
                mParams.gravity = gravity;
                if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
                    mParams.horizontalWeight = 1.0f;
                }
                if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
                    mParams.verticalWeight = 1.0f;
                }
                mParams.x = mX;
                mParams.y = mY;
                mParams.verticalMargin = mVerticalMargin;
                mParams.horizontalMargin = mHorizontalMargin;
                mParams.packageName = packageName;
                mParams.hideTimeoutMilliseconds = mDuration ==
                    Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
                mParams.token = windowToken;
                if (mView.getParent() != null) {
                    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
                    mWM.removeView(mView);
                }
                if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
                // Since the notification manager service cancels the token right
                // after it notifies us to cancel the toast there is an inherent
                // race and we may attempt to add a window after the token has been
                // invalidated. Let us hedge against that.
                try {
                    mWM.addView(mView, mParams);
                    trySendAccessibilityEvent();
                } catch (WindowManager.BadTokenException e) {
                    /* ignore */
                }
            }
        }
    
        private void trySendAccessibilityEvent() {
            AccessibilityManager accessibilityManager =
                    AccessibilityManager.getInstance(mView.getContext());
            if (!accessibilityManager.isEnabled()) {
                return;
            }
            // treat toasts as notifications since they are used to
            // announce a transient piece of information to the user
            AccessibilityEvent event = AccessibilityEvent.obtain(
                    AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
            event.setClassName(getClass().getName());
            event.setPackageName(mView.getContext().getPackageName());
            mView.dispatchPopulateAccessibilityEvent(event);
            accessibilityManager.sendAccessibilityEvent(event);
        }
    
        public void handleHide() {
            if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
            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) {
                    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
                    mWM.removeViewImmediate(mView);
                }
    
                mView = null;
            }
        }
    }
    复制代码

    只需要稍微简单看一下就看懂,Toast 底层就是用这个内部类去实现,请记住,这个内部类叫做 TN,字段名为 mTN,接下来先让我们看一下 Toast 中 cancel 方法的源码

    cancel最终还是调用了内部类 TN 中的同名方法,接下来再看 Toast 中 show 方法的源码

    仔细观察的同学就会发现了,这个 show 的方法可不是像 cancel 一样只调用了 TN 内部类中的同名方法,还调用了 INotificationManager 这个 API,其实不难发现,这个 INotificationManager 是系统的 AIDL,不信的话我们再看一下这个 INotificationManager

    我相信学过 AIDL 的同学会明白,这里不再讲 AIDL 相关知识,如需了解请自行百度

    重点讲一下 INotificationManager,这个 AIDL 由系统实现的一个类,不同系统这个 AIDL 所对应的类也不相同,这就充分说明了为什么导致小米的机型关闭了通知栏权限还可以显示,而华为就不行的原因,具体原因请再看源码

    因为这里传了应用的包名给系统通知栏,如果这个包名对应的APP的通知栏权限被关闭了,吐司自然也就弹不出来了

    那么该如何着手解决这个问题

    先思考一个问题,Toast 显示是使用了 INotificationManager,和通知栏有关系,而Toast 的创建是使用了 WindowManager,和通知栏没有关系,那么我们可不可以通过 WindowManager 的方式来创建类似于 Toast 一样的东西呢,答案也是可以的,只不过在过程中会遇到非常棘手的问题,接下来让我们解决这些遇到的问题

    首先创建一个 WindowManager 需要 一个 View 参数和 WindowManager.LayoutParams 参数,这里说一下 WindowManager.LayoutParams 的创建,直接复制 Toast 部分代码

    WindowManager.LayoutParams params = new WindowManager.LayoutParams();
    params.height = WindowManager.LayoutParams.WRAP_CONTENT;
    params.width = WindowManager.LayoutParams.WRAP_CONTENT;
    params.format = PixelFormat.TRANSLUCENT;
    // 找不到 com.android.internal.R.style.Animation_Toast
    // params.windowAnimations = com.android.internal.R.style.Animation_Toast;
    params.windowAnimations = -1;
    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;
    复制代码

    然后使用 WindowManager 调用 addView 显示,然后报了错

    android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?
    复制代码

    其原因在于我们使用了 type,为什么不能加 TYPE_TOAST,因为通知权限在关闭后设置显示的类型为Toast会报错,所以这里我们把这句代码注释掉,然后就可以显示出来了

    params.type = WindowManager.LayoutParams.TYPE_TOAST;
    复制代码

    WindowManager 没有吐司的显示效果

    其原因在于我们复制了 Toast 的部分代码,而其中的动画代码引用了系统 R 文件中资源,而我无法直接在 Java 代码中引用

    params.windowAnimations = com.android.internal.R.style.Animation_Toast;
    复制代码

    Java代码不能引用这个Style不代表XML就不行,在这里创建一个 Style 并且继承原生 Toast 样式,这里我们可以自定义,也可以直接使用系统的,为了和系统的样式统一,这里就直接使用系统的

    <style name="ToastAnimation" parent="@android:style/Animation.Toast">
        <!--<item name="android:windowEnterAnimation">@anim/toast_enter</item>-->
        <!--<item name="android:windowExitAnimation">@anim/toast_exit</item>-->
    </style>
    复制代码

    然后重新指定 params.windowAnimations 即可解决该问题

    params.windowAnimations = R.style.ToastAnimation;
    复制代码

    WindowManager 没有自动消失的问题

    首先 WindowManager 并不能像 Toast 显示后自动消失,如果要像 Toast 一样自动消失很容易,在 WindowManager 显示后发送一个定时关闭的任务,那么问题来了,这个显示的时间如何定义?系统 Toast 显示的时间是什么样子?首先我们需要先看一下 Toast 给我们提供的两个常量值

    从这张图上我们并没有发现什么有价值的东西,我们继续往下找,看看是什么地方引用了这些常量

    继续通过查看源码得知

    但是通过测试,短吐司显示的时长为2-3秒,而长吐司显示的时长是3-4秒,所以这两个值并不是吐司显示时长的毫秒数,那么我们该如何得出正确的毫秒数呢?这个问题就留给大家去思考,这里不做解答

    只能使用当前 Activity 创建 WindowManager 的缺陷

    发现一个问题,Activity 和 Application 同样是 Context 的子类,如果使用 Activity 获取的 WindowManager 对象可以创建出来,但是如果使用 Application 获取的 WindowManager 对象却报了错

    android.view.WindowManager$BadTokenException: Unable to add window -- token null is not for an application
    复制代码

    报错已经说得很清楚了,创建 WindowManager 不能使用 Application 对象去创建,也就是说只能通过 Activity 对象去创建 WindowManager

    那么问题来了,每次弹这种 “Toast” 需要当前 Activity 对象,这个问题对于常年使用框架的同学是致命的

    这里以我做的框架 ToastUtils 为例子,显示一个吐司是这样子调用的

    ToastUtils.show("我是吐司");
    复制代码

    如果要解决在关闭通知栏权限后吐司还能再弹出来的问题,就需要改成

    ToastUtils.show(MainActivity.this, "我是吐司");
    复制代码

    先说一下这个问题带来的影响吧,我是框架的作者,对于我来说,只需要在 ToastUtils 中 show 方法多添加一个 Activity 参数即可,但是对于使用框架的人,在更新完框架后,整个项目所有使用到这个ToastUtils.show()方法都会报错,需要多传入一个Activity 参数,相信他们的内心几乎是崩溃的,那么有没有一种好的办法解决这个问题,答案当然是有了,可以用一个冷门的 API

    Application.registerActivityLifecycleCallbacks(ActivityLifecycleCallbacks callback);
    复制代码

    这个 API 是在 安卓 4.0 之后才有的,而现在大多数设备已经在 安卓 5.0 及以上,所以这个 API 还是有前途的,接下看一下 ActivityLifecycleCallbacks 这个接口有什么方法吧

    public interface ActivityLifecycleCallbacks {
        void onActivityCreated(Activity activity, Bundle savedInstanceState);
        void onActivityStarted(Activity activity);
        void onActivityResumed(Activity activity);
        void onActivityPaused(Activity activity);
        void onActivityStopped(Activity activity);
        void onActivitySaveInstanceState(Activity activity, Bundle outState);
        void onActivityDestroyed(Activity activity);
    }
    复制代码

    看到这里,相信各位已经知道真相了,这个方法用于监听应用中 Activity 中的生命周期方法

    那么我们就可以通过这个 API 来获取当前和用户交互的 Activity 对象,从而完成让当前 Activity 对象去创建 WindowManager

    使用 WindowManager 实现 Toast 出现局限性的问题

    当然用 WindowManager 创建的 View 必然也会受 Activity 的限制,因为就只能显示这个 Activity 上,如果在其他界面上则会显示不了,而系统原生的 Toast 则可以出现别的界面上,那有没有什么解决办法呢?

    WindowManager 在没有悬浮窗权限的时候就只能显示依附于调用的 Activity,当有授予了悬浮窗权限之后,可以通过改变type参数来更改 WindowManager 显示范围,可以让这个 WindowManager 显示在其他界面之上,这样 Toast 就不会随着 Activity 的不可见而变得不可见

    // 判断是否为 Android 6.0 及以上系统并且有悬浮窗权限
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Settings.canDrawOverlays(mToast.getView().getContext())) {
        // 解决使用 WindowManager 创建的 Toast 只能显示在当前 Activity 的问题
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
        }else {
            params.type = WindowManager.LayoutParams.TYPE_PHONE;
        }
    }
    复制代码

    如何在原生 Toast 和 WindowManager 中取舍

    这样我们比对一组数据:

    类型 显示范围 需要参数 兼容性 效率 通知栏权限 悬浮窗权限
    原生 Toast 所有界面 Context子类 一般 需要 不需要
    WindowManager 当前Activity Activity子类 一般 不需要 不需要

    经过对比,原生的 Toast 的优势还是要大于 WindowManager 的,所以如果在有在通知栏权限的前提下,建议使用原生的 Toast,我们可以通过判断通知栏权限是否被关闭,来判断是来显示原生 Toast 还是 WindowManager,方法代码如下:

    /**
     * 检查通知栏权限有没有开启
     */
    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;
        }
    }
    复制代码

    详细的源码地址请戳这里

    Android技术讨论Q群:78797078

    展开全文
  • Android Toast,即便关闭了通知权限也会正常显示
  • 不需要任何权限Toast,禁止通知之后可以弹出的Toast

    前言

    由于项目需要,需要再用户禁止通知权限之后仍然能弹出Toast.网上现成的尝试了三种方法测试之后发现都不太符合公司的需求而没有使用,其中机型适配(点名小米MIUI8)占了很大部分原因.而且要求是不能加任何权限!!!

    下面看效果图:
    (源码在文章结尾)

    使用系统Toast和禁止权限后的自定义Toast

    • 使用方法很简单,没有做二次封装.上代码
    public void showToast(View view){
            if(CustomToast.isNotificationEnabled(this)){
                //通知可用使用原生Toast
                Toast.makeText(getApplicationContext(), "系统Toast", Toast.LENGTH_SHORT).show();
            }else{
                CustomToast.makeText(getApplicationContext(),"自定义Toast", CustomToast.LENGTH_SHORT).show();
            }
        }

    首先判断用户是否禁止了通知(有的时候其实用户只是想屏蔽推送通知),如果未禁止直接使用系统原生的Toast,否则用我们自定义的
    判断是否有权限的代码就不贴了,跟本文关系不大.需要注意的是,API19以上才可以判断权限,所以可以加一个判断

    public static boolean isNotificationEnabled(Context context) {
            //API 19以下的直接使用自定义的Toast
            if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.KITKAT) {
                //这里19以下的我直接使用了CustomToast
                return false;
            }
            AppOpsManager mAppOps = null;
            mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
            //此处省略N行代码
            ************;
    }
     public static CustomToast makeText(Context context, CharSequence text, int duration) {
            Toast toast = Toast.makeText(context, text, duration);
            CustomToast exToast = new CustomToast(context);
            exToast.toast = toast;
            exToast.setDuration(duration);
            exToast.setText(text);
            //添加到集合中,自己维护队列
            equeue.add(exToast);
            return exToast;
        }

    makeText就返回了一个实例对象,并把这个对象加到了equeue集合中.为了仿照系统弹Toast的时候一个显示完毕再弹出第二个.下面看show方法了.

     public void show() {
            //正在展示 或者 要展示的view为null
            if (isShow || mView == null) {
                return;
            }
            isShow = true;
            toast.setView(mView);
            //这里通过反射拿到toast的TN对象
            initTN();
            try {
                //展示toast
                show.invoke(mTN);
            } catch (Exception e) {
                e.printStackTrace();
            }
            //Toast到时间隐藏
            handler.postDelayed(hideRunnable, mDuration == LENGTH_SHORT ? LENGTH_SHORT : LENGTH_LONG);
        }

    hideRunnable只是调了一个hide方法

    private void hide() {
            if (!isShow) return;
            try {
                hide.invoke(mTN);
            } catch (Exception e) {
                e.printStackTrace();
            }
            isShow = false;
            equeue.remove(0);
            //队列中还有消息则继续show
            if (equeue.size() > 0) {
                equeue.get(0).show();
            }
        }

    该方法出自 突破小米悬浮窗权限控制–不需要权限的悬浮窗

    private void initTN() {
            try {
                Field tnField = toast.getClass().getDeclaredField("mTN");
                tnField.setAccessible(true);
                mTN = tnField.get(toast);
                show = mTN.getClass().getMethod("show");
                hide = mTN.getClass().getMethod("hide");
    
                Field tnParamsField = mTN.getClass().getDeclaredField("mParams");
                tnParamsField.setAccessible(true);
    
                WindowManager.LayoutParams params = (WindowManager.LayoutParams) tnParamsField.get(mTN);
                params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                        | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
    
                /**调用tn.show()之前一定要先设置mNextView*/
                Field tnNextViewField = mTN.getClass().getDeclaredField("mNextView");
                tnNextViewField.setAccessible(true);
                tnNextViewField.set(mTN, toast.getView());
            } catch (Exception e) {
                e.printStackTrace();
            }
            //这里设置toast的位置
            toast.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM, 0, 200);
        }

    简单的介绍到这里了.欢迎交流,
    参考文章:http://www.codexiu.cn/android/blog/25708/

    源码地址:https://github.com/jackyZuo/CustomToastDemo

    展开全文
  • 1.当通知权限被关闭时在华为等手机上Toast不显示; 2.Toast的队列机制在不同手机上可能会不相同; 3.Toast的BadTokenException问题; 当发现系统Toast存在问题时,不少同学都会采用自定义的TYPE_TOAST弹窗来实现...
  • WindowManager类是一个接口类,继承制ViewManager,...源码分析:WindowManager,TYPE_TOAST悬浮窗权限检测问题 WindowManager类是一个接口类,继承制ViewManager,Viewmanager是一个简单的接口类,很简...
  • 相信很多朋友发现了华为等部分手机把通知权限关闭之后会导致Toast无法正常弹出的情况。 原因:谷歌为了让应用的 Toast 能够显示在其他应用上面,所以使用了通知栏相关的 API,但是这个 API 随着用户屏蔽通知栏而变得...
  • miui8基于Toast实现自定义窗口,接点击事件,不需要权限android.permission.SYSTEM_ALERT_WINDOW,Toast的点击事件支持4.4 以上,如果想兼容4.4一下,只需要将窗口类型改为TYPE_SYSTEM_ALERT,就可以了,这个事需要...
  • 直接跟踪Toast的源码,发现Toast其实是通过NotificationManagerService维护一个toast队列,然后通知Toast的客户端TN调用WindowManager添加view,所以当关闭消息通知权限时toast是无法显示。/** * Show the view for...
  • 仔细想想可能权限的问题。后面网上搜索给出如下答案: 跟踪Toast的源代码,make方法省略,做了一些初始化的工作,show方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public void ...
  • Toast之悬浮窗(不需要权限

    千次阅读 2017-10-31 16:20:05
    概述现在有好多应用需要做一个悬浮窗的功能或者说是可以在其他应用的上面显示自己的界面的时候,大多数的操作是(WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE),这样做就避免不了需要申请权限 ...
  • 代替Toast/SnackBar新方式,使用WindowManager 无需申请权限 WIKI 中文说明 Usage allprojects { repositories { ... maven { url 'https://jitpack.io' } 复制代码implementation '...
  • 代替Toast/SnackBar新方式,使用WindowManager 无需申请权限WIKI中文说明Usageallprojects {  repositories { ...  maven { url 'https://jitpack.io' }implementation '...
  • Android项目实战--手机卫士30--读取应用的权限以及自定义Toast
  • 前言不知道大家是否遇到了当你们的App在5.0以上系统中被用户关闭消息通知后(其实用户本身只是想关闭Notification的,猜测),系统的Toast也神奇的无法显示。当然这个问题并不复杂,有很多种解决方案,我们逐一探讨...
  • 前言 不知道大家是否遇到了当你们的App在5.0以上系统中被用户关闭消息通知后(其实用户本身只是想...插楼~【Android】当关闭通知权限后无法显示Toast的解决方案V2.0 已经更新喽~推荐使用2.0.x以后的版本哦~这边以...
  • 前言既然能看到这篇博文,就说明你一定看过这个【Android】当关闭通知消息权限后无法显示系统Toast的解决方案 。然后,我很开心的告诉你,兄弟,可能你们的心病这次就解决了~当然,没看过上一篇博文的还是建议看下,...
  • Toast-详解

    2018-12-28 15:27:39
    Toast简介 问题 自2018年中旬依赖,发现Taost bug的...笔者在2017年第四季度时,遇到过在华为手机上权限影响问题。随着问题越来越多,整理一下Toast的问题。 资料 同学,你的系统Toast可能需要修复一下 皇叔推荐 ...
  • 当通知权限被关闭时在华为等手机上Toast不显示;2.Toast的队列机制在不同手机上可能会不相同;3.Toast的BadTokenException问题;当发现系统Toast存在问题时,不少同学都会采用自定义的TYPE_TOAST弹窗来实现相同效果...
  • 自定义一个有Toast的功能的View:package com.asfenqi.android.app.view;import android.app.Activity; import android.content.Context; import android.content.res.Resources; import android.os.Handler; import...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 389
精华内容 155
关键字:

toast权限