关于WindowInsets理解

一  什么是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,表示内容区延伸到状态栏或底部导航栏

四  总结

  1. 并不是每次布局都会分发WindowInsets,只有当windowInsets发生变化的时候,ViewRootImpl才会主动进行分发
  2. 消费WindowInsets方法有两种,一个是主动的通过设置mOnApplyWindowInsetsListener来进行处理,另一个则是被动的通过onApplyWindowInsets方法进行处理(需要将fitSystemWIndows属性设置为true)
  3. viewGroup会尝试自己先进行处理,然后再进行分发
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值