这不是沉浸式状态栏

前言

首先请你们看几张图:
html


以上的效果,通常咱们统称为沉浸式状态栏。其实,这种叫法不是很准确,并且也没有沉浸式状态栏这一说,只有沉浸模式。以上几种状况,能够称为透明状态栏或者状态栏着色。

1、两种状态

进行Android开发时,有两种方式都会对状态栏进行设置:Translucent Bar(透明栏)和Immersive Mode(沉浸模式)。二者的区别,比较直观的一点,就是体如今屏幕中的View可点击区域,以下所示。android

  • 沉浸模式
    隐藏status bar(状态栏),使屏幕全屏,让Activity接收全部的(整个屏幕的)触摸事件。git

  • 状态栏着色
    设置状态栏颜色,状态栏部分不接收处理触摸事件。布局侵入状态栏的后面,必须启用fitsSystemWindows属性来调整布局才不至于被状态栏覆盖。如透明状态栏、与titlebar颜色一致的状态栏,即如以前的图所示。github

2、如何沉浸

从3.x版本开始, 系统DecorView提供了setSystemUiVisibility方法, 能够经过设置Flag更改SystemUI的属性。各个设置的参数含义以下所示:api

参数 api版本 含义
View.SYSTEM_UI_FLAG_VISIBLE 14 默认标记
View.SYSTEM_UI_FLAG_LOW_PROFILE 14 低功耗模式, 会隐藏状态栏图标, 在4.0上能够实现全屏
View.SYSTEM_UI_FLAG_LAYOUT_STABLE 16 保持整个View稳定,常跟bar 悬浮、隐藏共用, 使View不会由于SystemUI的变化而作layout
View.SYSTEM_UI_FLAG_FULLSCREEN 16 状态栏隐藏
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 16 状态栏上浮于Activity
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 14 隐藏导航栏
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 16 导航栏上浮于Activity
View.SYSTEM_UI_FLAG_IMMERSIVE 19 Kitkat新加入的Flag, 沉浸模式, 能够隐藏掉status跟navigation bar, 而且在第一次会弹泡提醒, 它会覆盖掉以前两个隐藏bar的标记, 而且在bar出现的位置滑动能够呼出bar
View.SYSTEM_UI_FLAG_IMMERSIVE_STIKY 19 与上面惟一的区别是, 呼出隐藏的bar后会自动再隐藏掉

综上所述,要实现全屏沉浸模式,只需以下设置便可:bash

//Hide
getWindow().getDecorView().setSystemUiVisibility(
                View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                        | View.SYSTEM_UI_FLAG_LOW_PROFILE
                        | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                        | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                        | View.SYSTEM_UI_FLAG_FULLSCREEN
                        | View.SYSTEM_UI_FLAG_IMMERSIVE_STIKY);复制代码

实现沉浸效果以下图所示。左边是隐藏状态栏和导航栏的效果,整个屏幕均可以接收触摸反馈;右边图是滑动显示的状态栏和导航栏的效果,内容显示在系统栏的下方,显示一段时间后会自动隐藏。app

沉浸模式
沉浸模式

3、如何着色

3.1 两个系统节点

对于状态栏着色,有两个比较关键的系统节点须要关注,分别是4.4和5.0。基于两个系统节点,咱们又能够分红三个阶段进行讨论。ide

  • 4.4之前
    状态栏不支持设置颜色。函数

  • 4.4 ~ 5.0
    状态栏支持透明效果,可是系统不提供接口进行颜色设置(有办法,文章后面会介绍)。工具

  • 5.0及以上
    系统提供接口对状态栏进行任意颜色设置。

3.2 实现的两种方式

3.2.1 主题和布局设置
  1. 在values、values-v1九、values-v21的style.xml都设置一个 Translucent System Bar 风格的Theme

    • values/style.xml

      <style name="ImageTranslucentTheme" parent="AppTheme">
        <!--在Android 4.4以前的版本上运行,直接跟随系统主题-->
      </style>复制代码
    • values-v19/style.xml

      <style name="ImageTranslucentTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="android:windowTranslucentStatus">true</item>
        <item name="android:windowTranslucentNavigation">true</item>
      </style>复制代码
    • values-v21/style.xml

      <style name="ImageTranslucentTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="android:windowTranslucentStatus">false</item>
        <item name="android:windowTranslucentNavigation">true</item>
        <item name="android:statusBarColor">@android:color/transparent</item>
      </style>复制代码
  2. 在AndroidManifest.xml中对指定Activity的theme进行设置
<activity android:name=".MainActivity"
    android:theme="@style/ImageTranslucentTheme"
    >
</activity>复制代码
  1. 在Activity的布局文件中设置背景图片,同时,须要把android:fitsSystemWindows设置为true
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@mipmap/paperscan_guide_step_two_inside"
    android:fitsSystemWindows="true"
    >

    <TextView
        android:text="透明状态栏~"
        android:textColor="@android:color/white"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</LinearLayout>复制代码

如此能够获得效果以下所示

实现透明效果的图
实现透明效果的图

那么,为何要将android:fitsSystemWindows设置为true呢?若是不设置会怎么样?咱们能够来测试下,下图是不设置(默认为false)的效果。可见内容跑到了状态栏的下面,被状态栏覆盖了。因此,最直观的一点咱们能够发现,设置了android:fitsSystemWindows为true,可让内容不会顶到状态栏的下面。文章Android沉浸式状态栏实现fitsSystemWindows已经作了详细的分析,你们有兴趣能够看看。

android:fitsSystemWindows为false的图
android:fitsSystemWindows为false的图

接着上面的问题,既然咱们实现透明状态栏效果的页面都须要设置fitsSystemWindows属性,因此咱们想到了一种方便的方法,在theme中加上以下的android:fitsSystemWindows设置:

<item name="android:fitsSystemWindows">true</item>复制代码

运行发现真的能够,显示的内容也没有和状态栏重叠。可是,当咱们显示一个toast的时候,发现问题了。

Toast.makeText(MainActivity.this,"toast sth...",Toast.LENGTH_SHORT).show();复制代码

toast错位的图片
toast错位的图片

如图所示,Toast打印出来的文字都向上偏移了。缘由是由于咱们是在Theme中设置的fitsSystemWindows属性,会影响使用了该theme的activity或application的行为,形成依附于Application Window的Window(好比Toast)错位。针对Toast错位的问题,解决方法也简单,就是使用应用级别的上下文

Toast.makeText(getApplicationContext(),"toast sth...",Toast.LENGTH_SHORT).show();复制代码

虽然说Toast错误问题也是有方法能够解决,可是若是这样使用,不经意间会给咱们的应用埋下不少坑。因此个人建议是:不要滥用,只在有须要的地方添加fitsSystemWindows属性。

3.2.2 代码设置

正如前面提到的,只有4.4以上的系统才支持透明状态栏设置,5.0以上的系统支持设置状态栏任意颜色。因此5.0以上的系统设置状态栏的颜色就很简单了,直接调用系统接口就能够了:

// 设置此flag才可对状态栏进行颜色设置
activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
// 取消设置透明状态栏,否则颜色设置不生效
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
// 设置状态栏颜色
activity.getWindow().setStatusBarColor(color);复制代码

而对于系统是4.4到5.0之间的机子,要设置状态栏的颜色就稍微要繁琐一点了。首先,须要设置页面状态栏为透明;而后,新建一个和状态栏高度一致的view,填充到DecorView上;最后,经过设置这个填充view的颜色,咱们就能实现相似对状态栏颜色进行控制的效果了。

// 设置透明
activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
// 生成view填充DecorView
ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
View fakeStatusBarView = decorView.findViewById(FAKE_STATUS_BAR_VIEW_ID);
if (fakeStatusBarView != null) {
    if (fakeStatusBarView.getVisibility() == View.GONE) {
        fakeStatusBarView.setVisibility(View.VISIBLE);
    }
    fakeStatusBarView.setBackgroundColor(calculateStatusColor(color, statusBarAlpha));
} else {
    decorView.addView(createStatusBarView(activity, color, statusBarAlpha));
}
// 设置fitsSystemWindows属性
setRootView(activity);复制代码

3.3 着色原理

在文章的前面部分,在系统4.4~5.0的机子上,如何设置状态栏颜色的原理其实已经作了说明。就是在透明的状态栏下放置一个和状态栏大小一致的view,经过更改view的颜色来实现更改状态栏颜色的效果。那么,当在5.0及以上的机子,当咱们经过以下代码设置状态栏颜色:

getWindow().setStatusBarColor(RED);复制代码

其实,这里调用的是PhoneWindow的setStatusBarColor方法,具体实现以下:

@Override
public void setStatusBarColor(int color) {
    mStatusBarColor = color;
    mForcedStatusBarColor = true;
    if (mDecor != null) {
        mDecor.updateColorViews(null, false /* animate */);
    }
}复制代码

最终调用的是DecorView的updateColorViews函数,DecorView是属于Activity的PhoneWindow的内部对象,也就说,更新的对象从所谓的Window进入到了Activity自身的布局视图中,接着看DecorView,这里只关注更改颜色。在方法updateColorViews中,调用了以下代码,代码实现的功能就是修改状态栏颜色:

updateColorViewInt(mNavigationColorViewState, sysUiVisibility,
    mWindow.mNavigationBarColor, navBarSize, navBarToRightEdge || navBarToLeftEdge,
    navBarToLeftEdge,
    0 /* sideInset */, animate && !disallowAnimate, false /* force */);复制代码

这里mStatusColorViewState其实就表明StatusBar的背景颜色对象,主要属性包括显示的条件以及颜色值:

private final ColorViewState mStatusColorViewState = new ColorViewState(
        SYSTEM_UI_FLAG_FULLSCREEN, FLAG_TRANSLUCENT_STATUS,
        Gravity.TOP,
        Gravity.LEFT,
        STATUS_BAR_BACKGROUND_TRANSITION_NAME,
        com.android.internal.R.id.statusBarBackground,
        FLAG_FULLSCREEN);复制代码

再来看updateColorViewInt方法:

private void updateColorViewInt(final ColorViewState state, int sysUiVis, int color,
        int size, boolean verticalBar, boolean seascape, int sideMargin,
        boolean animate, boolean force) {
    // 关键点1  
    state.present = (sysUiVis & state.systemUiHideFlag) == 0
            && (mWindow.getAttributes().flags & state.hideWindowFlag) == 0
            && ((mWindow.getAttributes().flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0
                    || force);
    // 关键点2
    boolean show = state.present
            && (color & Color.BLACK) != 0
            && ((mWindow.getAttributes().flags & state.translucentFlag) == 0  || force);
    boolean showView = show && !isResizing() && size > 0;

    ...

    if (view == null) {
        if (showView) {
                // 关键3 设置view的颜色
            state.view = view = new View(mContext);
            view.setBackgroundColor(color);
            view.setTransitionName(state.transitionName);
            view.setId(state.id);
            visibilityChanged = true;
            view.setVisibility(INVISIBLE);
            state.targetVisibility = VISIBLE;

            LayoutParams lp = new LayoutParams(resolvedWidth, resolvedHeight,
                    resolvedGravity);
            if (seascape) {
                lp.leftMargin = sideMargin;
            } else {
                lp.rightMargin = sideMargin;
            }
            addView(view, lp);
            updateColorViewTranslations();
        }
    } else {
        int vis = showView ? VISIBLE : INVISIBLE;
        visibilityChanged = state.targetVisibility != vis;
        state.targetVisibility = vis;
        LayoutParams lp = (LayoutParams) view.getLayoutParams();
        int rightMargin = seascape ? 0 : sideMargin;
        int leftMargin = seascape ? sideMargin : 0;
        if (lp.height != resolvedHeight || lp.width != resolvedWidth
                || lp.gravity != resolvedGravity || lp.rightMargin != rightMargin
                || lp.leftMargin != leftMargin) {
            lp.height = resolvedHeight;
            lp.width = resolvedWidth;
            lp.gravity = resolvedGravity;
            lp.rightMargin = rightMargin;
            lp.leftMargin = leftMargin;
            view.setLayoutParams(lp);
        }
        if (showView) {
            view.setBackgroundColor(color);
        }
    }
    ...
}复制代码

先看下关键点1,这里是根据SystemUI的配置决定是否显示状态栏背景颜色。若是状态栏都不显示,那就不必显示背景色了。再看关键点2,若是状态栏显示,但背景是透明色,也不必添加背景颜色,即不知足(color & Color.BLACK) != 0。而后,看一下translucentFlag,默认状况下,状态栏背景色与translucent半透明效果互斥,半透明就统一用半透明颜色,不会再添加额外颜色。最后,再来看关键点3,其实很简单,就是往DecorView上添加一个View,理论上DecorView也是一个FrameLayout,因此最终的实现就是在FrameLayout添加一个有背景色的View。

4、100分状态栏着色实践

目前网上封装好的状态栏设置工具备很多,用得比较多的是StatusBarUtil,还有已经废弃的SystemBarTint。可是有个问题,不管哪一个工具库,都不敢保证可以百分百实现状态栏着色的效果。所以,兼容性问题是一直存在的。经过实践分析总结,兼容性问题主要有两类:

  • 部分ROM(大多数是4.4~5.0的机子)自己就是不支持状态栏着色效果
  • 5.0以上的机子,不支持常规操做方式,须要采用特殊方式处理(4.4~5.0的处理方式)

当出现如上所述的问题时,咱们的处理方式通常是这样(不侵入修改第三方库):

  1. 实现一个兼容性工具类,获取本机信息,判断相关兼容性问题
  2. 在业务代码中判断兼容性条件,分状况判断是否设置状态栏颜色。支持状态栏设置的,还须要分状况判断使用哪一种方式设置。
if (RomUtil.isCompactSystemVersionForImmersion()) {

    if (RomUtil.isCompactRomForImmersion()) {
        StatusBarUtils.setColor(ActivityMain.this, Color.TRANSPARENT);
    } else {
        setTranslucentStatus(true);
        tintManager = new SystemBarTintManager(this);
        tintManager.setStatusBarTintEnabled(true);
        tintManager.setStatusBarTintColor(Color.TRANSPARENT);//通知栏所需颜色
    }

}复制代码

看上面实现,会发现代码逻辑很繁琐。并且当一个新的地方须要设置状态栏颜色时,相应的这一堆代码都须要再写一遍。显然,这种方式不是太友好,固然,咱们能够将条件判断都修改到库里面,直接修改库源码。可是这又带来另外一个问题,当库有更新了,升级库就很麻烦。

考虑到上述的一些问题,因此在咱们设计的库中,将兼容性判断的相关细节隐藏在库中,使用的时候只需调用相关方法便可。库对外提供了兼容性配置接口,用户能够自由配置,决定是否须要设置状态栏颜色。即实现对库无侵入的修改,即便升级了库,相应的兼容性配置信息也不会丢失。整个库设计的UML图以下所示:

库设计的UML图
库设计的UML图

要使用库也很是简单,只须要以下几步:

  • 在build.gradle中导入库
compile 'com.zr.statusbarmanager:library:1.0.0-beta'复制代码
  • 实现ICompatConfig接口,重写方法实现兼容性配置。接口主要提供了两个检查配置的方法,分别以下所示:
/**
 * 检测是否支持状态栏设置
 * @return
 */
boolean checkCompatiblity();

/**
 * 检测是否须要特殊设置的Rom
 * @return
 */
boolean checkSpecialRom();复制代码

上述的两个方法,checkCompatiblity用来判断是否支持设置状态栏颜色。若是返回false,则不会对状态栏设置颜色。checkSpecialRom在实际测试过程当中,有一款华为手机,搭载EMUI 3.1系统,对应Android系统版本5.1。咱们发现用常规直接设置状态栏颜色的方式设置是不生效的,可是用5.0如下系统设置状态栏颜色方式设置就能够生效。因此,该方法就是为了此类特殊ROM而定义的。

固然库也提供了一个默认的实现DefaultStatusBarCompatConfig,这也是咱们在项目开发过程当中根据兼容性测试发现的一些问题进行的配置,绝对颇有参考价值。若是用户本身实现配置,只须要以下设置:

public class StatusBarCompactConfig implements ICompatConfig {
    @Override
    public boolean checkCompatiblity() {
        if (CompatUtil.checkCompatiblity()) {
            // TODO: 17/7/28 自定义的兼容性判断逻辑
        } else {
            return false;
        }
    }

    @Override
    public boolean checkSpecialRom() {
        if (CompatUtil.checkSpecialRom()) {
            return true;
        } else {
            // TODO: 17/7/28 自定义特殊ROM判断逻辑
        }
    }
}复制代码

其中CompatUtil是库提供的兼容性判断工具类,若用户在使用库的时候须要带上库已经提供的相关兼容性信息,能够调用工具类的相关方法。若是不须要,也能够彻底本身实现。

  • 在Application中初始化库的设置
StatusBarManager.getsInstance().init(new DefaultStatusBarCompatConfig());复制代码
  • 在页面中调用库方法设置状态栏颜色
StatusBarManager.getsInstance().setColor(MainActivity.this, Color.TRANSPARENT);复制代码

5、那些坑

5.1 颜色设置不成功回滚

由于库不能保证设置状态栏颜色在全部机子上都生效,部分机子咱们能够在兼容性测试的时候作到兼容,可是不保证不会有漏网之鱼。这会致使一个问题,就是在这些不支持的手机上,没有达到预期效果,下降了用户体验。这个时候若是可以发现设置颜色失败,而后对这些不支持的机型可以作特殊处理,那就最好了。

因而想到的一种方案:

  1. 先设置颜色
  2. 而后在页面打开后,进行应用内截屏,获取当前DecorView的图像截屏(不须要root权限)
  3. 取截屏图片状态栏部分的颜色与设置的颜色比对,若是颜色一致代表设置成功,不然说明设置失败。若是设置失败的话,此时能够弹框提示用户进行操做,将设置失败的影响降到最低。

方案看着仍是可行的,那么开始验证。实际测试的时候,发现并非咱们想的那样的。咱们测试是正确实现了状态栏的设置,可是实际的视觉效果并无变成设置的颜色,仍是显示的系统的状态栏颜色。以下图所示,红框内是截屏图像,发现与实际屏幕显示的图像状态栏颜色并不一致。

猜想是有些ROM对状态栏进行了定制,在状态栏相同的位置覆盖了一个相同的view,且这个view的层级比Decoeview高,截图获取的是view下方的DecorView视图。(你们若是有什么好的判断的方式,期待交流~)

5.2 windowTranslucentStatus与statusBarColor不能同时生效

Android4.4的时候,加了个windowTranslucentStatus属性,实现了状态栏导航栏半透明效果,而Android5.0以后以上状态栏、导航栏支持颜色随意设定,因此,5.0以后通常不使用须要使用该属性,并且设置状态栏颜色与windowTranslucentStatus是互斥的。因此,默认状况下android:windowTranslucentStatus是false。也就是说:‘windowTranslucentStatus’和‘windowTranslucentNavigation’设置为true后就再设置‘statusBarColor’和‘navigationBarColor’就没有效果了

boolean show = state.present
                && (color & Color.BLACK) != 0
                && ((mWindow.getAttributes().flags & state.translucentFlag) == 0  || force);复制代码

能够看到,添加背景View有一个必要条件

(mWindow.getAttributes().flags & state.translucentFlag) == 0复制代码

也就是说一旦设置了

<item name="android:windowTranslucentStatus">true</item>复制代码

相应的状态栏或者导航栏的颜色设置就不在生效。不过它并不影响fitSystemWindow的逻辑。

5.3 设置SystemUiVisibility属性

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
    activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
    activity.getWindow().setStatusBarColor(calculateStatusColor(color, statusBarAlpha));
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
    ...
}复制代码

以上代码设置状态栏颜色,当设置状态栏透明时,咱们发现一个现象。在5.0以前的机子上,内容能够延伸到状态栏下面;而在5.0以上的机子上,顶部会有一块空出的view,如图所示。

4.4和5.0效果图展现
4.4和5.0效果图展现

为何会致使这个现象呢?这里要从WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS这个属性提及。在5.0以前系统上,该属性设置为true,5.0及以后系统,属性设置为false。查看WindowManager中该属性的注释,发现以下一段话:

<p>When this flag is enabled for a window, it automatically sets
 * the system UI visibility flags {@link View#SYSTEM_UI_FLAG_LAYOUT_STABLE} and
 * {@link View#SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN}.</p>复制代码

原来设置FLAG_TRANSLUCENT_STATUS为true以后,会自动设置SYSTEM_UI_FLAG_LAYOUT_STABLESYSTEM_UI_FLAG_LAYOUT_FULLSCREEN的系统UI属性,经过前面沉浸设置,能够知道这就是实现内容显示到状态栏下的设置。由于5.0以上系统FLAG_TRANSLUCENT_STATUS为false,固然内容也将不会显示到导航栏下。因此,在5.0的机子上,须要加上此段设置代码:

activity.getWindow().getDecorView().set(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);复制代码

6、结束

在Android项目开发过程当中,免不了要和系统栏打交道。以上是做者根据平时项目开发经验、并结合网上查阅的资料对状态栏相关设置进行的总结。但愿对你们有帮助,欢迎你们交流讨论~

项目地址:github.com/yushiwo/Sta…

参考文章

  1. Android App 沉浸式状态栏解决方案
  2. StatusBarUtil 状态栏工具类(实现沉浸式状态栏/变色状态栏)
  3. 使人困惑的fitsSystemWindows属性
  4. what-are-windowinsets
  5. Android沉浸式状态栏实现
  6. 沉浸式管理:让你的APP更优雅
  7. 使用setSystemUiVisibility适配statusbar和navigationbar
  8. StatusBarAdapt
  9. 全屏、沉浸式、fitSystemWindow使用及原理分析:全方位控制“沉浸式”的实现
  10. Android SystemBar
  11. 探索Android半透明状态栏
  12. 沉浸式状态栏
  13. 由沉浸式状态栏引起的血案
  14. Android开发:Translucent System Bar 的最佳实践
  15. 全屏、沉浸式、fitSystemWindow使用及原理分析:全方位控制“沉浸式”的实现
相关文章
相关标签/搜索