Android 半沉浸状态栏、导航栏 适配LOLLIPOP( 5.0 )

# 写在前面

完全沉浸:隐藏状态栏、导航栏,在需要时呼出,且会自动再次隐藏。用到的场景很少,例如阅读。
半沉浸:状态栏、导航栏都可以显示且透明,app内容在二者之下。
我实现的是后者。

Api 21中,官方为我们提供了可以直接修改statusbar和navigationbar颜色的方法:

  • window.setStatusBarColor(@ColorInt int color);
  • window.setNavigationBarColor(@ColorInt int color);

但是在此之前还需要立一些flag (官方注释如此解释道):

    /**
     * Sets the color of the status bar to {@code color}.
     *
     * For this to take effect,
     * the window must be drawing the system bar backgrounds with
     * {@link android.view.WindowManager.LayoutParams#FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS} and
     * {@link android.view.WindowManager.LayoutParams#FLAG_TRANSLUCENT_STATUS} must not be set.
     *
     * If {@code color} is not opaque, consider setting
     * {@link android.view.View#SYSTEM_UI_FLAG_LAYOUT_STABLE} and
     * {@link android.view.View#SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN}.
     * <p>
     * The transitionName for the view background will be "android:status:background".
     * </p>
     */

需要给window设置flag:

 window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);

以及清除对应的flag:(如果加上这两个Flag,会变成半透明状态,且设置颜色无效果)

window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);

那么简单的修改颜色就可以写成:

    public static void setColor(@NonNull Window window, @ColorInt Integer statusbarColor, @ColorInt Integer navigationColor) {
        window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
        window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
        window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
        
        window.setStatusBarColor(statusbarColor);
        window.setNavigationBarColor(navigationColor);
    }

# 沉浸式 状态栏 与 导航栏 的实现

chenjin.png

#思路

与很多方法不同,我通过设置 decorChild0 的padding来控制状态栏、导航栏的沉浸与否。

  1. 在api 21及以上版本中 :
  • 我们的内容包含在 decorView.getChildAt(0) 中,后面简称 decorChild0;
  • decorView.getChildAt(1) 是 statusbar;
  • decorView.getChildAt(2) 是 navigationbar;
  1. decorChild0、statusbar、navigationbar同处于decorView这个FramLayout中,之所以我们的内容不被statusbar和navigationbar遮住一部分,是因为window在设置flag后替decorChild0设置了paddingTop和marginBot,并且这不是立即生效的,你会发现设置flag后立即设置decorChild0的padding、margin是没有效果的。而在我延迟了20毫秒后设置是生效的。
  2. 我的 方法 和 参数 :
 public static void setColor(@NonNull Window window, @ColorInt Integer statusbarColor, @ColorInt Integer navigationColor, Boolean belowSta, Boolean belowNav)
  • window : 当前的 window
  • statusbarColor : 状态栏颜色 ( 为 null 时,不改变当前颜色 )
  • navigationColor : 导航栏颜色 ( 为 null 时,不改变当前颜色 )
  • belowSta : 状态栏是否沉浸 ( 为 null 时,不改变当前状态 )
  • belowNav : 导航栏是否沉浸 ( 为 null 时,不改变当前状态 )

步骤 1. 设置与清除 Flag

        int flags = window.getAttributes().flags;//拿到window的flag值
        if ((flags & WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) !=
                WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) {
//若没有该flag,则添加
            window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
        }
        if ((flags & WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) ==
                WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) {
//若有该flag,则清除
            window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
        }

        if ((flags & WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION) ==
                WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION) {
//若有该flag,则清除
            window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
        }

只会用到这三个Flag.

步骤 2. 设置颜色

//当颜色参数不为 null ,并且与前色值不一样时设置
        if (statusbarColor != null && statusbarColor != window.getStatusBarColor()) {
            window.setStatusBarColor(statusbarColor);
        }
        if (navigationColor != null && navigationColor != window.getNavigationBarColor()) {
            window.setNavigationBarColor(navigationColor);
        }

步骤 3. 将 belowSta 和 belowNav 保存至 decorChild0 中

为什么要保存:将改变 decorChild0 的padding、margin重新提一个静态方法出来,在其他地方设置了window的 flag 后,方便刷新状态。

        ViewGroup decorView = (ViewGroup) window.getDecorView();
        final View decorChild = decorView.getChildAt(0);
        if (belowSta != null) {
            decorChild.setTag(R.id.tag_decor_child_below_statusbar, belowSta);
        }
        if (belowNav != null) {
            decorChild.setTag(R.id.tag_decor_child_below_navigation, belowNav);
        }
  • R.id.tag_decor_child_below_statusbar、R.id.tag_decor_child_below_navigation需要在 res / values 下面添加 resource 值:
    <item name="tag_decor_child_check_retry" type="id" />// 用来保存,定时器对象
    <item name="tag_decor_child_below_statusbar" type="id" />
    <item name="tag_decor_child_below_navigation" type="id" />

步骤 4. 获取 decorChild0 的状态

    public static void refreshDecorChildLayout(@NonNull final Window window) {
        ViewGroup decorView = (ViewGroup) window.getDecorView();
        final View decorChild = decorView.getChildAt(0);
//定时器,这里我用的 rxjava 来做延迟和重复操作
        Object checkRetryObj = decorChild.getTag(R.id.tag_decor_child_check_retry);
        Disposable checkRetry;
        if (checkRetryObj != null && checkRetryObj instanceof Disposable) {
            checkRetry = (Disposable) checkRetryObj;
            if (!checkRetry.isDisposed()) {
//若正在做刷新操作则停止
                checkRetry.dispose();
            }
        }
        Object belowStaObj = decorChild.getTag(R.id.tag_decor_child_below_statusbar);
        Object belowNavObj = decorChild.getTag(R.id.tag_decor_child_below_navigation);
        //判断 statusbar、navigationbar 是否可见,若不可见,则必须为沉浸状态,否则会留出空白
        boolean[] systemUiVisible = Statusbar.isSystemUiVisible(window);
//需要的状态
        final Boolean belowSta, belowNav;
        if (belowStaObj != null && belowStaObj instanceof Boolean) {
            belowSta = (Boolean) belowStaObj | !systemUiVisible[0];
        } else {
            belowSta = !systemUiVisible[0] ? true : null;
        }
        if (belowNavObj != null && belowNavObj instanceof Boolean) {
            belowNav = (Boolean) belowNavObj | !systemUiVisible[1];
        } else {
            belowNav = !systemUiVisible[1] ? true : null;
        }
//在此保存状态
        decorChild.setTag(R.id.tag_decor_child_below_statusbar, belowSta);
        decorChild.setTag(R.id.tag_decor_child_below_navigation, belowNav);

步骤 5. 延时设置沉浸效果

需要用到 rxjava :

    implementation "io.reactivex.rxjava2:rxjava:2.1.14"
    implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'

延时设置沉浸效果 :

//这里是每隔50毫秒设置一次,重复3次,防止设置失败
        checkRetry = Observable.interval(50, 50, TimeUnit.MILLISECONDS)//检查间隔
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .take(3)//重复检查次数
                .subscribe(new Consumer<Long>() {
                    private boolean refreshPadding = false;

                    @Override
                    public void accept(Long aLong) throws Exception {
                        refreshPadding = false;
                        int paddingTop = decorChild.getPaddingTop();
                        int paddingBottom = decorChild.getPaddingBottom();

                        //根据状态判断需要设置的paddingTop的高度.statusbar
                        if (belowSta != null) {
                            if (belowSta && paddingTop != 0) {
                                paddingTop = 0;
                                refreshPadding = true;
                            } else if (!belowSta) {
                                int statusBarHeight = StatusbarTools.getStatusBarHeight(window.getContext());
                                if (paddingTop != statusBarHeight) {
                                    paddingTop = statusBarHeight;
                                    refreshPadding = true;
                                }
                            }
                        }
                        //根据状态判断需要设置的paddingBot的高度.navigation
                        if (belowNav != null) {
                            if (belowNav && paddingBottom != 0) {
                                paddingBottom = 0;
                                refreshPadding = true;
                            } else if (!belowNav) {
                                int navigationHeight = StatusbarTools.getNavigationHeight(window.getContext());
                                if (paddingBottom != navigationHeight) {
                                    paddingBottom = navigationHeight;
                                    refreshPadding = true;
                                }
                            }
                        }
//如非paddingTop或paddingBot值有改变,尽量不调用setPadding方法
                        if (refreshPadding) {
                            decorChild.setPadding(decorChild.getPaddingLeft(), paddingTop, decorChild.getPaddingRight(), paddingBottom);
                        }
                        //设置marginTop与marginBot为 0
                        FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) decorChild.getLayoutParams();
                        if (layoutParams.topMargin != 0 || layoutParams.bottomMargin != 0) {
                            layoutParams.topMargin = 0;
                            layoutParams.bottomMargin = 0;
                            decorChild.setLayoutParams(layoutParams);
                        }
//不需要设置 FitsSystemWindows
//                        if (!decorChild.getFitsSystemWindows()) {
//                            decorChild.setFitsSystemWindows(false);
//                        }

                    }
                });
//保存延时刷新对象,便于下次刷新时取消前一次刷新
        decorChild.setTag(R.id.tag_decor_child_check_retry, checkRetry);
    }

判断 statusbar、navigationbar 是否可见的方法 :

      /**
     * 在这里不能用内容高度和屏幕真实高度作对比来判断导航栏显示。
     * 这里只适用于21以后的版本,方法是从DecorView源码中来的,
     * 测试了模拟器21版本,和我自己手机Android 8.1.0都是有效的
     * api min is 21 version
     * 0:statusbar is visible
     * 1:navigation is visible
     *
     * @return statusbar, navigation是否可见
     */
    public static boolean[] isSystemUiVisible(Window window) {
        boolean[] result = new boolean[]{false, false};
        if (window == null) {
            return result;
        }
        WindowManager.LayoutParams attributes = window.getAttributes();
        if (attributes != null) {
            result[0] = (attributes.flags & WindowManager.LayoutParams.FLAG_FULLSCREEN) != WindowManager.LayoutParams.FLAG_FULLSCREEN;
            //
            ViewGroup decorView = (ViewGroup) window.getDecorView();
            result[1] = (((attributes.systemUiVisibility | decorView.getWindowSystemUiVisibility()) &
                    View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) && (attributes.flags & WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0;
        }
        //
        Object decorViewObj = window.getDecorView();
        Class<?> clazz = decorViewObj.getClass();
        int mLastBottomInset = 0, mLastRightInset = 0, mLastLeftInset = 0;
        try {
            Field mLastBottomInsetField = clazz.getDeclaredField("mLastBottomInset");
            mLastBottomInsetField.setAccessible(true);
            mLastBottomInset = mLastBottomInsetField.getInt(decorViewObj);
        } catch (Exception e) {
            e.printStackTrace();
        }
        try {
            Field mLastRightInsetField = clazz.getDeclaredField("mLastRightInset");
            mLastRightInsetField.setAccessible(true);
            mLastRightInset = mLastRightInsetField.getInt(decorViewObj);
        } catch (Exception e) {
            e.printStackTrace();
        }
        try {
            Field mLastLeftInsetField = clazz.getDeclaredField("mLastLeftInset");
            mLastLeftInsetField.setAccessible(true);
            mLastLeftInset = mLastLeftInsetField.getInt(decorViewObj);
        } catch (Exception e) {
            e.printStackTrace();
        }
        boolean isNavBarToRightEdge = mLastBottomInset == 0 && mLastRightInset > 0;
        int size = isNavBarToRightEdge ? mLastRightInset : (mLastBottomInset == 0 && mLastLeftInset > 0 ? mLastLeftInset : mLastBottomInset);
        result[1] = result[1] && size > 0;
        return result;
    }

获取状态栏和导航栏高度:

    /**
     * @return 状态栏高度
     */
    public static int getStatusBarHeight(@Nullable Context context) {
        if (context == null) return 0;
        int result = 0;
        int resId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
        if (resId != 0) {
            result = context.getResources().getDimensionPixelOffset(resId);
        }
        return result;
    }

    /**
     * @return 导航栏高度
     */
    public static int getNavigationHeight(@Nullable Context context) {
        if (context == null) return 0;
        int result = 0;
        Resources resources = context.getResources();
        int resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android");
        if (resourceId != 0) {
            result = resources.getDimensionPixelSize(resourceId);
        }
        return result;
    }

# 缺点:

  1. 在任何调用 window.addFlag() 或 window.setFlag() 后需要调用刷新方法来刷新沉浸状态,否则状态会有改变的可能。
  2. 由于原生方法和我的方法在反复地拉扯padding、margin值,所以当 '沉浸’ 并且 '连续修改颜色' 时,会出现内容抖动的情况,下面 Gif 中会发现。缓解:
  • 可以适当增加刷新延迟的时间,抖动会没有那么频繁;
  • 可以在连续修改颜色时,不调用刷新方法,在最后才调用。

- 附上图 和方法:

  1. 抖动的情况:
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    int alpha = (int) ((1f * progress / seekBar.getMax()) * 255);
    int statusbar = Color.argb(alpha, 255, 0, 0);
    int navigation = Color.argb(alpha, 0, 255, 0);
    Statusbar.setColor(getWindow(), statusbar, navigation, true, true);
 }
抖动.gif
  1. DrawerLayout + NavigationView :
// 状态栏沉浸
Statusbar.setColor(getWindow(), Color.parseColor("#40ff0000"),Color.parseColor("#900000ff"),true,null);
//状态栏不沉浸
Statusbar.setColor(getWindow(), Color.parseColor("#40ff0000"),Color.parseColor("#900000ff"),false,null);
//导航栏沉浸
Statusbar.setColor(getWindow(), Color.parseColor("#40ff0000"),Color.parseColor("#900000ff"),null,true);
//导航栏不沉浸
Statusbar.setColor(getWindow(), Color.parseColor("#40ff0000"),Color.parseColor("#900000ff"),null,false);
DrawerLayout+NavigationView.gif

为了更清晰,我将状态栏设置为25%透明红色,与Actionbar的蓝色叠加成了紫色,要想完全透明,设置 Transparent就行了。

# Tips:

如果发现顶部多出一个状态栏高度,请检查代码或者 layout.xml 中根布局是否设置了 android:fitsSystemWindows="true" 属性,去掉。

# 最后上图

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

推荐阅读更多精彩内容