×

Android沉浸式(透明)状态栏适配

96
xiaoyanger
2017.03.07 23:18* 字数 1158

在Android系统4.4以前,状态栏的背景色和字体颜色都是不能改变的。但是4.4以后Google增加了改变状态栏背景透明的方法,可以通过两种方式来设置。
直接在Activity中设置Window属性:

@Override
protected void onCreate(Bundle savedInstanceState) {
   if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
    }
    super.onCreate(savedInstanceState);
}

在xml的style文件中设置:

<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="android:windowTranslucentStatus">true</item>
</style>

使用android:windowTranslucentStatus属性需要在res目录下新建values-v19文件夹,style文件要放在里面。
尽量使用第一种方式类实现,据说是第二种方式在某些国产手机上没有效果。

两种方式都是让状态背景栏透明。但是在实际测试后发现,在android4.4系统上状态栏的背景是渐变半透明。而在android5.0+的系统上又有所不同又有差异:

在Genymotion模拟器、vivo、nexus6p的android5.0+系统上是半透明。
在xiaomi、oppo、huawei、leshi等5.0+系统是全透明的。

android4.4

模拟器android5.0+

小米5android6.0

从效果图也可以看出,上面两种方式使得内容区域延伸到状态栏下面去了。我们可以添加android:fitsSystemWindows来避免这样的情况。
在Activity的根布局文件中添加

android:fitsSystemWindows="true"

或者在主题style文件中添加

<item name="android:fitsSystemWindows">true</item>

android4.4

模拟器android5.0+

小米5android6.0

上图为了方便观察状态栏的背景颜色,就将Activity的布局文件的背景色设置成了红色。可以看出Toolbar所在的内容区域没有沉浸到状态栏下面了,并且状态栏的背景颜色还是之前一样,只是将内容区域向下偏移了状态栏高度的距离。这是因为fitsSystemWindows属性使得布局的paddingTop被重新改写了(paddingTop增加了状态栏的高度)。
要实现沉浸式的状态栏,其实就是状态栏的背景颜色和Toolbar的颜色一样。那么我们将Activity的背景色改为Toolbar紫色(内容区域的背景颜色设置为白色)看下效果:
android4.4

模拟器android5.0+

小米5android6.0

除了通过fitsSystemWindows这个属性,我们可以自己给在Toolbar的上方添加一个和状态栏高度一样的Veiw,并将这个View的背景色设置成和Toolbar的背景色一样。也可以直接给Toobar设置paddingTop等于状态栏高度值。
为了方便,我一般是采用重写Toolbar,改写它的paddingTop值来实现。

/**
 * Created by xiaoyanger on 2017/3/1.
 * 沉浸式、版本兼容的Toolbar,状态栏透明.
 */
public class CompatToolbar extends Toolbar {

    public CompatToolbar(Context context) {
        this(context, null);
    }

    public CompatToolbar(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CompatToolbar(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setup();
    }

    public void setup() {
        int compatPadingTop = 0;
        // android 4.4以上将Toolbar添加状态栏高度的上边距,沉浸到状态栏下方
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            compatPadingTop = getStatusBarHeight();
        }
        this.setPadding(getPaddingLeft(), getPaddingTop() + compatPadingTop, getPaddingRight(), getPaddingBottom());
    }
    
    public int getStatusBarHeight() {
        int statusBarHeight = 0;
        int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
        if (resourceId > 0) {
            statusBarHeight = getResources().getDimensionPixelSize(resourceId);
        }
        Log.d("CompatToolbar", "状态栏高度:" + px2dp(statusBarHeight) + "dp");
        return statusBarHeight;
    }

    public float px2dp(float pxVal) {
        final float scale = getContext().getResources().getDisplayMetrics().density;
        return (pxVal / scale);
    }
}

需要注意的是,网上有很多文章说状态栏的高度是25dp,但实际测试后发现并不是所有的机型都是25dp。使用getStatusBarHeight()可以准确的获取状态栏的高度值,看下获取到高度的日志:

android4.4模拟器

android6.0模拟器

oppo r9

小米5

各个机型获取到的状态栏高度很多都不一样,所以最好还是重写Toolbar,动态获取到系统状态栏的高度来改写它的上边距。

国产的大部分手机在android5.0+的系统都将原生的半透明状态栏改成了全透明,因此通过上面的方式基本达到了沉浸式体验。
其实在原生android5.0+系统上可以通过这个方法使状态栏背景色完全透明:

@Override
protected void onCreate(Bundle savedInstanceState) {
    // 5.0以上系统状态栏透明
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        Window window = getWindow();
        window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
        window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
        window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
        window.setStatusBarColor(Color.TRANSPARENT);
    }
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
}

模拟器(原生系统)android5.0+

可以将上面两个方法封装到BaseActivity中,以便在各个界面直接调用:

protected void setTranslucentStatus() {
    // 5.0以上系统状态栏透明
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        Window window = getWindow();
        window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
        window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
        window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
        window.setStatusBarColor(Color.TRANSPARENT);
    } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
    }
}

通过setTranslucentStatus()方法,4.4的系统状态栏背景色渐变半透明,绝大多数5.0+的系统状态栏背景色全透明(测试时发现有部分华为手机状态栏背景是浅紫色,估计是定制系统给修改了)。
需要注意的是,不管是<item name="android:windowTranslucentStatus">true</item>还是setTranslucentStatus()方法,这两种方式都会使这个设置<itemname="colorPrimaryDark">@color/colorPrimaryDark</item>不会有任何效果。


状态栏的背景色按照上面的方式适配基本上满足沉浸式体验,但是有时候会遇到这样的需求,状态栏和Toolbar背景色都是白底黑字,比如简书消息界面:


简书消息界面Toobar和状态栏白底黑字

状态栏的白色背景可以通过上面的方式来实现,但是修改状态栏的文字和图标颜色就比较麻烦了,原生的android系统只有在6.0+才有官方的api,但是在MIUI和FlymeUI提供了相应的方法。

/**
 * 设置Android状态栏的字体颜色,状态栏为亮色的时候字体和图标是黑色,状态栏为暗色的时候字体和图标为白色
 *
 * @param dark 状态栏字体和图标是否为深色
 */
protected void setStatusBarFontDark(boolean dark) {
    // 小米MIUI
    try {
        Window window = getWindow();
        Class clazz = getWindow().getClass();
        Class layoutParams = Class.forName("android.view.MiuiWindowManager$LayoutParams");
        Field field = layoutParams.getField("EXTRA_FLAG_STATUS_BAR_DARK_MODE");
        int darkModeFlag = field.getInt(layoutParams);
        Method extraFlagField = clazz.getMethod("setExtraFlags", int.class, int.class);
        if (dark) {    //状态栏亮色且黑色字体
            extraFlagField.invoke(window, darkModeFlag, darkModeFlag);
        } else {       //清除黑色字体
            extraFlagField.invoke(window, 0, darkModeFlag);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }

    // 魅族FlymeUI
    try {
        Window window = getWindow();
        WindowManager.LayoutParams lp = window.getAttributes();
        Field darkFlag = WindowManager.LayoutParams.class.getDeclaredField("MEIZU_FLAG_DARK_STATUS_BAR_ICON");
        Field meizuFlags = WindowManager.LayoutParams.class.getDeclaredField("meizuFlags");
        darkFlag.setAccessible(true);
        meizuFlags.setAccessible(true);
        int bit = darkFlag.getInt(null);
        int value = meizuFlags.getInt(lp);
        if (dark) {
            value |= bit;
        } else {
            value &= ~bit;
        }
        meizuFlags.setInt(lp, value);
        window.setAttributes(lp);
    } catch (Exception e) {
        e.printStackTrace();
    }
    // android6.0+系统
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        if (dark) {
            getWindow().getDecorView().setSystemUiVisibility(
                    View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                            | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
        }
    }
}

状态栏和Toobar白底黑字只能适配到android6.0+以及小米和魅族手机,其他手机的只能调整UI设计了。可以看一下简书和掘金,在小米、魅族和android6.0+的系统上都是白底黑字,但是在其他的系统上一般是将状态栏的背景色设置为浅灰色。

简书消息界面oppo r9(5.1系统)

其实状态栏显示的颜色是灰色,但是有多种方式来实现,参考下简书、掘金、今日头条,他们都使用了右滑返回上一界面,在滑动过程中就能看出状态背景色的实现。
简书

掘金

今日头条

简书的效果可以不用上面的方式使内容区域沉浸到状态栏下面,直接使用的<item name="colorPrimaryDark">@color/colorPrimaryDark</item>直接修改的状态栏的颜色。

掘金的状态栏是半透明的,内容区域已经沉浸到状态栏下方,可以使用上面的方法,对于不是android6.0+、MIUI、FlymeUI的系统通过window.setStatusbarColor方法将状态栏颜色改为半透明的灰色。

今日头条的效果可以采用上面的方法实现沉浸式,状态栏全透明,Toolbar没有增加paddingTop,而是在第二个界面中状态栏的下方(Toolbar上方)加了一块同状态栏高度一样的View,颜色为灰色。

其实,从上面三个app的状态栏适配效果来看,要达到真正的沉浸式,今日头条的效果要好一些。但是今日头条并没有在各个机型上适配状态栏的字体和图标颜色。
下面自己来实现不同机型的状态栏背景色和字体图标颜色的适配。
首先,新建一个CompatStartusbarActivity,继承自BaseActivity

public class CompatStatusBarActivity extends BaseActivity {

    private FrameLayout mFrameLayoutContent;
    private View mViewStatusBarPlace;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        super.setContentView(R.layout.activity_compat_status_bar);

        mViewStatusBarPlace = findViewById(R.id.view_status_bar_place);
        mFrameLayoutContent = (FrameLayout) findViewById(R.id.frame_layout_content_place);

        ViewGroup.LayoutParams params = mViewStatusBarPlace.getLayoutParams();
        params.height = getStatusBarHeight();
        mViewStatusBarPlace.setLayoutParams(params);
    }

    @Override
    public void setContentView(@LayoutRes int layoutResID) {
        View contentView = LayoutInflater.from(this).inflate(layoutResID, null);
        mFrameLayoutContent.addView(contentView);
    }

    /**
     * 设置沉浸式状态栏
     *
     * @param fontIconDark 状态栏字体和图标颜色是否为深色
     */
    protected void setImmersiveStatusBar(boolean fontIconDark, int statusBarColor) {
        setTranslucentStatus();
        if (fontIconDark) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
                    || OsUtil.isMIUI()
                    || OsUtil.isFlyme()) {
                setStatusBarFontIconDark(true);
            } else {
                if (statusBarColor == Color.WHITE) {
                    statusBarColor = 0xffcccccc;
                }
            }
        }
        setStatusBarPlaceColor(statusBarColor);
    }

    private void setStatusBarPlaceColor(int statusColor) {
        if (mViewStatusBarPlace != null) {
            mViewStatusBarPlace.setBackgroundColor(statusColor);
        }
    }


    public int getStatusBarHeight() {
        int statusBarHeight = 0;
        int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
        if (resourceId > 0) {
            statusBarHeight = getResources().getDimensionPixelSize(resourceId);
        }
        return statusBarHeight;
    }

    /**
     * 设置状态栏透明
     */
    private void setTranslucentStatus() {
        // 5.0以上系统状态栏透明
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            Window window = getWindow();
            window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
            window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                    | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
            window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
            window.setStatusBarColor(Color.TRANSPARENT);
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
        }
    }

    /**
     * 设置Android状态栏的字体颜色,状态栏为亮色的时候字体和图标是黑色,状态栏为暗色的时候字体和图标为白色
     *
     * @param dark 状态栏字体是否为深色
     */
    private void setStatusBarFontIconDark(boolean dark) {
        // 小米MIUI
        try {
            Window window = getWindow();
            Class clazz = getWindow().getClass();
            Class layoutParams = Class.forName("android.view.MiuiWindowManager$LayoutParams");
            Field field = layoutParams.getField("EXTRA_FLAG_STATUS_BAR_DARK_MODE");
            int darkModeFlag = field.getInt(layoutParams);
            Method extraFlagField = clazz.getMethod("setExtraFlags", int.class, int.class);
            if (dark) {    //状态栏亮色且黑色字体
                extraFlagField.invoke(window, darkModeFlag, darkModeFlag);
            } else {       //清除黑色字体
                extraFlagField.invoke(window, 0, darkModeFlag);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        // 魅族FlymeUI
        try {
            Window window = getWindow();
            WindowManager.LayoutParams lp = window.getAttributes();
            Field darkFlag = WindowManager.LayoutParams.class.getDeclaredField("MEIZU_FLAG_DARK_STATUS_BAR_ICON");
            Field meizuFlags = WindowManager.LayoutParams.class.getDeclaredField("meizuFlags");
            darkFlag.setAccessible(true);
            meizuFlags.setAccessible(true);
            int bit = darkFlag.getInt(null);
            int value = meizuFlags.getInt(lp);
            if (dark) {
                value |= bit;
            } else {
                value &= ~bit;
            }
            meizuFlags.setInt(lp, value);
            window.setAttributes(lp);
        } catch (Exception e) {
            e.printStackTrace();
        }
        // android6.0+系统
        // 这个设置和在xml的style文件中用这个<item name="android:windowLightStatusBar">true</item>属性是一样的
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if (dark) {
                getWindow().getDecorView().setSystemUiVisibility(
                        View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                                | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
            }
        }
    }
}

布局文件activity_compat_status_bar.xml

<?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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.xiao.compatstatusbar.MainActivity">

    <View
        android:id="@+id/view_status_bar_place"
        android:layout_width="match_parent"
        android:layout_height="25dp"/>

    <FrameLayout
        android:id="@+id/frame_layout_content_place"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</LinearLayout>

setImmersiveStatusBar方法中中判断了不能修改状态栏字体和图标颜色则将白色的状态栏底色改成灰色,能修改状态栏字体和图标的颜色则改为给定的颜色statusBarColor。使用的时候直接调用这个方法即可。


下面是使用示例:
MainActivity继承自CompatStatusBarActivity

public class MainActivity extends CompatStatusBarActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar_main);
        int color = 0xffaa66cc;
        toolbar.setBackgroundColor(color);
        setImmersiveStatusBar(false, color);
    }

    public void go(View view) {
        startActivity(new Intent(this, NextActivity.class));
    }
}

布局文件activity_main.xml

<?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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.xiao.compatstatusbar.MainActivity">

    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar_main"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="@color/colorPrimary">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <TextView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:gravity="center"
                android:text="标题"
                android:textColor="@android:color/white"
                android:textSize="18sp"/>
        </LinearLayout>
    </android.support.v7.widget.Toolbar>

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#99FF4081"
        android:onClick="go"
        android:text="页面1"
        android:textSize="44sp"/>
</LinearLayout>

NextActivity同样继承自CompatStatusBarActivity:

public class NextActivity extends CompatStatusBarActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_next);

        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar_next);
        int color = 0xffffffff;
        toolbar.setBackgroundColor(color);
        setImmersiveStatusBar(true, color);
    }
}

布局文件activity_next.xml

<?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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.xiao.compatstatusbar.MainActivity">

    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar_next"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="@color/colorPrimary">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <TextView
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:gravity="center"
                android:text="标题"
                android:textColor="#353535"
                android:textSize="18sp"/>
        </LinearLayout>
    </android.support.v7.widget.Toolbar>

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#99FF4081"
        android:text="页面2"
        android:textSize="44sp"/>
</LinearLayout>

最终适配效果:

模拟器android4.4

模拟器android5.0

模拟器android6.0

oppo r9

mi5

源码:https://github.com/xiaoyanger0825/CompatStatusBar

Android
Web note ad 1