fitSystemWindow

376 阅读1分钟

fitsSystemWindows 属性

该属性只影响当前 View 对状态栏的适配

该属性为 true 时,View 会自己适配状态栏,即给自己添加一个 paddTop,它的子 View 就不会被状态栏所遮挡,然后适配状态栏事件到此为止不会传递给它的子 View。

该属性为 false 时,View 会将事件传递给所有子 View,由子 View 自己处理。注意:子 View 无论消费不消费该事件都不会阻止它的兄弟 View 得到事件,它们能否得到事件完全取决于父 View。

原理

view 的三大流程起点都在 ViewRootImpl#performTraversals() 中,在执行三大流程前,会先调用 View#dispatchApplyWindowInsets()。所以首先得到事件的一定是 DecorView,DecorView 没有重写该方法,所以执行到 ViewGroup 中

// ViewGroup.java 

@Override
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {
    // 该方法就是看自己是不是消费事件
    insets = super.dispatchApplyWindowInsets(insets);
    if (insets.isConsumed()) { // 如果消耗整个事件结束
        return insets;
    }
    if (View.sBrokenInsetsDispatch) { 
        // android 32 时 if 判断不成立
        return brokenDispatchApplyWindowInsets(insets);
    } else {
        return newDispatchApplyWindowInsets(insets);
    }
}

private WindowInsets newDispatchApplyWindowInsets(WindowInsets insets) {
    final int count = getChildCount();
    for (int i = 0; i < count; i++) {
        // 遍历所有子 View。可以看到子 view 无论是否消耗该事件都不影响它的兄弟
        getChildAt(i).dispatchApplyWindowInsets(insets);
    }
    return insets;
}

View 的处理流程也比较简单:添加 padding 或 margin 或根据位置在 layout 时将偏移子 View 位置

// View.java
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {
    try {
        mPrivateFlags3 |= PFLAG3_APPLYING_INSETS;
        if (mListenerInfo != null && mListenerInfo.mOnApplyWindowInsetsListener != null) {
            // 如果调用 setOnApplyWindowInsetsListener() 设置过监听,就走自己的回调
            // 否则执行默认逻辑
            return mListenerInfo.mOnApplyWindowInsetsListener.onApplyWindowInsets(this, insets);
        } else {
            return onApplyWindowInsets(insets);
        }
    } finally {
        mPrivateFlags3 &= ~PFLAG3_APPLYING_INSETS;
    }
}

onApplyWindowInsets() 内部虽然分了几个分支,但最终会到 applyInsets(),该方法就会设置 paddingTop,实现对状态栏的适配。

涉及方法

  1. dispatchApplyWindowInsets:整个事件的适配入口
  2. setOnApplyWindowInsetsListener:设置事件监听,它的优先级高于 onApplyWindowInsets
  3. onApplyWindowInsets:用于当前 View 对状态栏的适配

viewpager+fragment

  1. vp 自定义 windowInsets 的分发。旧版本时, windowInsets 只分发给第一个设置了 fitsSystemWindows = true 的子 view,这里为了处理多个 frg 就需要自定义分发逻辑。因为自定义了分发逻辑,所以 vp 的 fitsSystemWindows 配置成 true/false 都没有影响。

    // demo 版,描述大致意思
    vp.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
        @Override
        public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
            int childCount = vp.getChildCount();
            for (int i = 0; i < childCount; i++) {
                // 遍历所有的 view,依次分发
                vp.getChildAt(i).dispatchApplyWindowInsets(insets);
            }
            return insets.consumeSystemWindowInsets();
        }
    });
    
  2. 对于 frg:背景需要延伸到状态栏下,内容又不能延伸过去。也就是说 frg#view 应包括状态栏部分,布局子 view 时需额外添加 padding,避免状态栏覆盖子 view。这正是 CoordinatorLayout 的默认实现逻辑。所以 frg 使用 CoordinatorLayout 作为根 view 且 fitsSystemWindows = true。可以自定义实现自己的业务。

  3. 应用 theme 中可配置两个属性。第一个保证能延伸至状态栏下,第二个保证能延伸至底部虚拟按键下

    <item name="android:windowTranslucentStatus">true</item>
    <item name="android:windowTranslucentNavigation">true</item>
    

最终整体效果如下,左右两边是两个不同的 frg

image.png