Snackbar,你这是怎么了?

起因

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介绍

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容