前语
最近新接手保护一个组件,需要解决组件的一些遗留历史问题,其中有一个问题便是页面的 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
便是 DecorView
,DecorView
没有重写 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 沉溺式状态栏必知必会