WindowInsets 在View下的的分发(二)

来源:互联网 发布:小米智能家庭软件 编辑:程序博客网 时间:2024/05/29 16:26

绪论

  在上一篇中,大概说明了下WindowInsets的概念和分发逻辑,然而在部分情况下,我们会发现即便设置了fitSSystemWindows = true 也并没有生效;而且从上文已知的情况可以看出,即便消费WindowInsets似乎也只是在消费SystemWindowInsets,其它的Insets似乎并没有被消耗。这一篇将解决这两个问题。

WindowInsets 在View下的的分发(二)

mWindowDecorInsets和mStableInsets的消耗

fitSSystemWindows = true 生效的首要条件

这个问题的答案,我们可以在ViewRootImpl和Decoreview这两个类中找到答案
在ViewRootImpl的源码中,我们可以发现这样一段代码

private void performTraversals() {    ...    ...    dispatchApplyInsets(host);    ...    ...}void dispatchApplyInsets(View host) {    host.dispatchApplyWindowInsets(getWindowInsets(true /* forceConstruct */));}WindowInsets getWindowInsets(boolean forceConstruct) {    if (mLastWindowInsets == null || forceConstruct) {        mDispatchContentInsets.set(mAttachInfo.mContentInsets);        mDispatchStableInsets.set(mAttachInfo.mStableInsets);        Rect contentInsets = mDispatchContentInsets;        Rect stableInsets = mDispatchStableInsets;        // For dispatch we preserve old logic, but for direct requests from Views we allow to        // immediately use pending insets.        if (!forceConstruct                && (!mPendingContentInsets.equals(contentInsets) ||                    !mPendingStableInsets.equals(stableInsets))) {            contentInsets = mPendingContentInsets;            stableInsets = mPendingStableInsets;        }        Rect outsets = mAttachInfo.mOutsets;        if (outsets.left > 0 || outsets.top > 0 || outsets.right > 0 || outsets.bottom > 0) {            contentInsets = new Rect(contentInsets.left + outsets.left,                    contentInsets.top + outsets.top, contentInsets.right + outsets.right,                    contentInsets.bottom + outsets.bottom);        }        mLastWindowInsets = new WindowInsets(contentInsets,                null /* windowDecorInsets */, stableInsets,                mContext.getResources().getConfiguration().isScreenRound(),                mAttachInfo.mAlwaysConsumeNavBar);    }    return mLastWindowInsets;}

从上述代码中,可以发现被dispatchapply的WindowInsets来源于getWindowInsets(…)。而在这个函数中,我们可以发现mWindowDecorInsets值为null,表明其从一开始就是消耗状态。
再看看DecoreView的代码

@Overridepublic WindowInsets onApplyWindowInsets(WindowInsets insets) {    ...    ...    insets = updateColorViews(insets, true /* animate */);    insets = updateStatusGuard(insets);    insets = updateNavigationGuard(insets);    if (getForeground() != null) {        drawableChanged();    }    return insets;}WindowInsets updateColorViews(WindowInsets insets, boolean animate) {    WindowManager.LayoutParams attrs = mWindow.getAttributes();    int sysUiVisibility = attrs.systemUiVisibility | getWindowSystemUiVisibility();    ...    ...    boolean consumingNavBar =        (attrs.flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0                && (sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) == 0                && (sysUiVisibility & SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0        || mLastShouldAlwaysConsumeNavBar;    // If we did not request fullscreen layout, but we still got it because of the    // mForceWindowDrawsStatusBarBackground flag, also consume top inset.    boolean consumingStatusBar = (sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) == 0            && (sysUiVisibility & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) == 0            && (attrs.flags & FLAG_LAYOUT_IN_SCREEN) == 0            && (attrs.flags & FLAG_LAYOUT_INSET_DECOR) == 0            && mForceWindowDrawsStatusBarBackground            && mLastTopInset != 0;    int consumedTop = consumingStatusBar ? mLastTopInset : 0;    int consumedRight = consumingNavBar ? mLastRightInset : 0;    int consumedBottom = consumingNavBar ? mLastBottomInset : 0;    int consumedLeft = consumingNavBar ? mLastLeftInset : 0;    if (mContentRoot != null            && mContentRoot.getLayoutParams() instanceof MarginLayoutParams){        MarginLayoutParams lp = (MarginLayoutParams) mContentRoot.getLayoutParams();        if (lp.topMargin != consumedTop || lp.rightMargin != consumedRight                || lp.bottomMargin != consumedBottom || lp.leftMargin != consumedLeft) {            lp.topMargin = consumedTop;            lp.rightMargin = consumedRight;            lp.bottomMargin = consumedBottom;            lp.leftMargin = consumedLeft;            mContentRoot.setLayoutParams(lp);            if (insets == null) {                // The insets have changed, but we are not currently in the process                // of dispatching them.                requestApplyInsets();            }        }        if (insets != null) {            insets = insets.replaceSystemWindowInsets(                    insets.getSystemWindowInsetLeft() - consumedLeft,                    insets.getSystemWindowInsetTop() - consumedTop,                    insets.getSystemWindowInsetRight() - consumedRight,                    insets.getSystemWindowInsetBottom() - consumedBottom);        }    }    if (insets != null) {        insets = insets.consumeStableInsets();    }    return insets;}

从上述代码中,我们可以发现在没设置 SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 和 SYSTEM_UI_FLAG_HIDE_NAVIGATION这两个属性时,会通过设置margin的方式消耗掉底部和左部mSystemWindowInsets的底部,而没设置 SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 则会消费掉mSystemWindowInsets的顶部和右部。并且在insets不为空的情况下一定会消耗掉mStableInsets。这同时也是另外一个问题的答案,要想fitsSystemWindows起效,先得设置合适的sysUiVisibility属性。

部分特殊View的 WindowInsets分发逻辑
- DrawerLayout

public DrawerLayout(Context context, AttributeSet attrs, int defStyle) {    super(context, attrs, defStyle);    ...    ...    if (ViewCompat.getFitsSystemWindows(this)) {        IMPL.configureApplyInsets(this);        mStatusBarBackground = IMPL.getDefaultStatusBarBackground(context);    }    ...    ...}

WindowInsets的分发将通过IMPL.configureApplyInsets(this)实现,以android 版本大于20为例

public void configureApplyInsets(View drawerLayout) {    DrawerLayoutCompatApi21.configureApplyInsets(drawerLayout);}
public static void configureApplyInsets(View drawerLayout) {    if (drawerLayout instanceof DrawerLayoutImpl) {        drawerLayout.setOnApplyWindowInsetsListener(new InsetsListener());        drawerLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE                | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);    }}

在该函数中设置了OnApplyWindowInsetsListener,并设置了View.SYSTEM_UI_FLAG_LAYOUT_STABLE和 View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN两个属性,根据上文可以判断,Decoreview将不会消耗mSysWindowInsets的顶部,它会参与向下级的View分发,再看看InsetsListenr的实现。

static class InsetsListener implements View.OnApplyWindowInsetsListener {    @Override    public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {        final DrawerLayoutImpl drawerLayout = (DrawerLayoutImpl) v;        drawerLayout.setChildInsets(insets, insets.getSystemWindowInsetTop() > 0);        return insets.consumeSystemWindowInsets();    }}

DrawerLayout将会消耗mSystemWindowInsets。
再看下setChildInsets实现,代码实现在DrawerLayout类中

@Overridepublic void setChildInsets(Object insets, boolean draw) {    mLastInsets = insets;    mDrawStatusBarBackground = draw;    setWillNotDraw(!draw && getBackground() == null);    requestLayout();}

可以看出mLastInsets将会参与mSystemWindowInsets的后续处理
在onMeasure(…) 函数中可以发现

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    if (applyInsets) {        final int cgrav = GravityCompat.getAbsoluteGravity(lp.gravity, layoutDirection);        if (ViewCompat.getFitsSystemWindows(child)) {            IMPL.dispatchChildInsets(child, mLastInsets, cgrav);        } else {            IMPL.applyMarginInsets(lp, mLastInsets, cgrav);        }    }}//DrawerLayoutCompatApi21.javapublic static void dispatchChildInsets(View child, Object insets, int gravity) {    WindowInsets wi = (WindowInsets) insets;    if (gravity == Gravity.LEFT) {        wi = wi.replaceSystemWindowInsets(wi.getSystemWindowInsetLeft(),                wi.getSystemWindowInsetTop(), 0, wi.getSystemWindowInsetBottom());    } else if (gravity == Gravity.RIGHT) {        wi = wi.replaceSystemWindowInsets(0, wi.getSystemWindowInsetTop(),                wi.getSystemWindowInsetRight(), wi.getSystemWindowInsetBottom());    }    child.dispatchApplyWindowInsets(wi);}public static void applyMarginInsets(ViewGroup.MarginLayoutParams lp, Object insets,int gravity) {    WindowInsets wi = (WindowInsets) insets;    if (gravity == Gravity.LEFT) {        wi = wi.replaceSystemWindowInsets(wi.getSystemWindowInsetLeft(),                wi.getSystemWindowInsetTop(), 0, wi.getSystemWindowInsetBottom());    } else if (gravity == Gravity.RIGHT) {        wi = wi.replaceSystemWindowInsets(0, wi.getSystemWindowInsetTop(),                wi.getSystemWindowInsetRight(), wi.getSystemWindowInsetBottom());    }    lp.leftMargin = wi.getSystemWindowInsetLeft();    lp.topMargin = wi.getSystemWindowInsetTop();    lp.rightMargin = wi.getSystemWindowInsetRight();    lp.bottomMargin = wi.getSystemWindowInsetBottom();}

如果child中设置fitsSystemWindow = true 属性,则会执行子view的dispatch, 否则会重新设置View的margin属性

  • CoordinatorLayout

与DrawerLayout类似,通过设置OnApplyWindowInsetsListener来改变它的dispatchApply逻辑,与DrawerLayout最大的区别在于它对子view的分发是通过Behavior实现的。

  • CollapsingToolbarLayout

它也是通过设置OnApplyWindowInsetsListener来实现的, 并且当它的VieParent是AppBarLayout时,它的fitsSystemWindow属性与其ViewParent一致

@Overrideprotected void onAttachedToWindow() {    super.onAttachedToWindow();    // Add an OnOffsetChangedListener if possible    final ViewParent parent = getParent();    if (parent instanceof AppBarLayout) {        // Copy over from the ABL whether we should fit system windows            ViewCompat.setFitsSystemWindows(this, ViewCompat.getFitsSystemWindows((View) parent));        if (mOnOffsetChangedListener == null) {            mOnOffsetChangedListener = new OffsetUpdateListener();        }        ((AppBarLayout) parent).addOnOffsetChangedListener(mOnOffsetChangedListener);        // We're attached, so lets request an inset dispatch        ViewCompat.requestApplyInsets(this);    }}WindowInsetsCompat onWindowInsetChanged(final WindowInsetsCompat insets) {    WindowInsetsCompat newInsets = null;    if (ViewCompat.getFitsSystemWindows(this)) {        // If we're set to fit system windows, keep the insets        newInsets = insets;    }    // If our insets have changed, keep them and invalidate the scroll ranges...    if (!objectEquals(mLastInsets, newInsets)) {        mLastInsets = newInsets;        requestLayout();    }    // Consume the insets. This is done so that child views with fitSystemWindows=true do not    // get the default padding functionality from View    return insets.consumeSystemWindowInsets();}

它会消耗windowinsets并且让子view不再消耗WindowInsets了。

原创粉丝点击