Android透明化/沉浸式状态栏实践及源码分析

何为透明状态栏?有人说透明状态栏就是沉浸式状态栏,这里不多解释,详见为什么在国内会有很多用户把「透明栏」(Translucent Bars)称作 「沉浸式顶栏」?。本文将对于透明状态栏设置方法做一个讲解,以及遇到的问题该怎么解决,来面对各种需求。

本文所提到的透明状态栏其实指的是将顶部的导航栏延伸到状态栏,使之浑然一体(Google官方建议状态栏颜色比导航栏的颜色略深一点),并不代表一定不设置背景色,比如导航栏是白色,则可设置状态栏为白色,视情况而定。

相比于iOS系统,Android系统对于状态栏的设置就显得稍微复杂了一点。Android系统提供了API 19以上对状态栏的设置接口,而直到API 23以上才提供对于icon颜色的设置,还有就是各家厂商(如魅族,小米等)对于状态栏的有自己的定制,对于需要使用浅色背景状态栏的应用,没处理好的话往往导致浅色背景,白色icon ,状态栏不分你我的悲剧。。

纯白状态栏

(内心os:嗯?右上角那一个绿色的电池,用户一定知道他是状态栏对吧。)

我随即对比了一些主流app,发现在我的魅蓝2(Android 5.1 Flyme 4.5)上竟然都不支持透明状态栏,这对于我这种追求审美的人(其实是视觉提的需求)来说简直不能忍。在我折腾了几天之后,终于解决了这些问题,希望对大家思路有一些帮助。

言归正传,本文主要针对以下几点进行分析:1.是否隐藏状态栏(全屏模式) 2.状态栏的背景色的设置 3.状态栏icon的颜色的设置,而对于透明状态栏设置过程中,可能造成的icon颜色设置成功,而背景颜色设置失败,等等原因造成的浅色底,浅色字或深色底,深色字等错误情况的处理及兜底方案 4.源码实现。

下面我们就以上几点来讨论一下Android中透明状态栏的实现。

1. 全屏模式(沉浸式状态栏)

这种情况其实用得并不多,基本上使用场景在闪屏页展示广告或logo,以及一些阅读类app需要尽可能的利用到屏幕大小,展示更多的内容。

阅读类app展示

设置方法很简单,分两种,在API > 16时:

  • theme中定义:将自定义该Activtiy的theme,并在其中添加
<item name="android:windowFullscreen">true</item>
  • 代码中定义:
window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)

2. 状态栏的背景色

我们都知道,在Android5.0(即API > 21)时,Google官方提供接口设置对应的状态栏背景色

window.setStatusBarColor(@ColorInt int color);

那我们要想在Android5.0以下设置背景色就真的没有办法了吗?并不是,我们发现在Android4.4以后,出现了windowTranslucentStatus这一特性,所以思路如下:

  1. 先设置状态栏的透明属性
  2. 给根布局顶部加上一个和状态栏一样大小的矩形View,充当假的状态栏
  3. 设置FitsSystemWindows属性为true,此时跟布局会延伸到状态栏,处在顶部位置就是之前设置的view,这样就可以以假乱真了。
       if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            // 设置状态栏透明
            activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
            // 生成一个状态栏大小的矩形View
            View statusView = createStatusView(activity, color);
            ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
            decorView.addView(statusView);
            // 设置根布局的参数
            ViewGroup rootView = (ViewGroup) ((ViewGroup) activity.findViewById(android.R.id.content)).getChildAt(0);
            rootView.setFitsSystemWindows(true);
            rootView.setClipToPadding(true);
        }

设置矩形色块的代码如下:

  private static View createStatusView(Activity activity, int color) {
        // 获得状态栏高度
        int resourceId = activity.getResources().getIdentifier("status_bar_height", "dimen", "android");
        int statusBarHeight = activity.getResources().getDimensionPixelSize(resourceId);

        // 绘制一个和状态栏一样高的矩形
        View statusView = new View(activity);
        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                statusBarHeight);
        statusView.setLayoutParams(params);
        statusView.setBackgroundColor(color);
        return statusView;
    }

效果如下:

全透明状态栏

3. 状态栏icon的颜色

我们通过调研发现,魅族和小米的两个厂商分别对于Flyme及MIUI做了状态栏的定制,所以我们可以在Flyme4.0以上及MIUIV6以上都可以实现icon颜色的替换,结合上述的状态栏背景色的替换,我们就可以对于魅族及小米两款机型适配浅色的导航栏了。

3.1 魅族Flyme4.0+

    /**
     * 魅族Flyme4+以上透明状态栏
     * @param window 
     * @param dark 是否把状态栏字体及图标颜色设置为深色
     * @return 是否设置成功
     */
    public static boolean setMeizuStatusBarDarkIcon(Window window, boolean dark) {
        boolean result = false;
        if (window != null) {
            try {
                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);
                result = true;
            } catch (Exception e) {
                //错误处理
            }
        }
        return result;
    }

3.2 MIUIV6.0+

 /**
     * 设置状态栏字体图标为深色,需要MIUIV6以上
     * @param window
     * @param dark 是否把状态栏字体及图标颜色设置为深色
     * @return  是否设置成功
     *
     */
    public static boolean setMIUIStatusBarLightMode(Window window, boolean dark) {
        boolean result = false;
        if (window != null) {
            Class clazz = window.getClass();
            try {
                int darkModeFlag = 0;
                Class layoutParams = Class.forName("android.view.MiuiWindowManager$LayoutParams");
                Field  field = layoutParams.getField("EXTRA_FLAG_STATUS_BAR_DARK_MODE");
                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);//清除黑色字体
                }
                result=true;
            }catch (Exception e){
                //错误处理
            }
        }
        return result;
    }

效果如下:

适配浅色导航栏

4. 设置背景色失败处理方案

4.1 解决思路

说了那么多,有童鞋可能就问了,上述说的代码可能对大多数机型有效,但是万一某些机型设置失败了呢?体验不但没提升,反倒让用户不能忍受了。

好吧,下面我们来看一下应该怎么解决。思路如下:

  1. 获取状态栏的背景色,与需要设置的颜色进行比对,如果相等则设置成功。
  2. 如果背景色设置失败,针对非Flyme机型,我们回退状态栏icon颜色,还原到最初状态。
  3. 上述为什么要强调非Flyme机型,是因为用过Flyme机型的童鞋肯定知道有一个透明状态栏的开关,当开启后,Flyme会对状态栏进行处理,但处理效果并不和我们想要的一样(比如视图顶部为黑色背景,但是却设置了白色状态栏),并且两者都对状态栏进行操作,操作的冲突也有可能引起上述的icon颜色不明显的情况,所以我们自己定义一个ColorDrawable进行替换。

4.2 获取状态栏颜色

   /**
     * 获取状态栏颜色
     *
     * @param window 需要获取的activity
     * @return color   状态栏的颜色值
     */
    public static int getColor(Window window){
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            return window.getStatusBarColor();
        }else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            ViewGroup decorView = (ViewGroup) window.getDecorView();
            int count = decorView.getChildCount();
            if (count > 0 && decorView.getChildAt(count - 1) instanceof StatusBarView) {
                int color = Color.TRANSPARENT;
                Drawable background = decorView.getChildAt(count - 1).getBackground();
                if (background instanceof ColorDrawable)
                    color = ((ColorDrawable) background).getColor();
                return color;
            }
        }
        return -1;
    }

下面讲一下针对如何屏蔽Flyme机型沉浸式状态栏打开时的影响:

 private static boolean setMeizuStatusColor(Window window, @ColorInt int color, int alpha) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            final View view = window.getDecorView().findViewById(android.R.id.statusBarBackground);
            if (view != null) {
                if (!(view.getBackground() instanceof MeizuColorDrawable)) {
                        //替换成自定义ColorDrawable
                    view.setBackground(new MeizuColorDrawable());
                }

                if (view.getBackground() instanceof MeizuColorDrawable) {
                    MeizuColorDrawable colorDrawable = (MeizuColorDrawable) view.getBackground();
                    int finalColor = StatusBarUtil.calculateStatusColor(color, alpha);
                    view.setBackgroundColor(finalColor);
                    colorDrawable.setColor(finalColor);
                    return true;
                }
            }
        }
        return false;
    }

4.3 兜底方案

上面提到了一些失败判断途径,那么如果还是对一些机型无效呢?我的建议是可以在本地预埋一个开关,万一出问题,服务器可以对相应机型的沉浸式开关关掉即可,或者采用hotfix等方式来进行修复。

5. 源码分析

看过上面针对魅族和小米设置icon的源代码我们知道,共同点都是通过反射来进行调用相应的方法,或者对于相应的状态栏视图进行设置颜色。
我们来看看源码,在Window中有一个抽象方法是setStatusBarColor而在PhoneWindow中我们找到了该方法的实现

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

mDecor.updateColorViews()

        private WindowInsets updateColorViews(WindowInsets insets, boolean animate) {
        ...
                updateColorViewInt(mNavigationColorViewState, sysUiVisibility, mNavigationBarColor,
                        navBarSize, navBarToRightEdge, 0 /* rightInset */,
                        animate && !disallowAnimate);

                boolean statusBarNeedsRightInset = navBarToRightEdge
                        && mNavigationColorViewState.present;
                int statusBarRightInset = statusBarNeedsRightInset ? mLastRightInset : 0;
                updateColorViewInt(mStatusColorViewState, sysUiVisibility, mStatusBarColor,
                        mLastTopInset, false /* matchVertical */, statusBarRightInset,
                        animate && !disallowAnimate);
        ...
}

那么mNavigationColorViewState和mStatusColorViewState又是什么呢?
别着急,我们继续往下看

       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);
        private final ColorViewState mNavigationColorViewState = new ColorViewState(
                SYSTEM_UI_FLAG_HIDE_NAVIGATION, FLAG_TRANSLUCENT_NAVIGATION,
                Gravity.BOTTOM,
                Gravity.RIGHT,
                NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME,
                com.android.internal.R.id.navigationBarBackground,
                0 /* hideWindowFlag */);

mNavigationColorViewState和mStatusColorViewState这两个都是ColorViewState定义好的ColorViewState,主要存储了关于statusbar的一些视图信息。包括id,translucentFlag,verticalGravity等等。

接着看updateColorViewInt()方法,这个方法是对一个View设置颜色的,自然这个View也就是指的状态栏的View,下面来看一下实现:

private void updateColorViewInt(final ColorViewState state, int sysUiVis, int color,
              int size, boolean verticalBar, int rightMargin, boolean animate) {
              state.present = size > 0 && (sysUiVis & state.systemUiHideFlag) == 0
                    && (getAttributes().flags & state.hideWindowFlag) == 0
                    && (getAttributes().flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0;
            boolean show = state.present
                    && (color & Color.BLACK) != 0
                    && (getAttributes().flags & state.translucentFlag) == 0;

            boolean visibilityChanged = false;
            View view = state.view;

            int resolvedHeight = verticalBar ? LayoutParams.MATCH_PARENT : size;
            int resolvedWidth = verticalBar ? size : LayoutParams.MATCH_PARENT;
            int resolvedGravity = verticalBar ? state.horizontalGravity : state.verticalGravity;

              //对传入的ColorViewState的view做非空判断
            if (view == null) {
                if (show) {
                    //新建一个view
                    state.view = view = new View(mContext);
                    //设置背景色
                    view.setBackgroundColor(color);
                    view.setTransitionName(state.transitionName);
                    view.setId(state.id);
                    visibilityChanged = true;
                    //有人可能会感觉奇怪,为什么show为true的时候,这里反倒要设置INVISIBLE。从后面代码可以知道,这里无非是想通过动画来做一个渐显操作,而不是立马显示出来。
                    view.setVisibility(INVISIBLE);
                    state.targetVisibility = VISIBLE;

                    LayoutParams lp = new LayoutParams(resolvedWidth, resolvedHeight,
                            resolvedGravity);
                    lp.rightMargin = rightMargin;
                    addView(view, lp);
                    updateColorViewTranslations();
                }
            } else {
                int vis = show ? VISIBLE : INVISIBLE;
                visibilityChanged = state.targetVisibility != vis;
                state.targetVisibility = vis;
                if (show) {
                    LayoutParams lp = (LayoutParams) view.getLayoutParams();
                    if (lp.height != resolvedHeight || lp.width != resolvedWidth
                            || lp.gravity != resolvedGravity || lp.rightMargin != rightMargin) {
                        lp.height = resolvedHeight;
                        lp.width = resolvedWidth;
                        lp.gravity = resolvedGravity;
                        lp.rightMargin = rightMargin;
                        view.setLayoutParams(lp);
                    }
                    //设置背景色
                    view.setBackgroundColor(color);
                }
            }
            //改变状态栏visibility属性时加入动画
            if (visibilityChanged) {
                view.animate().cancel();
                if (animate) {
                    if (show) {
                        //显示时先设置为全透明
                        if (view.getVisibility() != VISIBLE) {
                            view.setVisibility(VISIBLE);
                            view.setAlpha(0.0f);
                        }
                        //关于透明度的动画插值器
                        view.animate().alpha(1.0f).setInterpolator(mShowInterpolator).
                                setDuration(mBarEnterExitDuration);
                    } else {
                        view.animate().alpha(0.0f).setInterpolator(mHideInterpolator)
                                .setDuration(mBarEnterExitDuration)
                                .withEndAction(new Runnable() {
                                    @Override
                                    public void run() {
                                        state.view.setAlpha(1.0f);
                                        state.view.setVisibility(INVISIBLE);
                                    }
                                });
                    }
                } else {
                    view.setAlpha(1.0f);
                    view.setVisibility(show ? VISIBLE : INVISIBLE);
                }
            }
        }

6. 总结

话说了这么多,其实就是想让大家知道为什么很多app放弃在较低版本的Android系统对状态栏进行视觉修改,尤其是浅色导航栏的应用。[捂脸]

本人目前在网易搬砖,如果想要挪挪地方,请猛戳此处,选好部门及岗位以后,欢迎投简历给我(changan_luo@163.com),我来帮您内推。

最后欢迎拍砖,说得不好的地方或者有误的地方可以在评论区交流~

喜欢的话给民工打赏晚饭加个鸡腿就更好了,谢谢~

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

推荐阅读更多精彩内容