美文网首页Android知识Android开发程序员
WindowInsets 在View下的的分发(二)

WindowInsets 在View下的的分发(二)

作者: me_touch | 来源:发表于2017-09-05 10:56 被阅读0次

    绪论

    在上一篇中,大概说明了下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的代码

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

    从上述代码中,我们可以发现在没设置 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类中

    @Override
    public 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.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中设置fitsSystemWindow = true 属性,则会执行子view的dispatch, 否则会重新设置View的margin属性

    • CoordinatorLayout

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

    • CollapsingToolbarLayout

    它也是通过设置OnApplyWindowInsetsListener来实现的, 并且当它的VieParent是AppBarLayout时,它的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();
    }
    

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

    相关文章

      网友评论

        本文标题:WindowInsets 在View下的的分发(二)

        本文链接:https://www.haomeiwen.com/subject/zvlujxtx.html