android透明状态栏——你要的只是几个方法

以前写的项目基本没有考虑过Android透明状态栏的,最近项目有用到,便网上搜索一番。搜到的文章要么是讲解不全、存在错误,要么就是使用一些库,也不说原理,上来就是给你看效果图,然后十几个方法让你眼花缭乱,看源码就是越看越复杂。算了,自己写个简单能用的就好。所以,本文将通俗易懂地介绍Android透明状态栏原理,以及几种常见使用场景的处理方法。

下面是实现的效果:


透明状态栏.gif

以上效果支持状态栏和标题栏(ToolBar或者自定义)背景一致且可修改(改变颜色、渐变色、不透明度看你自己怎么改),标题栏位置不被状态栏遮挡,6.0之上浅色状态栏背景修改字体颜色为深色。支持DrawerLayout使用、Fragment使用。虽然效果很多,其实原理都是一样的。

本文会教你如何修改状态栏的颜色、如何解决状态栏和标题栏内容重合的问题

顺便说下,这里的透明状态栏指的是布局内容延伸到状态栏的效果,网上很多人也叫做沉浸式状态栏,这里不做辩驳。

原理

透明状态栏的设置存在兼容性。Android 4.4(API 19)之前不支持透明状态栏,而Android 5.0(API 21)之后版本对于透明状态栏则有不同的处理方式。所以,我们要兼容的就是保证4.4之前的版本虽然不支持透明状态栏,但是UI效果还是要协调,而4.4之后的版本跟根据不同的API版本尽量达成一致的显示效果。

设置透明状态栏,最核心的方法就是下面这一个方法:

 public static void makeStatusBarTransparent(Activity activity) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
            return;
        }
        Window window = activity.getWindow();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
            window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
            int option = window.getDecorView().getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
            window.getDecorView().setSystemUiVisibility(option);
            window.setStatusBarColor(Color.TRANSPARENT);
        } else {
            window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
        }
    }

你应该在网上找到了很多版本,类似的代码。归根到底,就这一个方法对Activity进行设置即可实现透明状态栏。这个方法调用后,页面布局内容就会延伸至状态栏,状态栏即为透明色。


image.png

但是,这里要注意下,其带来了一个问题是页面布局会延伸至状态栏,你布局中的标题栏和状态栏重合了,这显示不是我们想要的效果。我们想要的是原有标题栏还是在原来标题栏的位置,只不过状态栏的颜色和标题栏一致。接下来,我们将要解决的就是标题栏和状态栏重合的问题。这个问题,也是你看到的所有第三方库想要帮你解决的问题。第三方库为了满足广大开发者的需求,也是为了适配各种情况,通常会增加N多种接口或者方式来处理这个问题,但随之而来带来的就是复杂性,不易理解。接口方法太多了,通常都要组合搭配调用才能生效,不看源码,光看接口估计会很懵逼。

好勒,接下来,我们就介绍几种方式来解决标题栏和状态栏重合问题。

标题栏和状态栏重合问题

解决状态栏和标题栏重叠的几种方法原理都是一致的:让布局中的标题栏有一个状态栏高度的marginTop或者paddingTop即可。但因为4.4之前是不存在状态栏重合这种问题,所以也就不需要有一个状态栏高度的marginTop或者paddingTop,所以要考虑一个兼容性问题。

几种解决办法:

  • 使用系统提供的fitSystemWindows属性
  • 给标题控件动态增加paddingTop
  • 在DecorView中动态创建一个状态栏高度的View,然后更改View背景为想要的颜色,然后设置标题栏marginTop为状态栏高度
  • 在布局中定义一个假的用来充当状态栏的View,动态设置其高度或者可见性

不同方式都有各自的优劣,建议根据不同的场景来灵活应用。

方式一:系统提供的fitSystemWindows属性

fitsSystemWindows属性:

  • 在透明状态栏或者全屏时候生效。(4.4之前添加了这个属性也没有任何效果的,满足兼容性)
  • 如果将某个View的该属性设置为true,则系统会为该View添加一个paddingTop,值为状态栏的高度。
  • 如果多个View同时设置该属性为true,则只有第一个会起作用。此为一般情况。
  • 默认系统只处理第一个fitsSystemWindows,但每个控件自己可以单独处理fitsSystemWindows。

fitsSystemWindows属性为true的时候,在透明状态栏模式下,4.4(含)之后的版本控件,系统会针对扫描到的该属性设置为true的第一个控件设置一个状态栏高度的paddingTop(原有View的paddingTop将会失效)。

注意:上面这段话是我的个人理解,并不严谨。

知道上面的原理后,我们可以根据需要来为哪个控件设置fitsSystemWindows属性为true。

因为是设定paddingTop,如果你的标题栏是一个固定高度比如50dp,那么设置后是不会有效的,毕竟高度写死了的。所以呢通常设置这个属性的View高度是一个wrap_content,如果不是的话呢,那么你就在外面包一个FrameLayout或者LinearLayout,然后将原来View的背景设置到外面的包装Layout上。

比如下面的在原来标题栏RelativeLayout布局外面包装一个LinearLayout,然后给LinearLayout添加一个fitsSystemWindows属性,并且修改LinearLayout的背景为标题栏背景色。


image.png

对了,我试过fitsSystemWindows如果在Fragment中使用貌似是不生效的。

方式二:给标题栏控件动态增加paddingTop

4.4之前的话就不加了。可以用代码来动态增加,或者适用的话也可以放到values文件中。代码获取状态栏高度:

 public static int getStatusBarHeight() {
        Resources resources = Resources.getSystem();
        int resourceId = resources.getIdentifier("status_bar_height", "dimen", "android");
        return resources.getDimensionPixelSize(resourceId);
    }

给标题栏控件动态增加高度的方法:

public static void fitTitleBar(Activity activity, final View titleBar) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
            return;
        }
        makeStatusBarTransparent(activity);
        final ViewGroup.LayoutParams layoutParams = titleBar.getLayoutParams();
        assert layoutParams != null;
        if (layoutParams.height == ViewGroup.LayoutParams.WRAP_CONTENT ||
                layoutParams.height == ViewGroup.LayoutParams.MATCH_PARENT) {
            titleBar.post(new Runnable() {
                @Override
                public void run() {
                    layoutParams.height = titleBar.getHeight() + getStatusBarHeight();
                    titleBar.setPadding(titleBar.getPaddingLeft(),
                            titleBar.getPaddingTop() + getStatusBarHeight(),
                            titleBar.getPaddingRight(),
                            titleBar.getPaddingBottom());
                }
            });
        } else {
            layoutParams.height += getStatusBarHeight();
            titleBar.setPadding(titleBar.getPaddingLeft(),
                    titleBar.getPaddingTop() + getStatusBarHeight(),
                    titleBar.getPaddingRight(),
                    titleBar.getPaddingBottom());
        }
    }

如果不是固定高度的控件,那么直接增加一个状态栏高度的paddingTop,如果是写死高度的控件,则需要同时给高度增加一个状态栏高度。

在应用此方法的时候要注意,这个titleBar控件增加了paddingTop或者高度后,其内部布局要非常协调,不要影响到美观了。如果你的标题栏用RelativeLayout写的,然后里面文本居中那么使用此方法后显示效果会很丑陋,可以参考第一种方式在外面包装一层。

除了代码控制,还可以根据API版本不同配置不同像素高度的status_bar_offset,然后设置到布局中。通常,状态栏高度是24或者25dp。在4.4(API 19)之前不需要处理重叠问题所以设置status_bar_offset为0dp,在values-v19中设置status_bar_offset为25dp,然后根据布局的不同动态增加padding。

比如:
values/dimens.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <dimen name="status_bar_offset">0dp</dimen>
</resources>

values-v19/dimens.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <dimen name="status_bar_offset">25dp</dimen>
</resources>

布局文件

<LinearLayout android:id="@+id/top_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/holo_green_dark"
        android:paddingTop="@dimen/status_bar_offset"
        android:orientation="vertical">
        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="50dp">
            <TextView android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Hello"
                android:textColor="@color/white"
                android:layout_centerInParent="true"/>
        </RelativeLayout>
    </LinearLayout>

除了上面这种写法,还有很多种写法,发挥你的想象力!

根据版本不同处理不同的padding这种,个人还是比较推崇的,不用写太多代码,直接布局就写好了。当然可能是针对一些比较简单的页面处理哈。

方式三:动态创建状态栏高度的View添加到DecorView中,然后设置标题栏的marginTop为状态栏高度

很多第三方库都喜欢这种方式。个人不喜欢用,个人钟爱第二种和第一种哈哈。
这种方式也有局限性,比如对于DrawerLayout的情况,如果采用这种方式将statusBar添加到DecorView上,会导致侧边栏也会被状态栏所覆盖。

public static View setStatusBarColor(Activity activity, int color) {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
          return null;
    }
    makeStatusBarTransparent(activity);
    return applyStatusBarColor(activity, color);
}

private static View applyStatusBarColor(Activity activity, int color) {
    View fakeStatusBar = ensureStatusBar(activity);
    fakeStatusBar.setBackgroundColor(color);
    return fakeStatusBar;
}

private static View ensureStatusBar(Activity activity) {
    ViewGroup parent = (ViewGroup) activity.getWindow().getDecorView();
    View fakeStatusBar = parent.findViewWithTag(TAG_FAKE_STATUS_BAR);
    if (fakeStatusBar == null) {
        fakeStatusBar = createStatusBar(activity);
        fakeStatusBar.setTag(TAG_FAKE_STATUS_BAR);
        parent.addView(fakeStatusBar);
    }
    return fakeStatusBar;
}

private static View createStatusBar(Activity activity) {
    View statusBarView = new View(activity);
    ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, getStatusBarHeight());
    statusBarView.setLayoutParams(layoutParams);
    return statusBarView;
}

public static void addMarginTopEqualStatusBarHeight(View view) {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
          return;
    }
    view.setTag(TAG_OFFSET);
    Object haveSetOffset = view.getTag(KEY_OFFSET);
    if (haveSetOffset instanceof Boolean) {
        return;
    }
    ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
    layoutParams.setMargins(layoutParams.leftMargin,
              layoutParams.topMargin + getStatusBarHeight(),
              layoutParams.rightMargin,
              layoutParams.bottomMargin);
    view.setTag(KEY_OFFSET, true);
}


  public static void subtractMarginTopEqualStatusBarHeight(View view) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return;
        Object haveSetOffset = view.getTag(KEY_OFFSET);
        if (haveSetOffset == null || !(Boolean) haveSetOffset) {
            return;
        }
        ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
        layoutParams.setMargins(layoutParams.leftMargin,
                layoutParams.topMargin - getStatusBarHeight(),
                layoutParams.rightMargin,
                layoutParams.bottomMargin);
        view.setTag(KEY_OFFSET, false);
    }

方式四 在布局中定义假的充当状态栏的View,然后动态设置其高度。

这种方式是为了解决方式三不适用于DrawerLayout的情况,因为DecorView中添加的View是盖在整个布局上的,所以会遮挡侧边栏。那么将其移动到布局中自己可控就好勒。

具体代码不提供了,有思路就好。

状态栏背景颜色为浅色导致状态字体看不清的问题解决

状态栏颜色默认为白色,如果你的状态栏背景为浅色,那么会导致字体看不清楚。Android 6.0之后提供了方法设置为深色字体。

下面设置为light模式,则是让状态栏字体颜色变深,反之变浅。

 public static void setStatusBarLightMode(Activity activity, boolean isLightMode) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            Window window = activity.getWindow();
            int option = window.getDecorView().getSystemUiVisibility();
            if (isLightMode) {
                option |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
            } else {
                option &= ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
            }
            window.getDecorView().setSystemUiVisibility(option);
        }
    }

综述

综上,提供参考代码地址:https://github.com/luckyshane/TintBar

推荐阅读更多精彩内容