WindowInsets 在View下的的分發(fā)(二)

緒論

在上一篇中,大概說(shuō)明了下WindowInsets的概念和分發(fā)邏輯,然而在部分情況下,我們會(huì)發(fā)現(xiàn)即便設(shè)置了fitSSystemWindows = true 也并沒有生效;而且從上文已知的情況可以看出,即便消費(fèi)WindowInsets似乎也只是在消費(fèi)SystemWindowInsets,其它的Insets似乎并沒有被消耗。這一篇將解決這兩個(gè)問(wèn)題。

WindowInsets 在View下的的分發(fā)(一)

mWindowDecorInsets和mStableInsets的消耗

fitSSystemWindows = true 生效的首要條件

這個(gè)問(wèn)題的答案,我們可以在ViewRootImpl和Decoreview這兩個(gè)類中找到答案
在ViewRootImpl的源碼中,我們可以發(fā)現(xiàn)這樣一段代碼

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;
}

從上述代碼中,可以發(fā)現(xiàn)被dispatchapply的WindowInsets來(lái)源于getWindowInsets(...)。而在這個(gè)函數(shù)中,我們可以發(fā)現(xiàn)mWindowDecorInsets值為null,表明其從一開始就是消耗狀態(tài)。
再看看DecoreView的代碼

@Override
public 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 didn't 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're 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;
}

從上述代碼中,我們可以發(fā)現(xiàn)在沒設(shè)置 SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 和 SYSTEM_UI_FLAG_HIDE_NAVIGATION這兩個(gè)屬性時(shí),會(huì)通過(guò)設(shè)置margin的方式消耗掉底部和左部mSystemWindowInsets的底部,而沒設(shè)置 SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 則會(huì)消費(fèi)掉mSystemWindowInsets的頂部和右部。并且在insets不為空的情況下一定會(huì)消耗掉mStableInsets。這同時(shí)也是另外一個(gè)問(wèn)題的答案,要想fitsSystemWindows起效,先得設(shè)置合適的sysUiVisibility屬性。

部分特殊View的 WindowInsets分發(fā)邏輯

  • 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的分發(fā)將通過(guò)IMPL.configureApplyInsets(this)實(shí)現(xiàn),以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);
    }
}

在該函數(shù)中設(shè)置了OnApplyWindowInsetsListener,并設(shè)置了View.SYSTEM_UI_FLAG_LAYOUT_STABLE和 View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN兩個(gè)屬性,根據(jù)上文可以判斷,Decoreview將不會(huì)消耗mSysWindowInsets的頂部,它會(huì)參與向下級(jí)的View分發(fā),再看看InsetsListenr的實(shí)現(xiàn)。

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將會(huì)消耗mSystemWindowInsets。
再看下setChildInsets實(shí)現(xiàn),代碼實(shí)現(xiàn)在DrawerLayout類中

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

可以看出mLastInsets將會(huì)參與mSystemWindowInsets的后續(xù)處理
在onMeasure(...) 函數(shù)中可以發(fā)現(xiàn)

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.java
public 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中設(shè)置fitsSystemWindow = true 屬性,則會(huì)執(zhí)行子view的dispatch, 否則會(huì)重新設(shè)置View的margin屬性

  • CoordinatorLayout

與DrawerLayout類似,通過(guò)設(shè)置OnApplyWindowInsetsListener來(lái)改變它的dispatchApply邏輯,與DrawerLayout最大的區(qū)別在于它對(duì)子view的分發(fā)是通過(guò)Behavior實(shí)現(xiàn)的。

  • CollapsingToolbarLayout

它也是通過(guò)設(shè)置OnApplyWindowInsetsListener來(lái)實(shí)現(xiàn)的, 并且當(dāng)它的VieParent是AppBarLayout時(shí),它的fitsSystemWindow屬性與其ViewParent一致

@Override
protected 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();
}

它會(huì)消耗windowinsets并且讓子view不再消耗WindowInsets了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容