一 什么是WindowInsets?
WindowInsets源码解释为Window Content的一系列插值集合,可以理解为可以将其理解为不同的窗口装饰区域类型,比如一个Activity相对于手机屏幕需要空出的地方以腾给StatusBar、Ime、NavigationBar等系统窗口,具体表现为该区域需要的上下左右的宽高。
WindowInsets包括三类:SystemWindowInsets、StableInsets、WIndowDecorInsets
- SystemWindowInsets:表示全窗口下,被StatusBar, NavigationBar, IME 或者其它系统窗口部分或者全部覆盖的区域。
- StableInsets: 表示全窗口下,被系统UI部分或者全部覆盖的区域。
- WIndowDecorInsets:表示内容窗口下,被Android FrameWork提供的窗体,诸如ActionBar, TitleBar, ToolBar,部分或全部覆盖区域。
1.1 如何理解WindowInsets
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
getWindow().getDecorView().setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
@Override
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
Log.w("chric", "insets:" + insets);
Insets statusBarInset = insets.getInsets(WindowInsets.Type.statusBars());
Log.w("chric", "statusBarInset:" + statusBarInset);
Insets navigationInset = insets.getInsets(WindowInsets.Type.navigationBars());
Log.w("chric", "navigationInset:" + navigationInset);
Insets displayCutoutInset = insets.getInsets(WindowInsets.Type.displayCutout());
Log.w("chric", "displayCutoutInset:" + displayCutoutInset);
Insets imeInset = insets.getInsets(WindowInsets.Type.ime());
Log.w("chric", "imeInset:" + imeInset);
Insets systemBarsInset = insets.getInsets(WindowInsets.Type.systemBars());
Log.w("chric", "systemBarsInset:" + systemBarsInset);
}
return insets;
}
});
}
}
我们看下日志:
insets:WindowInsets{
statusBars=Insets{left=0, top=84, right=0, bottom=0} max=Insets{left=0, top=84, right=0, bottom=0} vis=true
navigationBars=Insets{left=0, top=0, right=0, bottom=168} max=Insets{left=0, top=0, right=0, bottom=168} vis=true
captionBar=Insets{left=0, top=0, right=0, bottom=0} max=null vis=false
ime=Insets{left=0, top=0, right=0, bottom=1155} max=null vis=true
systemGestures=Insets{left=0, top=84, right=0, bottom=168} max=Insets{left=0, top=84, right=0, bottom=168} vis=true
mandatorySystemGestures=Insets{left=0, top=84, right=0, bottom=168} max=Insets{left=0, top=84, right=0, bottom=168} vis=true
tappableElement=Insets{left=0, top=84, right=0, bottom=168} max=Insets{left=0, top=84, right=0, bottom=168} vis=true
displayCutout=Insets{left=0, top=0, right=0, bottom=0} max=null vis=false
windowDecor=null max=null vis=false
roundedCorners=RoundedCorners{[RoundedCorner{position=TopLeft, radius=0, center=Point(0, 0)}, RoundedCorner{position=TopRight, radius=0, center=Point(0, 0)}, RoundedCorner{position=BottomRight, radius=0, center=Point(0, 0)}, RoundedCorner{position=BottomLeft, radius=0, center=Point(0, 0)}]}
privacyIndicatorBounds=PrivacyIndicatorBounds {static bounds=Rect(1104, 0 - 1391, 84) rotation=0}
}
statusBarInset:Insets{left=0, top=84, right=0, bottom=0}
navigationInset:Insets{left=0, top=0, right=0, bottom=168}
displayCutoutInset:Insets{left=0, top=0, right=0, bottom=0}
imeInset:Insets{left=0, top=0, right=0, bottom=1155}
systemBarsInset:Insets{left=0, top=84, right=0, bottom=168}
这里的Rect的概念已经区别于View的Rect了,它的四个点已经不再表示围成矩形的坐标,而表示的是insets需要的左右的宽度,顶部和底部需要的高度。
二 WindowInsets的分发流程
WindowInset是window大小发生变化的时候传递给ViewRootImpl的,ViewrootImpl会储存该值,然后进入到performTraversals方法内
if (dispatchApplyInsets || mLastSystemUiVisibility !=
mAttachInfo.mSystemUiVisibility || mApplyInsetsRequested) {
mLastSystemUiVisibility = mAttachInfo.mSystemUiVisibility;
dispatchApplyInsets(host);
// We applied insets so force contentInsetsChanged to ensure the
// hierarchy is measured below.
dispatchApplyInsets = true;
}
mAttachInfo实际上是View类中的静态内部类AttachInfo类的对象
public void dispatchApplyInsets(View host) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "dispatchApplyInsets");
mApplyInsetsRequested = false;
WindowInsets insets = getWindowInsets(true /* forceConstruct */);
if (!shouldDispatchCutout()) {
// Window is either not laid out in cutout or the status bar inset takes care of
// clearing the cutout, so we don't need to dispatch the cutout to the hierarchy.
insets = insets.consumeDisplayCutout();
}
host.dispatchApplyWindowInsets(insets);
mAttachInfo.delayNotifyContentCaptureInsetsEvent(insets.getInsets(Type.all()));
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
ViewGroup.dispatchApplyWindowInsets
@Override
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {
insets = super.dispatchApplyWindowInsets(insets);
if (insets.isConsumed()) {
return insets;
}
if (View.sBrokenInsetsDispatch) {
return brokenDispatchApplyWindowInsets(insets);
} else {
return newDispatchApplyWindowInsets(insets);
}
}
先进行分发ViewGroup brokenDispatchApplyWindowInset
private WindowInsets brokenDispatchApplyWindowInsets(WindowInsets insets) {
final int count = getChildCount();
for (int i = 0; i < count; i++) {
insets = getChildAt(i).dispatchApplyWindowInsets(insets);
if (insets.isConsumed()) {
break;
}
}
return insets;
}
依次遍历
onApplyFrameworkOptionalFitSystemWindows:11391, View (android.view)
onApplyWindowInsets:11372, View (android.view)
dispatchApplyWindowInsets:11439, View (android.view)
dispatchApplyWindowInsets:7391, ViewGroup (android.view)
brokenDispatchApplyWindowInsets:7405, ViewGroup (android.view)
dispatchApplyWindowInsets:7396, ViewGroup (android.view)
brokenDispatchApplyWindowInsets:7405, ViewGroup (android.view)
dispatchApplyWindowInsets:7396, ViewGroup (android.view)
dispatchApplyInsets:2406, ViewRootImpl (android.view)
performTraversals:2814, ViewRootImpl (android.view)
View.dispatchApplyWindowInsets
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {
try {
mPrivateFlags3 |= PFLAG3_APPLYING_INSETS;
if (mListenerInfo != null && mListenerInfo.mOnApplyWindowInsetsListener != null) {
return mListenerInfo.mOnApplyWindowInsetsListener.onApplyWindowInsets(this, insets);
} else {
return onApplyWindowInsets(insets);
}
} finally {
mPrivateFlags3 &= ~PFLAG3_APPLYING_INSETS;
}
}
在此方法中首先将mPrivateFlags3 的PFLAG3_APPLYING_INSETS标志位置为1,然后如果开发者设置了listener的话就调用listener,否则调用onApplyWindowInsets方法。
如果无listener的话,就交给View本身的onApplyWindowInsets。一般的view都没有listener,CoordinatorLayout和AppBarLayout 、CollapsingToolbarLayout是存在listener的,而且CollapsingToolbarLayout是必然会消费掉WindowInsets的,CoordinatorLayout和AppBarLayout不消费WindowInsets。普通的view肯定调用的是本身的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) {
// We weren't called from within a direct call to fitSystemWindows,
// call into it as a fallback in case we're in a class that overrides it
// and has logic to perform.
if (fitSystemWindows(insets.getSystemWindowInsetsAsRect())) {
return insets.consumeSystemWindowInsets();
}
} else {
// We were called from within a direct call to fitSystemWindows.
if (fitSystemWindowsInt(insets.getSystemWindowInsetsAsRect())) {
return insets.consumeSystemWindowInsets();
}
}
return insets;
}
PFLAG3_FITTING_SYSTEM_WINDOWS标记表示正在处理WindowInsets,在第一次调用fitSystemWindows方法后,mPrivateFlags3 的 PFLAG3_FITTING_SYSTEM_WINDOWS标志为被置位1了,所以进入fitSystemWindowsInt方法。
PFLAG3_FITTING_SYSTEM_WINDOWS==0表示没有正在处理的WindowInsets
如果当前没有正在处理WindowInsets就呼叫fitSystemWindows方法进行处理
否则(PFLAG3_FITTING_SYSTEM_WINDOWS!=0)呼叫fitSystemWindowsInt方法直接进行相应的逻辑处理(internalSetPadding)
如果这两个方法结果返回true,表示消费了WindowInsets
简单的认为onApplyWindowInsets就是调用fitSystemWindowsInt,而fitSystemWindowsInt就是调computeFitSystemWindows和internalSetPadding。computeFitSystemWindows是计算padding,而internalSetPadding就正式设置padding,padding设置好了,子view就会小一些,被约束在padding里面。注意一点fitSystemWindowsInt只有FITS_SYSTEM_WINDOWS这个flag为true才会进去,flag不对直接返回false。
@Deprecated
protected boolean fitSystemWindows(Rect insets) {
if ((mPrivateFlags3 & PFLAG3_APPLYING_INSETS) == 0) {
if (insets == null) {
// Null insets by definition have already been consumed.
// This call cannot apply insets since there are none to apply,
// so return false.
return false;
}
// If we're not in the process of dispatching the newer apply insets call,
// that means we're not in the compatibility path. Dispatch into the newer
// apply insets path and take things from there.
try {
mPrivateFlags3 |= PFLAG3_FITTING_SYSTEM_WINDOWS;
return dispatchApplyWindowInsets(new WindowInsets(insets)).isConsumed();
} finally {
mPrivateFlags3 &= ~PFLAG3_FITTING_SYSTEM_WINDOWS;
}
} else {
// We're being called from the newer apply insets path.
// Perform the standard fallback behavior.
return fitSystemWindowsInt(insets);
}
}
这个方法是之前代码的逻辑,调用这个方法是为了保证基于之前版本开发的逻辑能够正常执行,首先判断PFLAG3_APPLYING_INSETS,这个标记表示当时正在执行WindowInsets的分发,
如果没有在分发(if ((mPrivateFlags3 & PFLAG3_APPLYING_INSETS) == 0)),那么针对windowInsets继续进行分发,并设定PFLAG3_FITTING_SYSTEM_WINDOWS,在View.onApplyWindowInsets方法内如果判断存在PFLAG3_FITTING_SYSTEM_WINDOWS标记,那么直接执行fitSystemWIndowInt方法
如果正在分发( flag& PFLAG3_APPLYING_INSETS !=0),那么直接执行else下一步的逻辑,进入fitSystemWindowsInt方法
//fitSystemWindows为true才会进行消费WindowInsets 否则直接返回false
private boolean fitSystemWindowsInt(Rect insets) {
if ((mViewFlags & FITS_SYSTEM_WINDOWS) == FITS_SYSTEM_WINDOWS) {
Rect localInsets = sThreadLocal.get();
//计算是否消费windowInsets
boolean res = computeFitSystemWindows(insets, localInsets);
//执行真正的Windowinsets消费逻辑 重新调整View的padding值
applyInsets(localInsets);
return res;
}
return false;
}
//计算是否消费windowInsets
protected boolean computeFitSystemWindows(Rect inoutInsets, Rect outLocalInsets) {
WindowInsets innerInsets = computeSystemWindowInsets(new WindowInsets(inoutInsets),
outLocalInsets);
inoutInsets.set(innerInsets.getSystemWindowInsetsAsRect());
return innerInsets.isSystemWindowInsetsConsumed();
}
public WindowInsets computeSystemWindowInsets(WindowInsets in, Rect outLocalInsets) {
boolean isOptionalFitSystemWindows = (mViewFlags & OPTIONAL_FITS_SYSTEM_WINDOWS) != 0
|| (mPrivateFlags4 & PFLAG4_FRAMEWORK_OPTIONAL_FITS_SYSTEM_WINDOWS) != 0;
if (isOptionalFitSystemWindows && mAttachInfo != null) {
OnContentApplyWindowInsetsListener listener =
mAttachInfo.mContentOnApplyWindowInsetsListener;
if (listener == null) {
// The application wants to take care of fitting system window for
// the content.
outLocalInsets.setEmpty();
return in;
}
Pair<Insets, WindowInsets> result = listener.onContentApplyWindowInsets(this, in);
outLocalInsets.set(result.first.toRect());
return result.second;
} else {
outLocalInsets.set(in.getSystemWindowInsetsAsRect());
return in.consumeSystemWindowInsets().inset(outLocalInsets);
}
}
mAttachInfo为view绑定window的标志,View.AttachInfo 里面的信息,就是View和Window之间的信息。每一个被添加到窗口上的View我们都会看到有一个AttachInfo,比如我们看DecorView和Window的绑定,AttachInfo 会通过View的diapatchAttachedTowWindow分发给View。如果是一个ViewGroup 那么这个这个AttachInfo也会分发给所有子View,以引用的方式。
isOptionalFitSystemWindows = (mViewFlags & OPTIONAL_FITS_SYSTEM_WINDOWS)!= 0
mAttachInfo == null
((mAttachInfo.mSystemUiVisibility & SYSTEM_UI_LAYOUT_FLAGS) == 0 && !mAttachInfo.mOverscanRequested))
条件1,// OPTIONAL_FITS_SYSTEM_WINDOWS 代表着是系统View,(mViewFlags & OPTIONAL_FITS_SYSTEM_WINDOWS) == 0 代表是用户的UI,(mViewFlags & OPTIONAL_FITS_SYSTEM_WINDOWS) != 0代表着系统View。
条件2,这个值始终为false。
条件3,A:!mAttachInfo.mOverscanRequested始终为true。 B:还记得前面说的 SYSTEM_UI_LAYOUT_FLAGS标记吗?mAttachInfo.mSystemUiVisibility & SYSTEM_UI_LAYOUT_FLAGS这个标记可以调用View的setSystemUiVisibility方法来设置,默认为0.所以如果没有调用setSystemUiVisibility来更改Flag的话,DecoreView的直接子View会消费掉事件,事件就不会往下面传递了。
public void makeOptionalFitsSystemWindows() {
setFlags(OPTIONAL_FITS_SYSTEM_WINDOWS, OPTIONAL_FITS_SYSTEM_WINDOWS);
}
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 {
// 设置Window
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
<!--关键点1-->
mDecor.makeOptionalFitsSystemWindows();
...
}
OPTIONAL_FITS_SYSTEM_WINDOWS是通过 makeOptionalFitsSystemWindows设置的,入口只在PhoneWindow中,通过mDecor.makeOptionalFitsSystemWindows()设置,在installDecor的时候,里面还未涉及用户view,所以通过mDecor.makeOptionalFitsSystemWindows标记的都是系统自己的View布局
private void applyInsets(Rect insets) {
mUserPaddingStart = UNDEFINED_PADDING;
mUserPaddingEnd = UNDEFINED_PADDING;
mUserPaddingLeftInitial = insets.left;
mUserPaddingRightInitial = insets.right;
internalSetPadding(insets.left, insets.top, insets.right, insets.bottom);
}
protected void internalSetPadding(int left, int top, int right, int bottom) {
mUserPaddingLeft = left;
mUserPaddingRight = right;
mUserPaddingBottom = bottom;
final int viewFlags = mViewFlags;
boolean changed = false;
// Common case is there are no scroll bars.
if ((viewFlags & (SCROLLBARS_VERTICAL|SCROLLBARS_HORIZONTAL)) != 0) {
if ((viewFlags & SCROLLBARS_VERTICAL) != 0) {
final int offset = (viewFlags & SCROLLBARS_INSET_MASK) == 0
? 0 : getVerticalScrollbarWidth();
switch (mVerticalScrollbarPosition) {
case SCROLLBAR_POSITION_DEFAULT:
if (isLayoutRtl()) {
left += offset;
} else {
right += offset;
}
break;
case SCROLLBAR_POSITION_RIGHT:
right += offset;
break;
case SCROLLBAR_POSITION_LEFT:
left += offset;
break;
}
}
if ((viewFlags & SCROLLBARS_HORIZONTAL) != 0) {
bottom += (viewFlags & SCROLLBARS_INSET_MASK) == 0
? 0 : getHorizontalScrollbarHeight();
}
}
if (mPaddingLeft != left) {
changed = true;
mPaddingLeft = left;
}
if (mPaddingTop != top) {
changed = true;
mPaddingTop = top;
}
if (mPaddingRight != right) {
changed = true;
mPaddingRight = right;
}
if (mPaddingBottom != bottom) {
changed = true;
mPaddingBottom = bottom;
}
if (changed) {
requestLayout();
invalidateOutline();
}
}
三 android:fitsSystemWindows属性
属性说明
fitsSystemWindows属性可以让view根据系统窗口来调整自己的布局;简单点说就是我们在设置应用布局时是否考虑系统窗口布局,这里系统窗口包括系统状态栏、导航栏、输入法等,包括一些手机系统带有的底部虚拟按键。
android:fitsSystemWindows=”true” (触发View的padding属性来给系统窗口留出空间)
这个属性可以给任何view设置,只要设置了这个属性此view的其他所有padding属性失效,同时该属性的生效条件是只有在设置了透明状态栏(StatusBar)或者导航栏(NavigationBar)此属性才会生效。
注意: fitsSystemWindows只作用在Android4.4及以上的系统,因为4.4以下的系统StatusBar没有透明状态。
应用场景
在不同Android版本下,App状态栏和不同版本中系统本身的状态栏的适配;
兼容带有底部虚拟按键的手机系统。
属性使用
1、默认效果
先贴一张未对系统状态栏和导航栏做透明设置时测试布局效果图:
2、系统窗口透明后效果
当设置了透明状态栏(StatusBar)和透明导航栏(NavigationBar)时效果图:
透明状态栏代码设置:
//布局设置
<item name="android:windowTranslucentStatus">true</item>
//或者代码设置
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
}
透明导航栏代码设置:
//布局设置
<item name="android:windowTranslucentNavigation">true</item>
//或者代码设置
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
}
3、设置fitsSystemWindows属性后效果
现在就到了我们关键的fitsSystemWindows属性登场了,只要在根布局中加上android:fitsSystemWindows=”true”效果如下图:
设置了android:fitsSystemWindows=”true”属性后针对透明的状态栏会自动添加一个值等于状态栏高度的paddingTop;针对透明的系统导航栏会自动添加一个值等于导航栏高度的paddingBottom
总结:
- 1. fitsSystemWindows 生效前提:当前页面没有标题栏,并且状态栏或者底部导航栏透明
- 2. fitsSystemWindows = true,表示内容区不延伸到状态栏或底部导航栏
- 3. fitsSystemWindows = false,表示内容区延伸到状态栏或底部导航栏
四 总结
- 并不是每次布局都会分发WindowInsets,只有当windowInsets发生变化的时候,ViewRootImpl才会主动进行分发
- 消费WindowInsets方法有两种,一个是主动的通过设置mOnApplyWindowInsetsListener来进行处理,另一个则是被动的通过onApplyWindowInsets方法进行处理(需要将fitSystemWIndows属性设置为true)
- viewGroup会尝试自己先进行处理,然后再进行分发