关于安卓沉浸式状态栏的研究

2017-01-11 10:21 #旧文章

前言

沉浸式状态栏是一个好东西,但是使用起来并没有想象中的那样一帆风顺。

成品图
成品图

需要解决的问题

  1. 我希望状态栏是半透明的,而不仅是比主颜色深
  2. 在设置了一些 flag 以后,布局跑到了状态栏 / 导航栏底下

设置状态栏和导航栏透明

要使状态栏和导航栏透明,可以直接使用如下代码

public static void setBarsTranslucent(Window window, int color) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION | WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.setStatusBarColor(color);
window.setNavigationBarColor(color);
}else if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION | WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
}
}

以上代码做了:

  1. 如果系统版本在 LOLLIPOP 以上,则设置状态栏和导航栏的颜色为 color
    其中 color 的格式为 ARGB,推荐使用 0×20000000
  2. 如果系统版本在 LOLLIPOP 以下,KITKA_WATCH 以上,则设置导航栏和状态栏为透明

其中的各种 flag 的作用不在这里介绍,详细请参照 Android Developer

解决布局进入状态栏 / 导航栏下方的问题

网上的文章提到了 fitsSystemWindows 这一个属性,但是使用起来我感觉不是特别的顺手,所以我翻看了安卓的源码。

其中 View.setFitsSystemWindows(boolean fitSystemWindows)方法的源码如下

public void setFitsSystemWindows(boolean fitSystemWindows) {
setFlags(fitSystemWindows ? FITS_SYSTEM_WINDOWS : 0, FITS_SYSTEM_WINDOWS);
}

于是我顺着 FITS_SYSTEM_WINDOWS 这个 flag 向下搜索,找到了如下方法

private boolean fitSystemWindowsInt(Rect insets) {
if ((mViewFlags & FITS_SYSTEM_WINDOWS) == FITS_SYSTEM_WINDOWS) {
mUserPaddingStart = UNDEFINED_PADDING;
mUserPaddingEnd = UNDEFINED_PADDING;
Rect localInsets = sThreadLocal.get();
if (localInsets == null) {
localInsets = new Rect();
sThreadLocal.set(localInsets);
}
boolean res = computeFitSystemWindows(insets, localInsets);
mUserPaddingLeftInitial = localInsets.left;
mUserPaddingRightInitial = localInsets.right;
internalSetPadding(localInsets.left, localInsets.top,
localInsets.right, localInsets.bottom);
return res;
}
return false;
}

接着我查找了该方法的调用,找到如下方法

protected boolean fitSystemWindows(Rect insets) {
if ((mPrivateFlags3 & PFLAG3_APPLYING_INSETS) == 0) {
if (insets == null) {
return false;
}
try {
mPrivateFlags3 |= PFLAG3_FITTING_SYSTEM_WINDOWS;
return dispatchApplyWindowInsets(new WindowInsets(insets)).isConsumed();
} finally {
mPrivateFlags3 &= ~PFLAG3_FITTING_SYSTEM_WINDOWS;
}
} else {
return fitSystemWindowsInt(insets);
}
}

于是接着查找调用

public WindowInsets onApplyWindowInsets(WindowInsets insets) {
if ((mPrivateFlags3 & PFLAG3_FITTING_SYSTEM_WINDOWS) == 0) {
if (fitSystemWindows(insets.getSystemWindowInsets())) {
return insets.consumeSystemWindowInsets();
}
} else {
if (fitSystemWindowsInt(insets.getSystemWindowInsets())) {
return insets.consumeSystemWindowInsets();
}
}
return insets;
}

注意到这一句了吗

insets.getSystemWindowInsets()

似乎在 WindowInsets 上可以做一点文章,所以我翻看了 WindowInsets 的源码

因为 WindowInsets 里面绝大部分方法都不能被访问,而且找不到对应的 getter,所以我查找了 WindowInsets 的使用,于是我发现了下面的接口

public interface OnApplyWindowInsetsListener {
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets);
}

到这里已经能发现解决方案了。

解决方案

首先我们在 onCreate(Bundle savedInstanceState)中获取根布局,即 setContentView(View view)中的 view

rootView = findViewById(R.id.root_view);

接着我们对 rootView 设置 OnApplyWindowInsetsListener

if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
rootView.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
@Override
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
// System.out.println(insets);
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
// 在这里获取各个方向的inset
// 顶部:insets.getSystemWindowInsetTop();
// 底部:insets.getSystemWindowInsetBotton();
// 左侧:insets.getSystemWindowInsetLeft();
// 右侧:insets.getSystemWindowInsetRight();
}
return insets;
}
});
}

写在最后

在这个研究完成后,我惊喜的发现如果此时 Activity 设置有

android:windowSoftInputMode="adjustResize|stateHidden"

那么键盘弹出时布局会自动被压缩,一石二鸟。