部分特殊View的WindowInsets分发逻辑
部分特殊View的WindowInsets分发逻辑
部分特殊 View 的 WindowInsets 分发逻辑
这些 view 都是设置了 OnApplyWindowInsetsListener
,不会走默认的分发 WindowInsets 逻辑(即默认设置 padding),都是自行处理 WindowsInsets。
AppBarLayout
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
public AppBarLayout() {
ViewCompat.setOnApplyWindowInsetsListener(
this,
new androidx.core.view.OnApplyWindowInsetsListener() {
@Override
public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) {
return onWindowInsetChanged(insets);
}
});
}
WindowInsetsCompat onWindowInsetChanged(final WindowInsetsCompat insets) {
WindowInsetsCompat newInsets = null;
if (ViewCompat.getFitsSystemWindows(this)) {
// If we're set to fit system windows, keep the insets
newInsets = insets;
}
// If our insets have changed, keep them and trigger a layout...
if (!ObjectsCompat.equals(lastInsets, newInsets)) {
lastInsets = newInsets;
updateWillNotDraw();
requestLayout();
}
return insets;
}
- 作为 CoordinatorLayout 的直接孩子,不然有些功能可能失效
- 需要设置
fitsSystemWindows
为 true,否则 lastInsets 记录的为 null - AppBarLayout 不消费 WindowInsets;记录最后的 lastInsets,只处理 top,在 measure 和 layout 时,加上 top 偏移
CoordinateLayout
- CoordinateLayout 本身设置了
fitsSystemWindow=true
属性 - 子 View 设置了
fitsSystemWindow=true
属性 - WindowInsets 分发给了子 View 的 Behavior
与 DrawerLayout 类似,通过设置 OnApplyWindowInsetsListener 来改变它的 dispatchApply 逻辑,与 DrawerLayout 最大的区别在于它对子 view 的分发是通过 Behavior 实现的。
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
@Override
public void setFitsSystemWindows(boolean fitSystemWindows) {
super.setFitsSystemWindows(fitSystemWindows);
setupForInsets();
}
private void setupForInsets() {
if (Build.VERSION.SDK_INT < 21) {
return;
}
if (ViewCompat.getFitsSystemWindows(this)) { // 设置了fitsSystemWindow=true属性
if (mApplyWindowInsetsListener == null) {
mApplyWindowInsetsListener =
new androidx.core.view.OnApplyWindowInsetsListener() {
@Override
public WindowInsetsCompat onApplyWindowInsets(View v,
WindowInsetsCompat insets) {
return setWindowInsets(insets);
}
};
}
// First apply the insets listener
ViewCompat.setOnApplyWindowInsetsListener(this, mApplyWindowInsetsListener);
// Now set the sys ui flags to enable us to lay out in the window insets
setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
} else {
ViewCompat.setOnApplyWindowInsetsListener(this, null);
}
}
final WindowInsetsCompat setWindowInsets(WindowInsetsCompat insets) {
if (!ObjectsCompat.equals(mLastInsets, insets)) {
mLastInsets = insets;
mDrawStatusBarBackground = insets != null && insets.getSystemWindowInsetTop() > 0;
setWillNotDraw(!mDrawStatusBarBackground && getBackground() == null);
// Now dispatch to the Behaviors
insets = dispatchApplyWindowInsetsToBehaviors(insets);
requestLayout();
}
return insets;
}
private WindowInsetsCompat dispatchApplyWindowInsetsToBehaviors(WindowInsetsCompat insets) {
if (insets.isConsumed()) {
return insets;
}
for (int i = 0, z = getChildCount(); i < z; i++) {
final View child = getChildAt(i);
if (ViewCompat.getFitsSystemWindows(child)) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Behavior b = lp.getBehavior();
if (b != null) {
// If the view has a behavior, let it try first
insets = b.onApplyWindowInsets(this, child, insets);
if (insets.isConsumed()) {
// If it consumed the insets, break
break;
}
}
}
}
return insets;
}
DrawerLayout
- DrawerLayout 本身设置了
fitsSystemWindow=true
属性 - 子 View 设置了
fitsSystemWindow=true
属性 - WindowInsets 分发给了子 View 的 dispatchApplyWindowInsets
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
if (ViewCompat.getFitsSystemWindows(this)) {
if (Build.VERSION.SDK_INT >= 21) {
setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
@Override
public WindowInsets onApplyWindowInsets(View view, WindowInsets insets) {
final DrawerLayout drawerLayout = (DrawerLayout) view;
drawerLayout.setChildInsets(insets, insets.getSystemWindowInsetTop() > 0);
return insets.consumeSystemWindowInsets();
}
});
setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
final TypedArray a = context.obtainStyledAttributes(THEME_ATTRS);
try {
mStatusBarBackground = a.getDrawable(0);
} finally {
a.recycle();
}
} else {
mStatusBarBackground = null;
}
}
public void setChildInsets(Object insets, boolean draw) {
mLastInsets = insets;
mDrawStatusBarBackground = draw;
setWillNotDraw(!draw && getBackground() == null);
requestLayout();
}
// 在onMeasure
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final boolean applyInsets = mLastInsets != null && ViewCompat.getFitsSystemWindows(this);
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (applyInsets) {
final int cgrav = GravityCompat.getAbsoluteGravity(lp.gravity, layoutDirection);
if (ViewCompat.getFitsSystemWindows(child)) {
if (Build.VERSION.SDK_INT >= 21) {
WindowInsets wi = (WindowInsets) mLastInsets;
if (cgrav == Gravity.LEFT) {
wi = wi.replaceSystemWindowInsets(wi.getSystemWindowInsetLeft(),
wi.getSystemWindowInsetTop(), 0,
wi.getSystemWindowInsetBottom());
} else if (cgrav == Gravity.RIGHT) {
wi = wi.replaceSystemWindowInsets(0, wi.getSystemWindowInsetTop(),
wi.getSystemWindowInsetRight(),
wi.getSystemWindowInsetBottom());
}
child.dispatchApplyWindowInsets(wi);
}
} else {
if (Build.VERSION.SDK_INT >= 21) {
WindowInsets wi = (WindowInsets) mLastInsets;
if (cgrav == Gravity.LEFT) {
wi = wi.replaceSystemWindowInsets(wi.getSystemWindowInsetLeft(),
wi.getSystemWindowInsetTop(), 0,
wi.getSystemWindowInsetBottom());
} else if (cgrav == Gravity.RIGHT) {
wi = wi.replaceSystemWindowInsets(0, wi.getSystemWindowInsetTop(),
wi.getSystemWindowInsetRight(),
wi.getSystemWindowInsetBottom());
}
lp.leftMargin = wi.getSystemWindowInsetLeft();
lp.topMargin = wi.getSystemWindowInsetTop();
lp.rightMargin = wi.getSystemWindowInsetRight();
lp.bottomMargin = wi.getSystemWindowInsetBottom();
}
}
}
// ...
}
}
DrawableLayout,自己设置了 fitsSystemWindow=true,在 onMeasure 分发给子 View
CollapsingToolbarLayout
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
public CollapsingToolbarLayout() {
ViewCompat.setOnApplyWindowInsetsListener(
this,
new androidx.core.view.OnApplyWindowInsetsListener() {
@Override
public WindowInsetsCompat onApplyWindowInsets(
View v, @NonNull WindowInsetsCompat insets) {
return onWindowInsetChanged(insets);
}
});
}
WindowInsetsCompat onWindowInsetChanged(@NonNull final WindowInsetsCompat insets) {
WindowInsetsCompat newInsets = null;
if (ViewCompat.getFitsSystemWindows(this)) {
// If we're set to fit system windows, keep the insets
newInsets = insets;
}
// If our insets have changed, keep them and invalidate the scroll ranges...
if (!ObjectsCompat.equals(lastInsets, newInsets)) {
lastInsets = newInsets;
requestLayout();
}
// Consume the insets. This is done so that child views with fitSystemWindows=true do not
// get the default padding functionality from View
return insets.consumeSystemWindowInsets();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
ensureToolbar();
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
final int mode = MeasureSpec.getMode(heightMeasureSpec);
final int topInset = lastInsets != null ? lastInsets.getSystemWindowInsetTop() : 0;
if (mode == MeasureSpec.UNSPECIFIED && topInset > 0) {
// If we have a top inset and we're set to wrap_content height we need to make sure
// we add the top inset to our height, therefore we re-measure
heightMeasureSpec =
MeasureSpec.makeMeasureSpec(getMeasuredHeight() + topInset, MeasureSpec.EXACTLY);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
// ...
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (lastInsets != null) {
// Shift down any views which are not set to fit system windows
final int insetTop = lastInsets.getSystemWindowInsetTop();
for (int i = 0, z = getChildCount(); i < z; i++) {
final View child = getChildAt(i);
if (!ViewCompat.getFitsSystemWindows(child)) {
if (child.getTop() < insetTop) {
// If the child isn't set to fit system windows but is drawing within
// the inset offset it down
ViewCompat.offsetTopAndBottom(child, insetTop);
}
}
}
}
// ...
}
- 只要 CollapsingToolbarLayout 的标记为 true,是一定消费 Insets 的。
- 需要设置
fitsSystemWindows
为 true,否则 lastInsets 记录的为 null,也就获取不到 top 值了 - AppBarLayout 不消费 WindowInsets;记录最后的 lastInsets,只处理 top,在 measure 会加上 top
- 在 layout 时上遍历所有子 View,如果子 View 没有设置 fitsSystemWindows 标记,只要 getTop() 的值小于 insetTop,就将其偏移到 insetTop(设置了标记的子 View 会在 StatusBar 下面(under)绘制,没有设置标记的子 View 会被挤下去(down))
- 在 onDraw 中,statusBarScrim 设置 setBounds
CollapsingToolbarLayout 还有一个有意思的逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
// Add an OnOffsetChangedListener if possible
final ViewParent parent = getParent();
if (parent instanceof AppBarLayout) {
AppBarLayout appBarLayout = (AppBarLayout) parent;
disableLiftOnScrollIfNeeded(appBarLayout);
// Copy over from the ABL whether we should fit system windows
ViewCompat.setFitsSystemWindows(this, ViewCompat.getFitsSystemWindows(appBarLayout));
if (onOffsetChangedListener == null) {
onOffsetChangedListener = new OffsetUpdateListener();
}
appBarLayout.addOnOffsetChangedListener(onOffsetChangedListener);
// We're attached, so lets request an inset dispatch
ViewCompat.requestApplyInsets(this);
}
}
如果 CollapsingToolbarLayout 的直接父 View 是 AppBarLayout,会同步 AppBarLayout 的 fitsSystemWindows 属性的值。
下面是 CollapsingToolbarLayout、AppBarLayout、TabLayout,ImageView 的 fitsSystemWindow 值的情况的表现:
ViewPager
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
void initViewPager() {
// ...
ViewCompat.setOnApplyWindowInsetsListener(this,
new androidx.core.view.OnApplyWindowInsetsListener() {
private final Rect mTempRect = new Rect();
@Override
public WindowInsetsCompat onApplyWindowInsets(final View v,
final WindowInsetsCompat originalInsets) {
// First let the ViewPager itself try and consume them...
final WindowInsetsCompat applied =
ViewCompat.onApplyWindowInsets(v, originalInsets);
if (applied.isConsumed()) {
// If the ViewPager consumed all insets, return now
return applied;
}
// Now we'll manually dispatch the insets to our children. Since ViewPager
// children are always full-height, we do not want to use the standard
// ViewGroup dispatchApplyWindowInsets since if child 0 consumes them,
// the rest of the children will not receive any insets. To workaround this
// we manually dispatch the applied insets, not allowing children to
// consume them from each other. We do however keep track of any insets
// which are consumed, returning the union of our children's consumption
final Rect res = mTempRect;
res.left = applied.getSystemWindowInsetLeft();
res.top = applied.getSystemWindowInsetTop();
res.right = applied.getSystemWindowInsetRight();
res.bottom = applied.getSystemWindowInsetBottom();
for (int i = 0, count = getChildCount(); i < count; i++) {
final WindowInsetsCompat childInsets = ViewCompat
.dispatchApplyWindowInsets(getChildAt(i), applied);
// Now keep track of any consumed by tracking each dimension's min
// value
res.left = Math.min(childInsets.getSystemWindowInsetLeft(),
res.left);
res.top = Math.min(childInsets.getSystemWindowInsetTop(),
res.top);
res.right = Math.min(childInsets.getSystemWindowInsetRight(),
res.right);
res.bottom = Math.min(childInsets.getSystemWindowInsetBottom(),
res.bottom);
}
// Now return a new WindowInsets, using the consumed window insets
return applied.replaceSystemWindowInsets(
res.left, res.top, res.right, res.bottom);
}
});
}
- ViewPager 自己先调用
onApplyWindowInsets(View)
来处理,如果 consumed 了,其子 view 就不会处理了 - ViewPager 自己未处理,再
dispatchApplyWindowInsets
分发给其子 view
本文由作者按照 CC BY 4.0 进行授权