前语

最近新接手保护一个组件,需要解决组件的一些遗留历史问题,其中有一个问题便是页面的 UI 被底部虚拟导航栏盖住了。一般遇到这种状况,咱们只需要设置一下 fitsSystemWindows=true 就可以了。可是合理我以为这个问题就这么简略的解决了时,我惊讶的发现,该页面的代码现已设置过了 fitsSystemWindows=true,仍是被遮挡了。顿时觉得非常隐晦,看来自己对 fitsSystemWindows 特点仍是没有了解透彻呀,趁此机会来了解一下 fitsSystemWindows

1. 什么时分需要运用 fitSystemWindows 这个特点呢?

回想一下,你是否从前遇到过底部导航栏隐瞒了布局 UI 的状况呈现呢?当咱们设置了和沉溺式相关的特点,像 SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN (视图延伸至状态栏区域,状态栏上浮于视图之上)或 SYSTEM_UI_FLAG_HIDE_NAVIGATION (视图延伸至导航栏区域,导航栏上浮于视图之上)。这个时分就可能会呈现布局 UI 放到了导航栏或状态栏下方,给用户一种界面被隐瞒的感觉。

为了解决上面的问题,Android 便提供了 fitSystemWindows 这个特点,简略来说该特点可以帮助 UI 保持正常的方位,避免被窗口中其他元素隐瞒,也便是主动帮 UI 处理了边距。

那么,为什么会呈现现已设置了 fitSystemWindows,可是 UI 仍是被隐瞒的状况呢?

2. 先了解一下 WindowInsets 的概念

为了让更好的了解,先来看看 WindowInsets 的概念。WindowInsets 是 Window Content 的刺进物。例如状态栏、导航栏、键盘等等,当它们显示时,会被刺进到 Window 窗口的显示区域。一个 inset 目标中包含 left、top、right、bottom 的 4 个 int 偏移值。和 View 的事件分发机制一样,WindowInset 也是从父 View 开端分发的,也被称为深度优先。

这儿简略捋一下,Activity 创立的时分,DecorView 也会被创立,可是此时 DecorView 还没有被加载到 Window 中,当处理 Activity 的 Resume 时,会把 DecorView 加载到 WindowManager 中。最终经过 ViewRootImpl 的 setView() 办法来履行加载 View 的逻辑。在这个办法中,会履行到我们熟知的 requestLayout 办法,兜兜转转最终来到 performTraversals,这也是一个我们了解的办法,是 View 的三大工作流程的进口办法。不理解的可以看这篇文章 View 系列 —— 浅谈 View 的三大工作流程。

当然了,这些内容对于了解本文的重点仅仅起到一个辅佐效果,你只需要知道在处理 View 的过程中,会履行到一个办法 dispatchApplyInsets,看名字就能知道这是一个分发 WindowInsets 的办法。

3. WindowInsets 的分发

public void dispatchApplyInsets(View host) {
    ...
    host.dispatchApplyWindowInsets(insets);
    ...
}

我只保留了该办法中重要的一句代码,这个 host 便是 DecorViewDecorView 没有重写 dispatchApplyWindowInsets办法,所以直接履行基类 ViewGroup 的 dispatchApplyWindowInsets

@Override
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {
    insets = super.dispatchApplyWindowInsets(insets);
    // 是否现已耗费过了该inset
    if (insets.isConsumed()) {
        return insets;
    }
    // 这个if判断不重要,只需知道没有耗费过inset就继续往下分发就好
    if (View.sBrokenInsetsDispatch) {
        return brokenDispatchApplyWindowInsets(insets);
    } else {
        return newDispatchApplyWindowInsets(insets);
    }
}
private WindowInsets brokenDispatchApplyWindowInsets(WindowInsets insets) {
    final int count = getChildCount();
    for (int i = 0; i < count; i++) {
        // 分发给子View,看子View是否要耗费inset
        insets = getChildAt(i).dispatchApplyWindowInsets(insets);
        // 假如子View耗费了,跳出循环
        if (insets.isConsumed()) {
            break;
        }
    }
    return insets;
}

ViewGroup 的 dispatchApplyWindowInsets 办法首先会判断当前的 inset 有没有被耗费,假如没有被耗费,就遍历子 View,履行 View 的 dispatchApplyWindowInsets

public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {
    try {
        mPrivateFlags3 |= PFLAG3_APPLYING_INSETS;
        // 假如设置了OnApplyWindowInsetsListener,直接回调给listener的onApplyWindowInsets办法
        if (mListenerInfo != null && mListenerInfo.mOnApplyWindowInsetsListener != null) {
            return mListenerInfo.mOnApplyWindowInsetsListener.onApplyWindowInsets(this, insets);
        } else {
            // 没有设置过listener的话,履行View的onApplyWindowInsets办法
            return onApplyWindowInsets(insets);
        }
    } finally {
        mPrivateFlags3 &= ~PFLAG3_APPLYING_INSETS;
    }
}

View 的处理也很简略,假如咱们自己设置了 OnApplyWindowInsetsListener,就直接回调给咱们自己来处理 inset,假如没有设置 listener,就走 View 的默许办法 onApplyWindowInsets 办法。那默许肯定是走 onApplyWindowInsets 了,接着看:

public WindowInsets onApplyWindowInsets(WindowInsets insets) {
    if ((mPrivateFlags4 & PFLAG4_FRAMEWORK_OPTIONAL_FITS_SYSTEM_WINDOWS) != 0
            && (mViewFlags & FITS_SYSTEM_WINDOWS) != 0) {
        return onApplyFrameworkOptionalFitSystemWindows(insets);
    }
    if ((mPrivateFlags3 & PFLAG3_FITTING_SYSTEM_WINDOWS) == 0) {
        if (fitSystemWindows(insets.getSystemWindowInsetsAsRect())) {
            return insets.consumeSystemWindowInsets();
        }
    } else {
        // 默许走这个逻辑,直接看这个办法
        if (fitSystemWindowsInt(insets.getSystemWindowInsetsAsRect())) {
            return insets.consumeSystemWindowInsets();
        }
    }
    return insets;
}
private boolean fitSystemWindowsInt(Rect insets) {
    // 假如设置了 fitSystemWindows == true,就会设置 FITS_SYSTEM_WINDOWS 符号位
    if ((mViewFlags & FITS_SYSTEM_WINDOWS) == FITS_SYSTEM_WINDOWS) {
        Rect localInsets = sThreadLocal.get();
        boolean res = computeFitSystemWindows(insets, localInsets);
        // 设置了 fitSystemWindows 特点后,从头处理 padding
        applyInsets(localInsets);
        // View 耗费了 inset 就返回 true
        return res;
    }
    return false;
}

简略来说,进入 onApplyWindowInsets 后,默许会履行 fitSystemWindowsInt 办法,这个办法会判断有没有设置 FITS_SYSTEM_WINDOWS 这个符号位,设置了才是履行 fitSystemWindowsInt 内部逻辑的条件。看来这个符号位也要理解是个啥东西,这儿咱们先接着往下看。

private void applyInsets(Rect insets) {
    mUserPaddingStart = UNDEFINED_PADDING;
    mUserPaddingEnd = UNDEFINED_PADDING;
    mUserPaddingLeftInitial = insets.left;
    mUserPaddingRightInitial = insets.right;
    // 从头设置padding
    internalSetPadding(insets.left, insets.top, insets.right, insets.bottom);
}

假如设置了 FITS_SYSTEM_WINDOWS 这个符号位,会经过 applyInsets() 办法处理 padding,并设置 View 现已耗费了该 inset。

4. FITS_SYSTEM_WINDOWS 这个符号位是怎样设置的呢?

上一个问题中咱们说了假如想让体系帮咱们从头设置 padding,条件是设置了 FITS_SYSTEM_WINDOWS 这个符号位,这样当分发 inset 的逻辑走到 fitSystemWindowsInt 办法中的时分,才会履行设置 padding 的操作。那么 FITS_SYSTEM_WINDOWS 这个符号位是怎样设置的呢?

揭晓答案,当设置了 View 的 fitsSystemWindows 特点为 true 的时分,就相当于设置了 FITS_SYSTEM_WINDOWS 这个符号位。

// View.java
public void setFitsSystemWindows(boolean fitSystemWindows) {
    // fitSystemWindows 特点的设置,首要便是设置了一个符号位
    setFlags(fitSystemWindows ? FITS_SYSTEM_WINDOWS : 0, FITS_SYSTEM_WINDOWS);
}

5. 总结

梳理了一遍 fitsSystemWindows 之后,咱们来总结一下。

首先,fitsSystemWindows 特点通常和沉溺式相关,咱们的 UI 可能会被状态栏或许导航栏隐瞒住,这时分就需要咱们给布局加一个 padding,比如说这个 padding 的巨细是状态栏或导航栏的高度,这样就能避免布局被盖住。Android 现已提供给咱们这个功用了,便是 fitsSystemWindows。当设置该特点为 true 后,符号位 FITS_SYSTEM_WINDOWS 被设置,代表该 View 可以耗费 inset。

WindowInsets 也是像事件分发机制一样,在 performTraversals 的时分进行分发,经过 DecorView、ViewGroup 最终抵达 View 的 dispatchApplyWindowInsets

需要注意的是,假如咱们在自定义 View 中重写了 onApplyWindowInsets() 办法或许是设置了 setOnApplyWindowInsetsListener() 来监听 WindowInsets 的变化,那么在 View 的 dispatchApplyWindowInsets 办法中,就不会走体系默许的 onApplyWindowInsets() 办法了,而是由事务自己来处理,自己经过 inset 设置 padding。

setOnApplyWindowInsetsListener() 的优先级更高,当存在 OnApplyWindowInsetsListener 时不会履行 onApplyWindowInsets。而咱们设置的 fitSystemWindows == true 其实便是经过体系默许的 onApplyWindowInsets 办法来完成的。即设置了 fitSystemWindows == true 后,onApplyWindowInsets 才会去主动调整 padding。

6. 最终回到最开端的问题

了解完了 fitsSystemWindows 后,回到咱们前语里提到的问题,为啥现已设置过了 fitsSystemWindows=true,UI 仍是被遮挡了。看了代码之后发现,本来有个当地悄悄运用了setOnApplyWindowInsetsListener,所以设置了 fitsSystemWindows 也没啥用,由于底子不会走体系的 onApplyWindowInsets 办法帮我主动调整 padding 了。所以我就改成统一运用 setOnApplyWindowInsetsListener 来处理 padding 了。之所以不必 fitsSystemWindows 特点,是由于这个特点用不好也会有坑的,究竟它是深度优先,第一个设置 fitSystemWindows == true 的 View 消费完 inset,后面的 View 就不会再消费了,难保不会在某些状况下呈现适配问题。所以果断运用 setOnApplyWindowInsetsListener

以上便是我对 fitsSystemWindows 的一些个人解析啦,有任何问题或许不足之处欢迎我们彼此沟通~

参阅

medium.com/androiddeve…

nich.work/2017/window…

Android Detail:Window 篇——WindowInsets 与 fitsSystemWindow

Android 沉溺式状态栏必知必会