Snackbar,你这是怎么了?

作者: GavinLi369 | 来源:发表于2017-05-20 20:29 被阅读345次

    起因

    Snackbar相信大家都不陌生,Material Design样式的消息通知,简洁的使用方式,相信很多人都已经替换掉Toast,改投Snackbar了。但就是这简简单单的一句代码:

    Snackbar.make(view, "This is the snackbar.", Snackbar.LENGTH_SHORT).show();
    

    最近却让我焦头烂额。到底是怎么回事呢?我这里写了个Demo来重现一些当时的场景:

        WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
        ViewGroup root = new RelativeLayout(MainActivity.this);
        root.setBackgroundColor(getResources().getColor(R.color.colorPrimary));
        WindowManager.LayoutParams params = new WindowManager.LayoutParams();
        params.type = WindowManager.LayoutParams.TYPE_PHONE;
        params.format = PixelFormat.RGBA_8888;
        params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
        params.gravity = Gravity.START | Gravity.TOP;
        params.width = 400;
        params.height = 300;
        params.x = 70;
        params.y = 300;
        windowManager.addView(root, params);
        Snackbar.make(root, "This is the snackbar.", Snackbar.LENGTH_SHORT).show();
    

    代码很简单,就是在一个悬浮框里显示一个Sanckbar,本想着平时经常使用的Snackbar在这也不会出什么问题,可现实却给了我重重的一击:

    NullPointerException

    这是怎么回事?平时在Activity用的好好的Snackbar怎么一到WindowManager就不行了呢?

    问题的源头

    既然报错了,那我们先到NullPointerException的源头看它一看:

      private Snackbar(ViewGroup parent) {
            mTargetParent = parent;
            mContext = parent.getContext();  //就是这里报的NullPointerException
    
            ThemeUtils.checkAppCompatTheme(mContext);
    
            LayoutInflater inflater = LayoutInflater.from(mContext);
            mView = (SnackbarLayout) inflater.inflate(
                    R.layout.design_layout_snackbar, mTargetParent, false);
    
            mAccessibilityManager = (AccessibilityManager)
                    mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
        }
    

    看到这里我更迷惑了,这里的parent按道理应该是我们传入的view,怎么可能为空呢?难道这里的parent另有其人?看来我们还是得进入Snack.make()方法一探究竟:

        public static Snackbar make(@NonNull View view, @NonNull CharSequence text,
                @Duration int duration) {
            Snackbar snackbar = new Snackbar(findSuitableParent(view));
            snackbar.setText(text);
            snackbar.setDuration(duration);
            return snackbar;
        }
    

    可以看到,我们传入的view是先经过一个findSuitableParent()方法调用的,听名字就知道肯定是这个方法捣的鬼。我们看看这个findSuitableParent()到底做了什么:

        private static ViewGroup findSuitableParent(View view) {
            ViewGroup fallback = null;
            do {
                if (view instanceof CoordinatorLayout) {
                    // We've found a CoordinatorLayout, use it
                    return (ViewGroup) view;
                } else if (view instanceof FrameLayout) {
                    if (view.getId() == android.R.id.content) {
                        // If we've hit the decor content view, then we didn't find a CoL in the
                        // hierarchy, so use it.
                        return (ViewGroup) view;
                    } else {
                        // It's not the content view but we'll use it as our fallback
                        fallback = (ViewGroup) view;
                    }
                }
    
                if (view != null) {
                    // Else, we will loop and crawl up the view hierarchy and try to find a parent
                    final ViewParent parent = view.getParent();
                    view = parent instanceof View ? (View) parent : null;
                }
            } while (view != null);
    
            // If we reach here then we didn't find a CoL or a suitable content view so we'll fallback
            return fallback;
        }
    

    方法的逻辑很简单,就是循环遍历view的父视图,如果某个父视图是CoordinatorLayout或者已经追溯到了Activity的content视图就直接返回,查找期间如果某个view是FrameLayout就将其设为fallback,作为备用,当查找到视图顶端还没有找到合适的ViewGroup时就返回fallback变量。

    看了这段代码,再看看我们之前传入的view,整个视图层级里,既没有CoordinatorLayout,也没有FrameLayout,又因为是直接使用WindowManager显示的,所以也没有content视图,真是要啥啥没有,自然最后的fallback为null,导致了后面的NullPointerException。

    Activity的视图层级 直接使用WindowManger的视图层级

    解决方案

    相信看到这大家都明白了,我们平时在Activity里不管什么视图都可以使用Snackbar,是因为Activity本身有content视图可以给Snackbar使用,所以就算我们本身的视图层级里没有CoordinatorLayout或者FrameLayout,Snackbar也是可以正常使用的。但这在WindowManager中就不好使了,为保证Snackbar的正常使用,我们的视图层级必须包含CoordinatorLayout或者FrameLayout。这里我选择使用FrameLayout作为根视图,其他代码不变:

    ViewGroup root = new FrameLayout(MainActivity.this);
    

    果然这么一改,Snackbar可以正常显示了:

    正常运行

    进一步探究

    代码虽然成功运行了,可我还是有些疑惑,为什么Snackbar必须要使用CoordinatorLayout或者FrameLayout,其他的ViewGroup怎么就入不了Snackbar的法眼呢?想到这,我觉得我有必要继续深究藏在Snackbar身后的秘密。

    你Snackbar不允许我使用其他的ViewGroup,我倒要看看我用一用会怎么样!

    当然,这里普通的调用是没法做到的,会直接报NullPointerException,我们必须采取一些小手段:

        Constructor<Snackbar> snackbarConstructor = Snackbar.class
            .getDeclaredConstructor(ViewGroup.class);
        snackbarConstructor.setAccessible(true);
        Snackbar snackbar = snackbarConstructor.newInstance(root);
        snackbar.setText("This is the snackbar.");
        snackbar.setDuration(Snackbar.LENGTH_SHORT);
        snackbar.show();
    

    这里我用反射直接构造一个Snackbar,将我们刚才的RelativeLayout根视图传入构造器。

    运行!

    Snackbar显示出错

    原来如此,看来Snackbar的视图的布局一定用了一些只有CoordinatorLayout和FrameLayout支持的属性,如果使用其他的ViewGroup,Snackbar的显示就会出现错误。那我们再看看Snackbar的xml文件到底写了些什么:

    <view xmlns:android="http://schemas.android.com/apk/res/android"
          class="android.support.design.widget.Snackbar$SnackbarLayout"
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:layout_gravity="bottom"
          android:theme="@style/ThemeOverlay.AppCompat.Dark"
          style="@style/Widget.Design.Snackbar" />
    

    可以看到这里Snackbar使用了android:layout_gravity="bottom"来保证Snackbar显示在视图底部,而这个layout_gravity属性只有CoordinatorLayout和FrameLayout支持,这就是为什么Snackbar不能使用其他ViewGroup的原因。

    另一个问题

    这个问题是在我解决上面的问题之后出现的:

    IllegalArgumentException

    其实原因很简单,大家可以翻到前面再看一下Snackbar的构造方法,里面有一句:

    ThemeUtils.checkAppCompatTheme(mContext);
    

    这一句是检查Context是否使用的AppCompat主题,如果不是就会抛出IllegalArgumentException,因为当时是在Service里面创建的视图,root视图的Context自然也是用的Service,而Service是没有Theme的,于是就产生了这个异常。解决的方法也很简单:

    ContextThemeWrapper wrapper = new ContextThemeWrapper(serviceContext, R.style.Theme_AppCompat);
    ViewGroup root = new RelativeLayout(wrapper);
    

    那为什么Snackbar一定要求AppCompat主题呢?其实也是因为xml文件内部使用了AppCompat的属性,我这里就不再展示了,如果感兴趣,可以自行查看。

    结语

    其实解决掉这个问题之后再回头看一看Snackbar的API介绍,解释的也还算清楚:

    Snackbar API介绍

    但知其然,也要知其所以然,这样才算真正的弄知识。

    相关文章

      网友评论

      本文标题:Snackbar,你这是怎么了?

      本文链接:https://www.haomeiwen.com/subject/rfeexxtx.html