现象:

在 xml 中指定了 android:layout_width="match_parent" 但 BottomSheetDialog 依然无法横向填充整个屏幕

原因:

首先 BottomSheetDialog 它只是一个 Dialog,继承于 Dialog ,所以有和 Dialog 相同的特性,和普通 Dialog 显示视图的方式相同。
设置视图使用的是 setContentView 方法,将视图或者视图 id 传入,它会将给定的视图填充到 layout.design_bottom_sheet_dialog 中 id 为 design_bottom_sheet 的 FrameLayout 中,且它的上级视图的宽度都为 match_parent
在 onCreate 中会将该视图依附的 window 的 layout 的宽高都设置为 match_parent :window.setLayout(-1, -1)。
继续往上追溯,Activity 与 Dialog 都是一个 Window 所依附 ,从他们的 style 中来看,找到 Dialog 默认的 style : Base.V7.ThemeOverlay.AppCompat.Dialog 其中有一个属性
<item name="android:windowIsFloating">true</item>
该属性两者不同,dialog 为 true ,Activity 为 false。
再看 Dialog 类中的 setContentView 方法,其实就是调用了 mWindow.setContentView(layoutResID), 而它的 winodw 就是 PhoneWindow
找到 PhoneWindow 中的 setContentView 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}

if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID, getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}

源码地址

如果 mContentParent 为 null 则执行创建 DecorView 逻辑,找到该方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
// Set up decor part of UI to ignore fitsSystemWindows if appropriate.
mDecor.makeFrameworkOptionalFitsSystemWindows();
final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(R.id.decor_content_parent);
......
省略大段代码
}
......
省略大段代码
}

源码地址

发现它主要是使用 generateLayout 来创建的

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
protected DecorView generateDecor(int featureId) {
// System process doesn't have application context and in that case we need to directly use
// the context we have. Otherwise we want the application context, so we don't cling to the
// activity.
Context context;
if (mUseDecorContext) {
Context applicationContext = getContext().getApplicationContext();
if (applicationContext == null) {
context = getContext();
} else {
context = new DecorContext(applicationContext, this);
if (mTheme != -1) {
context.setTheme(mTheme);
}
}
} else {
context = getContext();
}
return new DecorView(context, featureId, this, getAttributes());
}

protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.

TypedArray a = getWindowStyle();

if (false) {
System.out.println("From style:");
String s = "Attrs:";
for (int i = 0; i < R.styleable.Window.length; i++) {
s = s + " " + Integer.toHexString(R.styleable.Window[i]) + "=" + a.getString(i);
}
System.out.println(s);
}

mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);
int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR) & (~getForcedWindowFlags());
if (mIsFloating) {
setLayout(WRAP_CONTENT, WRAP_CONTENT); // 关键点是这句话
setFlags(0, flagsToUpdate);
} else {
setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate);
getAttributes().setFitInsetsSides(0);
getAttributes().setFitInsetsTypes(0);
}

if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
requestFeature(FEATURE_NO_TITLE);
} else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
// Don't allow an action bar if there is no title.
requestFeature(FEATURE_ACTION_BAR);
}

......
省略大段代码
}

源码地址

关键点我已经在代码中标注出来了,因为 dialog 的 windowIsFloating 为 true(前面说过) 所以 winodw 的宽高也就变成了 WRAP_CONTENT

如果按照原 demo 的写法,先执行 super.onCreate 再 setContentView ,winodw 的宽高先被设置为 MATCH_PARENT 然后又被 WRAP_CONTENT 覆盖,所以导致视图的宽为 WRAP_CONTENT ,看起来异常。

修改方式:

方式一:将 setContentView 和 super.onCreate 的调用顺序调换一下。
方式二:在代码中重新设置宽高。