0%

Android沉浸式状态栏趟坑之旅

前言

Android从4.4开始提出透明状态栏的概念,在5.0之后才有比较像样的沉浸式状态栏,虽然不能修改状态栏图标颜色,至少体验变得更好了,但是对于开发者来说,Android因为版本分裂,导致没法统一,对于开发者来说的确是一件比较头疼的事,下面让我们来看看其中的坑吧。

沉浸式状态栏实现

5.0及以上手机想要做到沉浸式状态栏还是比较容易的(仅仅针对官方Android来说),代码如下:

1
2
3
4
5
6
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Window window = getActivity().getWindow();
window.getDecorView()
.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
window.setStatusBarColor(Color.TRANSPARENT);
}


此时,你会发现另外一个问题(上图):华为有些机型(EMUI 3.1,Android 5.0.2 ,PLK-AL10)就算是Android 5.0以上,也没法做到状态栏透明,会遮挡部分内容,这样的方案是绝对不会允许的。只能针对华为手机进行适配,判断如果是华为手机,则是用如下方案:

1
2
3
4
5
6
7
8
9
10
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Window window = getActivity().getWindow();
window.getDecorView()
.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
window.setStatusBarColor(Color.TRANSPARENT);
if (RomUtils.isEmui()) {
//适配华为手机
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
}
}

其中RomUtils,由网友@夏末m提供,其代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
public class RomUtils {
private static final String TAG = "RomUtils";
public static final String ROM_MIUI = "MIUI";
public static final String ROM_EMUI = "EMUI";
public static final String ROM_FLYME = "FLYME";
public static final String ROM_OPPO = "OPPO";
public static final String ROM_SMARTISAN = "SMARTISAN";
public static final String ROM_VIVO = "VIVO";
public static final String ROM_QIKU = "QIKU";
private static final String KEY_VERSION_MIUI = "ro.miui.ui.version.name";
private static final String KEY_VERSION_EMUI = "ro.build.version.emui";
private static final String KEY_VERSION_OPPO = "ro.build.version.opporom";
private static final String KEY_VERSION_SMARTISAN = "ro.smartisan.version";
private static final String KEY_VERSION_VIVO = "ro.vivo.os.version";
private static String sName;
private static String sVersion;
public static boolean isEmui() {
return check(ROM_EMUI);
}
public static boolean isMiui() {
return check(ROM_MIUI);
}
public static boolean isVivo() {
return check(ROM_VIVO);
}
public static boolean isOppo() {
return check(ROM_OPPO);
}
public static boolean isFlyme() {
return check(ROM_FLYME);
}
public static boolean is360() {
return check(ROM_QIKU) || check("360");
}
public static boolean isSmartisan() {
return check(ROM_SMARTISAN);
}
public static String getName() {
if (sName == null) {
check("");
}
return sName;
}
public static String getVersion() {
if (sVersion == null) {
check("");
}
return sVersion;
}
public static boolean check(String rom) {
if (sName != null) {
return sName.equals(rom);
}
if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_MIUI))) {
sName = ROM_MIUI;
} else if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_EMUI))) {
sName = ROM_EMUI;
} else if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_OPPO))) {
sName = ROM_OPPO;
} else if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_VIVO))) {
sName = ROM_VIVO;
} else if (!TextUtils.isEmpty(sVersion = getProp(KEY_VERSION_SMARTISAN))) {
sName = ROM_SMARTISAN;
} else {
sVersion = Build.DISPLAY;
if (sVersion.toUpperCase().contains(ROM_FLYME)) {
sName = ROM_FLYME;
} else {
sVersion = Build.UNKNOWN;
sName = Build.MANUFACTURER.toUpperCase();
}
}
return sName.equals(rom);
}
public static String getProp(String name) {
String line = null;
BufferedReader input = null;
try {
Process p = Runtime.getRuntime().exec("getprop " + name);
input = new BufferedReader(new InputStreamReader(p.getInputStream()), 1024);
line = input.readLine();
input.close();
} catch (Exception ex) {
NTLog.e(TAG, "Unable to read prop " + name + ex);
return null;
} finally {
if (input != null) {
try {
input.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
return line;
}
}


解决了状态栏的问题,又发现了另外一个问题(上图):跟帖输入框在软键盘弹起时,被挡住了。没法办,继续寻找解决方案。在网上搜索了一番,发现这个是Android存在已久的bug,有人已经提供了解决方案,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class AndroidBug5497Workaround {
// For more information, see https://code.google.com/p/android/issues/detail?id=5497
// To use this class, simply invoke assistActivity() on an Activity that already has its content view set.
public static void assistActivity (Activity activity) {
new AndroidBug5497Workaround(activity);
}
private View mChildOfContent;
private int usableHeightPrevious;
private FrameLayout.LayoutParams frameLayoutParams;
private AndroidBug5497Workaround(Activity activity) {
FrameLayout content = (FrameLayout) activity.findViewById(android.R.id.content);
mChildOfContent = content.getChildAt(0);
mChildOfContent.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
public void onGlobalLayout() {
possiblyResizeChildOfContent();
}
});
frameLayoutParams = (FrameLayout.LayoutParams) mChildOfContent.getLayoutParams();
}
private void possiblyResizeChildOfContent() {
int usableHeightNow = computeUsableHeight();
if (usableHeightNow != usableHeightPrevious) {
int usableHeightSansKeyboard = mChildOfContent.getRootView().getHeight();
int heightDifference = usableHeightSansKeyboard - usableHeightNow;
if (heightDifference > (usableHeightSansKeyboard/4)) {
// keyboard probably just became visible
frameLayoutParams.height = usableHeightSansKeyboard - heightDifference;
} else {
// keyboard probably just became hidden
frameLayoutParams.height = usableHeightSansKeyboard;
}
mChildOfContent.requestLayout();
usableHeightPrevious = usableHeightNow;
}
}
private int computeUsableHeight() {
Rect r = new Rect();
mChildOfContent.getWindowVisibleDisplayFrame(r);
return (r.bottom - r.top);
}
}

一开始看到这个方案的时候,纵观代码,没有其他大问题,而且因为根布局的UI和Activity的生命周期是一致的,因此不用mChildOfContent.getViewTreeObserver().removeOnGlobalLayoutListener()也不会发生内存泄露,因此可以先尝试一下。

其原理也十分简单,就是监听整个布局,如果布局发生了变化了超过屏幕1/4高度,则可认为是键盘弹起了,此时,重新设置根布局LayoutParams即可。更详细信息可以参考:Android爬坑之旅:软键盘挡住输入框问题的终极解决方案

然而,并不管用!你会发现,输入框被虚拟键盘挡住了(上图)!不过,这个问题也不难解决,我们只需要把第一次根布局的高度记录下来,键盘收起的时候,把跟布局高度还原即可。最终代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/**
* Created by mason on 2017/10/11.
*
* 解决输入框被软键盘隐藏的问题 @See http://www.jianshu.com/p/a95a1b84da11 </br>
*/
public class AndroidBug5497Workaround {
// For more information, see https://code.google.com/p/android/issues/detail?id=5497
// To use this class, simply invoke assistActivity() on an Activity that already has its
// content view set.
public static void assistActivity(Activity activity) {
new AndroidBug5497Workaround(activity);
}
private View mChildOfContent;
private int mUsableHeightPrevious;
private FrameLayout.LayoutParams mFrameLayoutParams;
private int mContentHeight;
private boolean mIsContentHeightMeasured = false;
private AndroidBug5497Workaround(Activity activity) {
FrameLayout content = (FrameLayout) activity.findViewById(android.R.id.content);
mChildOfContent = content.getChildAt(0);
//界面变动监听
mChildOfContent.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver
.OnGlobalLayoutListener() {
public void onGlobalLayout() {
if ( !mIsContentHeightMeasured ) {
//记住第一次根布局高度,兼容华为等机型
mContentHeight = mChildOfContent.getHeight();
mIsContentHeightMeasured = true;
}
possiblyResizeChildOfContent();
}
});
mFrameLayoutParams = (FrameLayout.LayoutParams)
mChildOfContent.getLayoutParams();
}
/**
* 调整根布局高度
*/
private void possiblyResizeChildOfContent() {
int usableHeightNow = computeUsableHeight();
//当前可见高度和上一次可见高度不一致 布局变动
if (usableHeightNow != mUsableHeightPrevious) {
int usableHeightSansKeyboard = mChildOfContent.getRootView().getHeight();
int heightDifference = usableHeightSansKeyboard - usableHeightNow;
if (heightDifference > (usableHeightSansKeyboard / 4)) {
// keyboard probably just became visible
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
mFrameLayoutParams.height = usableHeightSansKeyboard - heightDifference +
SystemUtils.getStatusBarHeight();
} else {
mFrameLayoutParams.height = usableHeightSansKeyboard - heightDifference;
}
} else {
mFrameLayoutParams.height = mContentHeight;
}
mChildOfContent.requestLayout();
mUsableHeightPrevious = usableHeightNow;
}
}
/**
* 计算mChildOfContent可见高度
*
* @return
*/
private int computeUsableHeight() {
Rect r = new Rect();
mChildOfContent.getWindowVisibleDisplayFrame(r);
return (r.bottom - r.top);
}
}



后来又发现,全屏状态下播放视频内容显示不全(上图),推测是因为添加了全局layout监听,并且重新改了跟布局的显示高度,因此在全屏播放时把监听去掉,非全屏播放时加上监听,再重新运行,终于正常了!最终代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
/**
* Created by mason on 2017/10/12.
*
* 解决输入框被软键盘隐藏的问题 @See http://www.jianshu.com/p/a95a1b84da11 </br>
*/
public class AndroidBug5497Workaround {
// For more information, see https://code.google.com/p/android/issues/detail?id=5497
private ViewTreeObserver.OnGlobalLayoutListener mOnGlobalLayoutListener = null;
private FrameLayout.LayoutParams mFrameLayoutParams;
private FrameLayout.LayoutParams mOrigLayoutParams;
private boolean mIsContentHeightMeasured = false;
private int mUsableHeightPrevious;
private View mChildOfContent;
private int mContentHeight;
public AndroidBug5497Workaround(Activity activity) {
if (activity != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
FrameLayout content = (FrameLayout) activity.findViewById(android.R.id.content);
mChildOfContent = content.getChildAt(0);
mFrameLayoutParams = (FrameLayout.LayoutParams)
mChildOfContent.getLayoutParams();
mOrigLayoutParams = new FrameLayout.LayoutParams(mFrameLayoutParams);
addGlobalLayoutListener();
}
}
/**
* 全屏状态下如果还有OnGlobalLayoutListener监听会导致播放器内容显示不全
* 因此需要在Activity变成全屏前手动移除监听,变成非全屏前手动添加监听
*/
public void addGlobalLayoutListener() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
removeGlobalLayoutListener();
mOnGlobalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
public void onGlobalLayout() {
if (!mIsContentHeightMeasured) {
//兼容华为等机型
mContentHeight = mChildOfContent.getHeight();
mIsContentHeightMeasured = true;
}
possiblyResizeChildOfContent();
}
};
mChildOfContent.getViewTreeObserver()
.addOnGlobalLayoutListener(mOnGlobalLayoutListener);
}
}
public void removeGlobalLayoutListener() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (mOnGlobalLayoutListener != null && mChildOfContent != null) {
mFrameLayoutParams = new FrameLayout.LayoutParams(mOrigLayoutParams);
mChildOfContent.setLayoutParams(mFrameLayoutParams);
mChildOfContent.getViewTreeObserver()
.removeOnGlobalLayoutListener(mOnGlobalLayoutListener);
mOnGlobalLayoutListener = null;
}
}
}
/**
* 重新调整跟布局的高度
*/
private void possiblyResizeChildOfContent() {
int usableHeightNow = computeUsableHeight();
//当前可见高度和上一次可见高度不一致 布局变动
if (usableHeightNow != mUsableHeightPrevious) {
int usableHeightSansKeyboard = mChildOfContent.getRootView().getHeight();
int heightDifference = usableHeightSansKeyboard - usableHeightNow;
if (heightDifference > (usableHeightSansKeyboard / 4)) {
// keyboard probably just became visible
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
mFrameLayoutParams.height = usableHeightSansKeyboard - heightDifference +
SystemUtils.getStatusBarHeight();
} else {
mFrameLayoutParams.height = usableHeightSansKeyboard - heightDifference;
}
} else {
mFrameLayoutParams.height = mContentHeight;
}
mChildOfContent.setLayoutParams(mFrameLayoutParams);
mChildOfContent.requestLayout();
mUsableHeightPrevious = usableHeightNow;
}
}
/**
* 计算mChildOfContent可见高度
*
* @return
*/
private int computeUsableHeight() {
Rect r = new Rect();
mChildOfContent.getWindowVisibleDisplayFrame(r);
return (r.bottom - r.top);
}
}

该类是用方法也比较简单,新建一个对象,然后在监听横竖屏变化即可.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
AndroidBug5497Workaround mAndroidBugFixer;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 使用自定义actionbar,也可以把该方法放到Fragment.onViewCreated()下执行
setCustomActionbar();
}

private void setCustomActionbar() {
if (getActivity() == null) return;
ActionBar.LayoutParams lp = new ActionBar.LayoutParams(ActionBar.LayoutParams.MATCH_PARENT,
ActionBar.LayoutParams.MATCH_PARENT);
mActionBarView = LayoutInflater.from(getActivity())
.inflate(R.layout.news_video_detail_action_bar_layout, null);
mBackView = (ImageView) mActionBarView.findViewById(R.id.action_bar_back);
mBackView.setOnClickListener(this);
mTitleView = (TextView) mActionBarView.findViewById(R.id.action_bar_title);
ActionBar actionBar = getActionBar();
if (actionBar != null) {
actionBar.setCustomView(mActionBarView, lp);
actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
actionBar.setDisplayShowCustomEnabled(true);
actionBar.setDisplayShowHomeEnabled(false);
actionBar.setDisplayShowTitleEnabled(false);
actionBar.setBackgroundDrawable(
getResources().getDrawable(android.R.color.transparent));
}
if (SystemUtils.isLollipop()) {
Window window = getActivity().getWindow();
window.getDecorView()
.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
if (RomUtils.isEmui()) {
//适配华为手机
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
}
window.setStatusBarColor(Color.TRANSPARENT);
if (getActivity() != null) {
//修复输入框被软键盘覆盖问题
mAndroidBugFixer = new AndroidBug5497Workaround(getActivity());
}
}
}

@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
if (mAndroidBugFixer != null) {
mAndroidBugFixer.removeGlobalLayoutListener();
}
} else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
if (mAndroidBugFixer != null) {
mAndroidBugFixer.addGlobalLayoutListener();
}
}
}

PS:如果发现是用了该类,屏幕有跳一下,闪一下的现象,可以去掉adjustPan标记

1
2
// 去掉该行代码
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);

搞定了状态栏和输入框的问题,后来视觉又要求actionbar-title要居中,不过这也不是什么难事,自定义actionbar就可以了。最终代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
自定义actionbar view
private void setCustomActionbar() {
if (getActivity() == null) return;
ActionBar.LayoutParams lp = new ActionBar.LayoutParams(ActionBar.LayoutParams.MATCH_PARENT,
ActionBar.LayoutParams.MATCH_PARENT);
mActionBarView = LayoutInflater.from(getActivity())
.inflate(R.layout.news_video_detail_action_bar_layout, null);
mBackView = mActionBarView.findViewById(R.id.action_bar_back);
mBackView.setOnClickListener(this);
mTitleView = (TextView) mActionBarView.findViewById(R.id.action_bar_title);
ActionBar actionBar = getActionBar();
if (actionBar != null) {
actionBar.setCustomView(mActionBarView, lp);
actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
actionBar.setDisplayShowCustomEnabled(true);
actionBar.setDisplayShowHomeEnabled(false);
actionBar.setDisplayShowTitleEnabled(false);
actionBar.setBackgroundDrawable(
getResources().getDrawable(android.R.color.transparent));
//设置actionbar返回按钮图片
//actionBar.setHomeAsUpIndicator(R.drawable.base_action_bar_back);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Window window = getActivity().getWindow();
window.getDecorView()
.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
window.setStatusBarColor(Color.TRANSPARENT);

if (RomUtils.isEmui()) {
//状态栏适配华为手机
window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
}
window.setStatusBarColor(Color.TRANSPARENT);
if (getActivity() != null) {
//修复输入框被软键盘覆盖问题
mAndroidBugFixer = new AndroidBug5497Workaround(getActivity());
}
}
}
public ActionBar getActionBar() {
if (getActivity() instanceof FragmentActivity) {
return ((FragmentActivity)getActivity()).getSupportActionBar();
}
return null;
}

总结

这次解决问题的过程参考了网上的解决方案, 经过了严格的测试,才把代码提交上去。个人对于网上第三方开源库其实是有些谨慎的,原因有二:其一,网上的开源库不一定适合当前项目,可能会有兼容问题(support-v4、support-v7);其二,就算能解决,也不能保证该开源库没有其他问题。这也是这次做状态栏透明没有使用第三方开源库的原因。个人还是比较倾向于自己写,毕竟自己写的代码自己放心。