美文网首页
Android的沉浸式解析

Android的沉浸式解析

作者: Horps | 来源:发表于2023-04-22 18:45 被阅读0次
  • 概述

    默认的,app的内容总是填充在状态栏之下&导航栏之上,状态栏和导航栏被称为系统栏,沉浸式就是把app的内容延伸到系统栏,并且要控制内容分布不要被系统栏的信息遮盖,而且不能和系统的手势冲突,比如状态栏的下拉和导航栏的上滑。

    在开发过程中,你可能遇到过为什么fitSystemWindow有的版本有效果有的没效果或者达不到想要的效果的问题,不知道怎么完美解决沉浸式的实现,你可能也会使用第三方的框架来实现,比如QMUIWindowLayout,但是又不明白它的源码逻辑为什么要那么写,我们今天就来把这个骨头啃一啃。

  • 官方沉浸式做法

    1. 将内容延伸到系统栏:

      override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        WindowCompat.setDecorFitsSystemWindows(window, false)
      }
      
    2. 经过第一步之后,如果系统栏有颜色则会覆盖住主内容,所以要修改系统栏背景色为透明,从而让应用内容显示出来。

      其次,系统栏的图标等信息颜色可能会和背景融为一体,这是需要修改系统栏内容(通知图标等)的颜色来避免和应用内容颜色重叠。

      <style name="Theme.MyApp">
        <!-- 修改系统栏背景色为透明,values-v19/themes.xml -->
        <item name="android:windowTranslucentStatus">true</item>
        <item name="android:windowTranslucentNavigation">true</item>
        
        <!-- 修改系统栏背景色为透明,values-v21/themes.xml -->
        <item name="android:navigationBarColor">
           @android:color/transparent
        </item>
        <item name="android:statusBarColor">
           @android:color/transparent
        </item>
      
        <!-- 
                 修改系统栏内容(通知图标等)的颜色
                     values-v23/themes.xml 
                     Optional: set the status bar light and content dark. 
         -->
        <item name="android:windowLightStatusBar">
          true
        </item>
      </style>
      

      对于windowLightStatusBar来说,你也可以通过代码动态地修改系统栏的内容配色模式,只有两种,一种亮色模式一种暗色模式:

      val windowInsetsController = ViewCompat.getWindowInsetsController(window.decorView)
      windowInsetsController?.isAppearanceLightNavigationBars = true
      

      对于4.4到5.0之间的版本来说,只能指定android:windowTranslucentXxx属性来使系统栏透明,但是会有一个渐变阴影存在:

      截屏2023-04-21 14.42.40.png 截屏2023-04-21 14.42.53.png

      对于5.0及以上版本之后,可以设置android:statusBarColor和android:navigationBarColor置为完全透明,无阴影。

    3. 第三步就是处理布局重叠问题了,完成上面的设置后,你会发现UI重叠,这个时候需要:

      ViewCompat.setOnApplyWindowInsetsListener(rootView) { view, windowInsets ->
        val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
        view.updatePadding(statusInsets.left, statusInsets.top, statusInsets.right, statusInsets.bottom)
        //返回windowInsets而不是WindowInsetsCompat.CONSUMED,为了继续往下传递insets
        //WindowInsetsCompat.CONSUMED
        windowInsets                                              
      }
      

      给布局设置上下的偏移以达到不被系统栏遮挡,注意,如果内部的子View处理了insets,则可能会和这里的跟布局的处理重复从而造成一些问题,通常子View不需要再额外处理,除非有特殊需求。比如,BottomNavigationView设置了OnApplyWindowInsetsListener,其内部有了对于insets左右和底部padding的处理,因此它会自动适配导航栏的高度,防止遮挡,这时如果跟布局也增加了对于insets的margin则会多出一块偏移,所以这种情况下跟布局的处理返回就要是WindowInsetsCompat.CONSUMED,这样子View(BottomNavigationView)就不会收到回调了。

      通常,没有额外处理的话,只需要跟布局添加OnApplyWindowInsetsListener处理insets偏移,然后返回WindowInsetsCompat.CONSUMED即可,当然也可以换种方式,即每个页面的子View单独添加监听处理,这样就很繁琐也不好维护。

      设置android:fitsSystemWindows="true"也能实现避免遮挡,但这种方式需要单独页面配置。

    4. 对于刘海屏(DisplayCutout)来说,9.0及以上的版本才会兼容,对于该版本以下存在的情况来说则需要根据具体厂商的api进行手动获取高度并进行适当偏移。

下面我们从源码角度来分析WindowInsets的原理。

  • WindowCompat.setDecorFitsSystemWindows(window, false)

    这个方法让应用内容可以延伸到系统栏下面去,我们来看它是怎么实现的:

    public static void setDecorFitsSystemWindows(@NonNull Window window,
            final boolean decorFitsSystemWindows) {
        if (Build.VERSION.SDK_INT >= 30) {
            Api30Impl.setDecorFitsSystemWindows(window, decorFitsSystemWindows);
        } else if (Build.VERSION.SDK_INT >= 16) {
            Api16Impl.setDecorFitsSystemWindows(window, decorFitsSystemWindows);
        }
    }
    

    这里有版本适配处理,低于16(4.1)没有沉浸式。

    对于4.1到11之间的版本:

    static void setDecorFitsSystemWindows(@NonNull Window window,
            final boolean decorFitsSystemWindows) {
        final int decorFitsFlags = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
    
        final View decorView = window.getDecorView();
        final int sysUiVis = decorView.getSystemUiVisibility();
        decorView.setSystemUiVisibility(decorFitsSystemWindows
                ? sysUiVis & ~decorFitsFlags
                : sysUiVis | decorFitsFlags);
    }
    

    通过设置SystemUiVisibility来实现沉浸式 :

    • View.SYSTEM_UI_FLAG_LAYOUT_STABLE:表示systembar显示隐藏时都不会影响应用布局,否则应用布局可能会随之变化。
    • View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN:使应用内容延伸到系统栏下面。
    • View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION:隐藏导航栏,但实际上配置这个在5.0上并不会隐藏导航栏,这个标志只是表示需要延伸到系统栏下面而已,下面源码分析会看到,而使用SYSTEM_UI_FLAG_HIDE_NAVIGATION则可以隐藏导航栏。

    对于11版本之后的,会调用PhoneWindow的setDecorFitsSystemWindows方法:

    @Override
    public void setDecorFitsSystemWindows(boolean decorFitsSystemWindows) {
        mDecorFitsSystemWindows = decorFitsSystemWindows;
        applyDecorFitsSystemWindows();
    }
    
    private void applyDecorFitsSystemWindows() {
        ViewRootImpl impl = getViewRootImplOrNull();
        if (impl != null) {
            impl.setOnContentApplyWindowInsetsListener(mDecorFitsSystemWindows
                    ? sDefaultContentInsetsApplier
                    : null);
        }
    }
    

    沉浸式的时候mDecorFitsSystemWindows会被赋值为false,ViewRootImpl设置的OnContentApplyWindowInsetsListener为null:

    public void setOnContentApplyWindowInsetsListener(OnContentApplyWindowInsetsListener listener) {
        mAttachInfo.mContentOnApplyWindowInsetsListener = listener;
        if (!mFirst) {
            requestFitSystemWindows();
        }
    }
    

    requestFitSystemWindows中会调用scheduleTraversals方法进行布局渲染流程。

    为什么这里要判断mFirst为false呢?因为ViewRootImpl创建的时候mFirst是true,performTraversals中有:

    if (mFirst) {
        ...
        dispatchApplyInsets(host);
    }
    

    因此,在第一次创建渲染的时候自然会调用dispatchApplyInsets方法,此时不需要重复再去渲染。

  • dispatchApplyInsets

    此时重点来到了dispatchApplyInsets方法:

    public void dispatchApplyInsets(View host) {
        ...
        WindowInsets insets = getWindowInsets(true /* forceConstruct */);
        ...
        host.dispatchApplyWindowInsets(insets);
        ...
    }
    

    insets从getWindowInsets中获取,其实就是生成一个新的WindowInsets对象,此时的偏移量还没设置。host是mView,mView就是DecorView,DecorView并没有实现这个方法,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;
        }
    }
    

    如果设置了OnApplyWindowInsetsListener则会执行这个回调,否则会调用实现的onApplyWindowInsets方法,DecorView中就实现了这个方法:

    @Override
    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
        ...
        insets = updateColorViews(insets, true /* animate */);
        ...
        return insets;
    }
    

    在updateColorViews方法中:

    WindowInsets updateColorViews(WindowInsets insets, boolean animate) {
        WindowManager.LayoutParams attrs = mWindow.getAttributes();
        int sysUiVisibility = attrs.systemUiVisibility | getWindowSystemUiVisibility();
    
        final WindowInsetsController controller = getWindowInsetsController();
    
        // 和其他floating类型的window不同,IME是一种特殊的floating类型的window,也需要适配insets
        final boolean isImeWindow =
                mWindow.getAttributes().type == WindowManager.LayoutParams.TYPE_INPUT_METHOD;
          //只有非floating类型的window和IME才需要适配insets
          //floating窗口表示的类似于windows系统上的窗口,mDecorCaptionView持有最小化、关闭等操作按钮
        if (!mWindow.mIsFloating || isImeWindow) {
                  ...
            final ViewRootImpl viewRoot = getViewRootImpl();
            final @Appearance int appearance = viewRoot != null
                    ? viewRoot.mWindowAttributes.insetsFlags.appearance
                    : controller.getSystemBarsAppearance();
    
            if (insets != null) {
                ...
                //获取不随bar的显示隐藏而变化的区域
                final Insets stableBarInsets = insets.getInsetsIgnoringVisibility(
                        WindowInsets.Type.systemBars());
                  //系统栏、刘海屏和稳定显示区域取交集
                final Insets systemInsets = Insets.min(insets.getInsets(WindowInsets.Type.systemBars()
                                | WindowInsets.Type.displayCutout()), stableBarInsets);
                  //记录系统栏的偏移量,比如top是状态栏和刘海屏中较大的那一个
                mLastTopInset = systemInsets.top;
                mLastBottomInset = systemInsets.bottom;
                mLastRightInset = systemInsets.right;
                mLastLeftInset = systemInsets.left;
                          ...
            }
    
              //修改导航栏颜色
            boolean navBarToRightEdge = isNavBarToRightEdge(mLastBottomInset, mLastRightInset);
            boolean navBarToLeftEdge = isNavBarToLeftEdge(mLastBottomInset, mLastLeftInset);
            int navBarSize = getNavBarSize(mLastBottomInset, mLastRightInset, mLastLeftInset);
            updateColorViewInt(mNavigationColorViewState, calculateNavigationBarColor(appearance),
                    mWindow.mNavigationBarDividerColor, navBarSize,
                    navBarToRightEdge || navBarToLeftEdge, navBarToLeftEdge,
                    0 /* sideInset */, animate && !disallowAnimate,
                    mForceWindowDrawsBarBackgrounds, controller);
            boolean oldDrawLegacy = mDrawLegacyNavigationBarBackground;
            ...
    
            boolean statusBarNeedsRightInset = navBarToRightEdge
                    && mNavigationColorViewState.present;
            boolean statusBarNeedsLeftInset = navBarToLeftEdge
                    && mNavigationColorViewState.present;
            int statusBarSideInset = statusBarNeedsRightInset ? mLastRightInset
                    : statusBarNeedsLeftInset ? mLastLeftInset : 0;
            int statusBarColor = calculateStatusBarColor(appearance);
            updateColorViewInt(mStatusColorViewState, statusBarColor, 0,
                    mLastTopInset, false /* matchVertical */, statusBarNeedsLeftInset,
                    statusBarSideInset, animate && !disallowAnimate,
                    mForceWindowDrawsBarBackgrounds, controller);
                  ...
        }
          //是否隐藏导航栏
        boolean hideNavigation = (sysUiVisibility & SYSTEM_UI_FLAG_HIDE_NAVIGATION) != 0
                || !(controller == null || controller.isRequestedVisible(ITYPE_NAVIGATION_BAR));
          //这个是我们前面设置的false
        boolean decorFitsSystemWindows = mWindow.mDecorFitsSystemWindows;
        boolean forceConsumingNavBar = (mForceWindowDrawsBarBackgrounds
                        && (attrs.flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) == 0
                        && (sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) == 0
                        && decorFitsSystemWindows
                        && !hideNavigation)
                || (mLastShouldAlwaysConsumeSystemBars && hideNavigation);
    
        boolean consumingNavBar =
                ((attrs.flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0
                        && (sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) == 0
                        && decorFitsSystemWindows
                        && !hideNavigation)
                || forceConsumingNavBar;
    
        // 全凭可以通过sysUiVisibility、attrs和WindowInsetsController三个地方决定
        boolean fullscreen = (sysUiVisibility & SYSTEM_UI_FLAG_FULLSCREEN) != 0
                || (attrs.flags & FLAG_FULLSCREEN) != 0
                || !(controller == null || controller.isRequestedVisible(ITYPE_STATUS_BAR));
        boolean consumingStatusBar = (sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) == 0
                && decorFitsSystemWindows
                && (attrs.flags & FLAG_LAYOUT_IN_SCREEN) == 0
                && (attrs.flags & FLAG_LAYOUT_INSET_DECOR) == 0
                && mForceWindowDrawsBarBackgrounds
                && mLastTopInset != 0
                || (mLastShouldAlwaysConsumeSystemBars && fullscreen);
    
          //这里的consumedXxx表示上一级是否消耗掉了insets,如果消耗掉了,则下面的内容布局就不再消耗了,即不会延伸到系统栏下的区域
        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) {
                  //mContentRoot是DecorView中应用内容显示区域的父容器,这里会给它设置margin来达到消耗insets的效果,比如状态栏若是26px,则应用内容会在状态栏下26px的位置开始绘制,这就是非沉浸式的;如果要沉浸式,则需要consumedXxx是0
                lp.topMargin = consumedTop;
                lp.rightMargin = consumedRight;
                lp.bottomMargin = consumedBottom;
                lp.leftMargin = consumedLeft;
                mContentRoot.setLayoutParams(lp);
                          ...
            }
            if (insets != null) {
                  //这里会把系统栏的偏移量设置进去
                insets = insets.inset(consumedLeft, consumedTop, consumedRight, consumedBottom);
            }
        }
          ...
        return insets;
    }
    

    下面我们来整理一下上面逻辑的意义。

    1. 首先获取系统栏相对于屏幕边缘的四个边偏移。

    2. 其次判断是否应用沉浸式模式,判断的依据有很多,这里我们只考虑设置沉浸式方法中使用的相关标志的影响。

      还记得我们调用WindowCompat.setDecorFitsSystemWindows时的版本分支吧,对于16~30之间的版本,会通过设置setSystemUiVisibility为View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN的形式来使内容延伸到系统栏下面,对于这三个属性,SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION表示应用内容需要隐藏导航栏,因此配置这个属性会导致系统窗口不会consume掉insets偏移(其他属性你可以直接理解成符合条件,因为沉浸式设置时没有用到,比如attrs.flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS直接认为是true,相当于默认没配置),mLastShouldAlwaysConsumeSystemBars这个值是用来设置强制消费insets的,默认是false。因此,影响应用内容延伸到导航栏的因素就是setSystemUiVisibility设置为View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION或者SYSTEM_UI_FLAG_HIDE_NAVIGATION,或者给DecorView设置了WindowInsetController并且它的isRequestedVisible(ITYPE_NAVIGATION_BAR)返回false,SYSTEM_UI_FLAG_HIDE_NAVIGATION的意思是隐藏掉导航栏,自然应用区域随着需要延伸到最边缘,另外两个条件不会造成导航栏隐藏,但是表示需要延伸到导航栏。所以当设置了SYSTEM_UI_FLAG_HIDE_NAVIGATION、SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION或者添加的WindowInsetController的isRequestedVisible(ITYPE_NAVIGATION_BAR)返回false满足其一都会使consumingNavBar为false,若为false则mContentRoot(应用内容区域)就会较屏幕底部往上偏移0的距离,相当于不偏移,因此就会延伸到导航栏下面。

      同理,对于statusBar来说也是一样,产生影响的是SYSTEM_UI_FLAG_FULLSCREEN和SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN,一个表示隐藏状态栏,一个表示不隐藏但是需要延伸到状态栏下面,consumingStatusBar的判断没有单独根据fullscreen决定是因为默认把fullscreen当成需要延伸。mDecorFitsSystemWindows和上面的属性是或的关系,一样的作用,是针对不同版本的兼容,默认为true。

    3. 最后,设置mContentRoot的margin。

  • fitSystemWindow属性

    我们知道,View设置fitSystemView属性后会自动适配insets的偏移,这是怎么做到的呢?

    在ViewGroup中:

    @Override
    public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {
        insets = super.dispatchApplyWindowInsets(insets);
        if (insets.isConsumed()) {
            return insets;
        }
          //下面为了简单,是我把另一个方法中的代码内联过来了
        final int count = getChildCount();
        for (int i = 0; i < count; i++) {
            getChildAt(i).dispatchApplyWindowInsets(insets);
        }
        return insets;
    }
    

    可以看到,dispatchApplyWindowInsets会从DecorView往下分发,一直到最后一个View或者insets的isConsumed返回true:

    public boolean isConsumed() {
        return mSystemWindowInsetsConsumed && mStableInsetsConsumed
                && mDisplayCutoutConsumed;
    }
    

    WindowInstes类中有一个:

    public static final @NonNull WindowInsets CONSUMED;
    
    static {
        CONSUMED = new WindowInsets((Rect) null, null, false, false, null);
    }
    

    它符合isConsumed方法中为true的要求,如果不想继续往下分发insets则只需要在传递过程中返回一个WindowIsntes.CONSUMED即可。

    @Deprecated
    protected boolean fitSystemWindows(Rect insets) {
        if ((mPrivateFlags3 & PFLAG3_APPLYING_INSETS) == 0) {
            if (insets == null) {
                return false;
            }
            try {
                  //添加这个属性,在下面流程中会再次调用到fitSystemWindows方法
                mPrivateFlags3 |= PFLAG3_FITTING_SYSTEM_WINDOWS;
                  //dispatchApplyWindowInsets方法中会添加PFLAG3_APPLYING_INSETS,这个属性会导致再次回到fitSystemWindows方法时会调用fitSystemWindowsInt方法
                return dispatchApplyWindowInsets(new WindowInsets(insets)).isConsumed();
            } finally {
                mPrivateFlags3 &= ~PFLAG3_FITTING_SYSTEM_WINDOWS;
            }
        } else {
            return fitSystemWindowsInt(insets);
        }
    }
    

    可以看到,这个方法其实被废弃了,推荐通过给View设置OnApplyWindowInsetsListener的方式消费insets。dispatchApplyWindowInsets中同样也是会调用OnApplyWindowInsetsListener或者onApplyWindowInsets方法。View的这个方法默认实现如下:

    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) {
            if (fitSystemWindows(insets.getSystemWindowInsetsAsRect())) {
                return insets.consumeSystemWindowInsets();
            }
        } else {
              //如果不是因为设置了fitSystemWindows走到这的,也会调用fitSystemWindowsInt,因此你可以通过重写具体View的onApplyWindowInsets方法来消费insets
            if (fitSystemWindowsInt(insets.getSystemWindowInsetsAsRect())) {
                return insets.consumeSystemWindowInsets();
            }
        }
        return insets;
    }
    

    前面设置了PFLAG3_FITTING_SYSTEM_WINDOWS,因此这里又走回到fitSystemWindows,此时有了PFLAG3_APPLYING_INSETS,因此会走到fitSystemWindowsInt方法:

    private boolean fitSystemWindowsInt(Rect insets) {
        if ((mViewFlags & FITS_SYSTEM_WINDOWS) == FITS_SYSTEM_WINDOWS) {
            Rect localInsets = sThreadLocal.get();
            boolean res = computeFitSystemWindows(insets, localInsets);
            applyInsets(localInsets);
            return res;
        }
        return false;
    }
    

    可以看到,需要设置fitSystemWindow属性,才会调用applyInsets方法,localInsets是上面传下来的系统insets:

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

    这里会通过设置View的padding为insets的偏移来达到解决和沉浸式系统栏的UI重叠问题的。

  • 总结

    • 通过底层API获取系统栏的占用偏移,其中top通过状态栏或者刘海屏等的高度确定的,取偏移最大的那个值,而左右和下偏移则是由导航栏的大小决定的。
    • DecorView中会先决定是否把所有屏幕区域都可交给子View处理,你可以根据不同版本设置不同属性来控制。
    • DecorView会给mContentParent(用户布局存放的地方)设置margin来给系统栏留出空间,如果margin为0,则用户应用区域就会延伸到系统栏区域了,也就是沉浸式了,这就是原理。
    • WindowInsets会携带者偏移数据从DecorView开始往下传递,默认子View的消费逻辑是给其设置padding来避免和系统栏的UI重叠冲突,但是这样的默认设置只能适用于最外层的根布局View,为啥?因为它会给View设置四个边的padding,比如给标题栏的View设置了fitSystemWindow属性后,你会发现它多了一个不需要的bottomPadding空间,如果导航栏不是充满左右的话,还会有左右padding产生。但是用于最外层这种方式还是无法完美适合某些场景,比如我需要看起来状态栏的背景色和标题栏颜色一致,但是标题栏颜色和根布局颜色不一致,这种情况下你会发现fitSystemWindow会使状态栏的背景色和根布局的一致而不是标题栏,这就会导致看起来好像不是沉浸式模式。
  • 完美方案

    通过源码分析和总结,我们发现,传统的fitSystemWindow并不能完全解决沉浸式的适配问题,对此,有两种方式可以解决。

    • 重写View的fitSystemWindow来针对View的位置进行不同的insets设置,比如查到该View在布局的最顶部,则只需要应用上级传下来的insets的top,而botton设置为0,对于最底部的则相反,以此来针对不同的场景,腾讯的QUMUIWindowLayout就是用的这种方式。
    • 官方推出了新的兼容API来实现:
      ViewCompat.setOnApplyWindowInsetsListener(getBinder().root) { view, windowInsets ->
                  //防止UI冲突
                  val systemInsets =
                      windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
                  view.updatePadding(
                      systemInsets.left,
                      systemInsets.top,
      //                max(windowInsets.displayCutout?.safeInsetTop ?: 0, systemInsets.top),
                      systemInsets.right,
                      systemInsets.bottom
                  )
                  WindowInsetsCompat.CONSUMED
              }
      

      这里是针对根布局的通用设置,因此返回WindowInsetsCompat.CONSUMED使其不再往下传递,避免有的子View有自己的处理而产生重复设置的问题。

    上面是针对解决沉浸式模式下应用UI和系统栏的重叠问题的方法,他们都需要一个前提,就是我们上面分析的需要设置一些标志来开启沉浸式模式,调用下面这句代码可以完美实现:

    WindowCompat.setDecorFitsSystemWindows(window, false)
    

    WindowCompat内部做了兼容处理。

相关文章

  • Android 补充 LayerDrawable 沉浸式状态栏

    小菜前些日子整理了两次小小的沉浸式状态栏的总结:Android 沉浸式状态栏的多种样式 和 Android 沉浸式...

  • android沉浸式状态栏 轻量 简便

    android沉浸式状态栏 android 沉浸式状态栏 网上看了很多沉浸式,感觉用起来麻烦,而且有些库非常大,于...

  • Android 沉浸式模式与常见状态栏和导航栏效果

    Android沉浸式模式 官方称沉浸式状态栏为沉浸式模式。 什么是沉浸式?沉浸式就是让人专注当前的(由设计者营造)...

  • Android学习目录

    Theme风格 Android 沉浸式风格(为毛叫沉浸式这么唬人) Build编译 Android 多渠道打包(同...

  • android沉浸式状态栏

    android沉浸式状态栏 参考文章 另外两种android沉浸式状态栏实现思路 android4.4+实现MD状...

  • Android 沉浸式解析和轮子使用

    前言 我们先一起来回顾一下实现沉浸式状态栏的一般套路。在Android上,关于对StatusBar(状态栏)的操作...

  • AndroidView

    Android 沉浸式 (透明) 状态栏适配 Android 沉浸式状态栏仿淘宝、京东拖拽商品详情(可嵌套View...

  • android沉浸式

    一、内容概述: 1、如何修改状态栏颜色。2、如何修改状态栏文字颜色。3、如何不被虚拟键隐藏,或隐藏虚拟键。4、如何...

  • Android沉浸式状态栏技术方案调研与实现

    参考资料: Android关于沉浸式状态栏总结 Android 沉浸式状态栏完美解决方案 项目Demo GitHu...

  • 沉浸式状态栏

    1、1218683832/ImmersionBar android 4.4以上沉浸式状态栏和沉浸式导航栏管理,适配...

网友评论

      本文标题:Android的沉浸式解析

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