起因
Snackbar相信大家都不陌生躁愿,Material Design樣式的消息通知叛本,簡(jiǎn)潔的使用方式,相信很多人都已經(jīng)替換掉Toast彤钟,改投Snackbar了来候。但就是這簡(jiǎn)簡(jiǎn)單單的一句代碼:
Snackbar.make(view, "This is the snackbar.", Snackbar.LENGTH_SHORT).show();
最近卻讓我焦頭爛額。到底是怎么回事呢逸雹?我這里寫(xiě)了個(gè)Demo來(lái)重現(xiàn)一些當(dāng)時(shí)的場(chǎng)景:
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();
代碼很簡(jiǎn)單营搅,就是在一個(gè)懸浮框里顯示一個(gè)Sanckbar云挟,本想著平時(shí)經(jīng)常使用的Snackbar在這也不會(huì)出什么問(wèn)題,可現(xiàn)實(shí)卻給了我重重的一擊:
這是怎么回事转质?平時(shí)在Activity用的好好的Snackbar怎么一到WindowManager就不行了呢园欣?
問(wèn)題的源頭
既然報(bào)錯(cuò)了,那我們先到NullPointerException的源頭看它一看:
private Snackbar(ViewGroup parent) {
mTargetParent = parent;
mContext = parent.getContext(); //就是這里報(bào)的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按道理應(yīng)該是我們傳入的view沸枯,怎么可能為空呢?難道這里的parent另有其人赂弓?看來(lái)我們還是得進(jìn)入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是先經(jīng)過(guò)一個(gè)findSuitableParent()方法調(diào)用的,聽(tīng)名字就知道肯定是這個(gè)方法搗的鬼盈魁。我們看看這個(gè)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;
}
方法的邏輯很簡(jiǎn)單翔怎,就是循環(huán)遍歷view的父視圖,如果某個(gè)父視圖是CoordinatorLayout或者已經(jīng)追溯到了Activity的content視圖就直接返回备埃,查找期間如果某個(gè)view是FrameLayout就將其設(shè)為fallback姓惑,作為備用褐奴,當(dāng)查找到視圖頂端還沒(méi)有找到合適的ViewGroup時(shí)就返回fallback變量按脚。
看了這段代碼,再看看我們之前傳入的view敦冬,整個(gè)視圖層級(jí)里辅搬,既沒(méi)有CoordinatorLayout,也沒(méi)有FrameLayout脖旱,又因?yàn)槭侵苯邮褂肳indowManager顯示的堪遂,所以也沒(méi)有content視圖,真是要啥啥沒(méi)有萌庆,自然最后的fallback為null溶褪,導(dǎo)致了后面的NullPointerException。
解決方案
相信看到這大家都明白了践险,我們平時(shí)在Activity里不管什么視圖都可以使用Snackbar猿妈,是因?yàn)锳ctivity本身有content視圖可以給Snackbar使用,所以就算我們本身的視圖層級(jí)里沒(méi)有CoordinatorLayout或者FrameLayout巍虫,Snackbar也是可以正常使用的彭则。但這在WindowManager中就不好使了,為保證Snackbar的正常使用占遥,我們的視圖層級(jí)必須包含CoordinatorLayout或者FrameLayout俯抖。這里我選擇使用FrameLayout作為根視圖,其他代碼不變:
ViewGroup root = new FrameLayout(MainActivity.this);
果然這么一改瓦胎,Snackbar可以正常顯示了:
進(jìn)一步探究
代碼雖然成功運(yùn)行了芬萍,可我還是有些疑惑尤揣,為什么Snackbar必須要使用CoordinatorLayout或者FrameLayout,其他的ViewGroup怎么就入不了Snackbar的法眼呢担忧?想到這清笨,我覺(jué)得我有必要繼續(xù)深究藏在Snackbar身后的秘密过蹂。
你Snackbar不允許我使用其他的ViewGroup,我倒要看看我用一用會(huì)怎么樣!
當(dāng)然暴备,這里普通的調(diào)用是沒(méi)法做到的,會(huì)直接報(bào)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();
這里我用反射直接構(gòu)造一個(gè)Snackbar猬仁,將我們剛才的RelativeLayout根視圖傳入構(gòu)造器。
運(yùn)行轧房!
原來(lái)如此拌阴,看來(lái)Snackbar的視圖的布局一定用了一些只有CoordinatorLayout和FrameLayout支持的屬性,如果使用其他的ViewGroup奶镶,Snackbar的顯示就會(huì)出現(xiàn)錯(cuò)誤迟赃。那我們?cè)倏纯碨nackbar的xml文件到底寫(xiě)了些什么:
<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"
來(lái)保證Snackbar顯示在視圖底部,而這個(gè)layout_gravity
屬性只有CoordinatorLayout和FrameLayout支持厂镇,這就是為什么Snackbar不能使用其他ViewGroup的原因纤壁。
另一個(gè)問(wèn)題
這個(gè)問(wèn)題是在我解決上面的問(wèn)題之后出現(xiàn)的:
其實(shí)原因很簡(jiǎn)單,大家可以翻到前面再看一下Snackbar的構(gòu)造方法捺信,里面有一句:
ThemeUtils.checkAppCompatTheme(mContext);
這一句是檢查Context是否使用的AppCompat主題酌媒,如果不是就會(huì)拋出IllegalArgumentException,因?yàn)楫?dāng)時(shí)是在Service里面創(chuàng)建的視圖迄靠,root視圖的Context自然也是用的Service秒咨,而Service是沒(méi)有Theme的,于是就產(chǎn)生了這個(gè)異常掌挚。解決的方法也很簡(jiǎn)單:
ContextThemeWrapper wrapper = new ContextThemeWrapper(serviceContext, R.style.Theme_AppCompat);
ViewGroup root = new RelativeLayout(wrapper);
那為什么Snackbar一定要求AppCompat主題呢雨席?其實(shí)也是因?yàn)閤ml文件內(nèi)部使用了AppCompat的屬性,我這里就不再展示了吠式,如果感興趣陡厘,可以自行查看。
結(jié)語(yǔ)
其實(shí)解決掉這個(gè)問(wèn)題之后再回頭看一看Snackbar的API介紹奇徒,解釋的也還算清楚:
但知其然雏亚,也要知其所以然,這樣才算真正的弄知識(shí)摩钙。