适配安卓沉浸式状态栏的新姿势

Github Demo: https://github.com/lliuguangbo/AutoSystemBar

针对状态栏,官方从4.4版本开始支持,但是4.4和5.0以上API是不同的,6.0以上提供了两种状态栏图标样式
分别是白色和黑色样式。

针对状态栏图标样式的修改,小米和魅族提供额外的API,在6.0以下都支持,可以参考它们的文档:

Android4.4状态栏的API:
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);

设置该属性后,Activity的Layout会嵌入到状态栏下,
也就是setContentView(View)的set进去的view高度比之前高出了状态栏的高度。

Android5.0以上状态栏的API:
View decorView = window.getDecorView();
decorView.setSystemUiVisibility(decorView.getSystemUiVisibility()
        | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
        | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);

window.setStatusBarColor(Color.TRANSPARENT);

5.0 以上 FLAG_TRANSLUCENT_STATUS 与 FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS是有冲突的,需要调用clearFlags清楚这个flag否则会没有效果。 通过调用setStatusBarColor()来改变状态栏颜色。
但是当设置的颜色不是全透明时,Activity的setContentView(View)的set进去的view高度是没有发生变化的,如果设置的颜色是去透明时,setContentView(View)的set进去的view高度同样比之前高出了状态栏的高度。

官方Android6.0修改状态栏图标样式API
View decorView = window.getDecorView();
if(darkFont){
    //黑色样式
    decorView.setSystemUiVisibility(decorView.getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
}else {
    //白色样式
    decorView.setSystemUiVisibility(decorView.getSystemUiVisibility() & ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
}
适配状态栏的方案思考
  • 现在越来越多的应用开始适配状态栏了,不再是黑黑的状态栏;
  • 越来越多的应用会使用透明状态栏,把图片或者视屏嵌入到状态栏下充分利用空间,通常这个界面是可以滚动的,当滚动一定的位置时改变状态栏颜色。
  • 状态栏图标样式的适配

开发者通常的适配方法是:

  • 修改状态栏颜色,4.4版本需要额外添加一个状态栏高度的View来填充,通过改变这个View的背景来实现;5.0以上有两种方法,一种是调用setStatusBarColor(), 另一种和4.4的方案一致。
  • 这种情况适配起来还是挺麻烦的,透明状态栏时,activity的contentView的高度高出了状态栏的高度会引起一些布局上的问题,例如图片嵌入状态栏下,当滚动时ActionBar会重新出现,会导致ActionBar也嵌入状态栏下,很难看。适配时还要对不同的版本进行布局的调整(因为4.4以下没问题)
  • 对于状态栏图标样式的适配,如果你的ActionBar背景是白色的,状态栏改为白色,那么状态栏图标样式就需要改成黑色,如果还是白色会导致看不清状态栏图标

总的来说,对于第二种情况时适配还是挺麻烦的。

  • 采用额外添加一个状态栏高度的View来填充的方法可以统一4.4和5.0版本;
  • 获取ActionBar下最多的颜色,使用该颜色当做状态栏颜色,这时想到了官方提供com.android.support:palette-v7:26.1.0
  • 状态栏图标的样式根据状态栏颜色做出调整。
代码的实现

先撸个自定义ViewGroup,继承RelativeLayout。
下面是这个自定义ViewGroup需要加载的布局文件,分三部分:状态栏View,底部导航栏View, 中间内容部分

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <FrameLayout
        android:id="@+id/content"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@+id/status_view"
        android:layout_above="@+id/navigation_view"
        >
    </FrameLayout>

    <cn.albert.autosystembar.SystemBarView
        android:id="@+id/status_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:visibility="gone"
        android:layout_alignParentTop="true"
        />

    <cn.albert.autosystembar.SystemBarView
        android:id="@+id/navigation_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:visibility="gone"
        android:layout_alignParentBottom="true"
        />

</merge>

下面是自定义ViewGroup关键代码,命名为InternalLayout,在构造器里对状态栏View,底部导航栏View初始化高度。

class InternalLayout extends RelativeLayout{
    // 部分关键代码
    public InternalLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        Utils.initializeStatusBar(this);
        Utils.initializeNavigationBar(this);

        ViewCompat.setFitsSystemWindows(this, true);
        ViewCompat.requestApplyInsets(this);
        ViewCompat.setOnApplyWindowInsetsListener(this, new android.support.v4.view.OnApplyWindowInsetsListener() {
            @Override
            public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) {
                return insets.consumeSystemWindowInsets();
            }
        });
        inflate(context, R.layout.layout_content, this);
        mStatusView = findViewById(R.id.status_view);
        mStatusView.getLayoutParams().height = Utils.sStatusBarHeight;
        mNavigationView = findViewById(R.id.navigation_view);
        mNavigationView.getLayoutParams().height = Utils.sNavigationBarHeight;

        mContentLayout = (ViewGroup) findViewById(R.id.content);

    }
    
    
    void setContentView(View content) {
        if(content.getParent() == null){
            mContentLayout.addView(content);
        }
    }
}

下面是SystemBarView的代码, android4.4以下SystemBarView高度,宽带都为0。

class SystemBarView extends View{

    public SystemBarView(Context context) {
        super(context);
    }

    public SystemBarView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }else {
            setMeasuredDimension(0, 0);
            setVisibility(GONE);
        }
    }
}

InternalLayout内部有一个FrameLayout,用来装Activity的setContentView()里View,代码如下:

final View decorView = window.getDecorView();
final View androidContent = window.getDecorView().findViewById(Window.ID_ANDROID_CONTENT);
final ViewGroup realContent = ((ViewGroup) androidContent);
View content = realContent.getChildAt(0);
//content是Activity的setContentView()里的View
realContent.removeView(content);
InternalLayout layout = new InternalLayout(activity);
layout.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
layout.setContentView(content);
realContent.addView(layout);

接着实现在ActionBar里获取主颜色作为状态栏的颜色:

//decorView是Activity的DecorView, 获取decorView的bitmap
decorView.setDrawingCacheEnabled(true);
final Bitmap bitmap = Bitmap.createBitmap(decorView.getDrawingCache());
decorView.setDrawingCacheEnabled(false);


int top = Utils.sStatusBarHeight + PADDING;
int bottom = (int) (top + ACTION_BAR_DEFAULT_HEIGHT * decorView.getResources().getDisplayMetrics().density);
//PADDING = 10, ACTION_BAR_DEFAULT_HEIGHT = 48, rect 记录了一个矩形,在状态栏下面的一个矩形,10, 48 我随便定义的距离,这个矩形不用太精准,不用就是ActionBar的位置。
Rect rect = new Rect(0, top, bitmap.getWidth(), bottom);

//利用Palette的API来获取颜色
mBuilder = new Palette.Builder(bitmap)
                .clearFilters()
                .addFilter(FILTER)
                .setRegion(rect.left, rect.top, rect.right, rect.bottom);
Palette p = mBuilder.generate()
//这里会获取一个List<Palette.Swatch>, Swatch就是代表获取到一种颜色,
// getPopulation() :返回这颜色的数量。
// getRgb(): 返回rgb颜色

List<Palette.Swatch> swatches = new ArrayList<>(palette.getSwatches());                

//给swatches排排序,获取数量最大的那个
Collections.sort(swatches, new Comparator<Palette.Swatch>() {
    @Override
    public int compare(Palette.Swatch lhs, Palette.Swatch rhs) {
        if (lhs == null && rhs != null) {
            return 1;
        } else if (lhs != null && rhs == null) {
            return -1;
        } else if (lhs == null) {
            return 0;
        } else {
            return rhs.getPopulation() - lhs.getPopulation();
        }
    }
});
populationSwatch = swatches.get(0);
color = populationSwatch.getRgb();//color就是我需要的颜色,把它设置到状态栏即可

根据颜色去判断使用状态栏图标样式:


private static final float BLACK_MAX_LIGHTNESS = 0.05f;
private static final float WHITE_MIN_LIGHTNESS = 0.95f;

// 将color转成hsl色彩模式
private boolean isDarkStyle(int color) {
    boolean isDarkStyle = false;
    //mTemp是一个float数组,数组长度为3,数组的值依次为对色相(H)、饱和度(S)、明度(L)
    ColorUtils.colorToHSL(color, mTemp);
    if (mTemp[2] <= BLACK_MAX_LIGHTNESS) {
        isDarkStyle = false; // 白色
    } else if (mTemp[2] >= WHITE_MIN_LIGHTNESS) {
        isDarkStyle = true;  // 黑色
    }
    return isDarkStyle;
}

最后我将实践写成了一个库: https://github.com/lliuguangbo/AutoSystemBar

使用起来很简单, 有问题可以提issue, 也可以star以下表示支持,谢谢.

//1.
SystemBarHelper.Builder().into(activity)

//2.
SystemBarHelper.Builder()
                .statusBarColor()    // 设置状态栏颜色
                .statusBarFontStyle()  // 设置状态栏时间,电量的风格, 6.0以上, 部分国产机可以不用6.0以上.
                .navigationBarColor()  // 设置导航栏颜色
                .enableImmersedStatusBar()  // 布局嵌入状态栏,例如图片嵌入状态栏
                .enableImmersedNavigationBar()  // 布局嵌入导航栏,例如图片嵌入导航栏
                .enableAutoSystemBar(false)  // 根据状态栏下面的背景颜色自动调整状态栏的颜色, 自动调整状态栏时间,电量的风格, 默认是开启的
                .into(this)
                
//3.                
SystemBarHelper helper = SystemBarHelper.Builder().into(activity);
helper.setNavigationBarColor()
helper.setStatusBarColor()
helper.statusBarFontStyle()
helper.enableImmersedStatusBar()
helper.enableImmersedNavigationBar()
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,290评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,107评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,872评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,415评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,453评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,784评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,927评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,691评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,137评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,472评论 2 326
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,622评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,289评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,887评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,741评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,316评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,490评论 2 348

推荐阅读更多精彩内容