简洁明了的刘海屏适配方案

网上关于刘海屏适配的文章不少,可讲清楚的却没几篇,大多是拷贝文档、长篇大论,甚至热情的贴图告诉你什么是刘海屏,到最后你仍不确定到底是怎样的一个适配方案,才能让你的 app 真正的适配所有的刘海屏机型。

看到这篇文章你就无需再怨恨各大厂商的跟风“刘海”了,因为刘海屏的适配十分简单。

ok,废话说完了,开始适配。

首先要清楚的是哪些界面需要适配刘海屏:

  • 有状态栏的界面:刘海区域会显示状态栏,无需适配
  • 全屏界面:刘海区域可能遮挡内容,需要适配

如果你的应用里所有界面都有状态栏,那么恭喜你,你不用做任何操作,状态栏就那么自然的显示在刘海区域,毫无违和,刘海屏已适配完毕,可以点叉出去了。

不幸的是,你的应用中很大几率会有全屏界面,所谓的刘海屏适配,也正是针对这些全屏界面。

如果你什么都不做,默认规则不允许全屏界面内容显示到刘海区域,即刘海屏区域会保留一条黑边,你的全屏界面会在刘海下方展示,这看起来好像也是可以接受的,然后你竟说服产品达成共识,“无为而治”才是最强大的刘海屏适配方案!

但有些手机厂商(譬如oppo)不开心了,我辛辛苦苦研发的刘海屏手机,你们这些开发者竟直接放弃刘海区域!然后就在你的全屏界面下方加了一条提示:“全屏显示”,当用户点击开启后,强行把你的全屏界面显示到刘海区域,然后一切都乱套了…

嗯~ “无为而治”行不通。

只能允许全屏界面内容显示到刘海区域了,参考各大厂商的适配文档,我们可以知道如何允许,比如华为机型只需在 AndroidManifest 中配置:

1
2
3
<meta-data
android:name="android.notch_support"
android:value="true" />

配置后,华为机型上的全屏界面就会显示到刘海区域了,但这个刘海,是可能挡住我们全屏界面中的内容的。这时需要将全屏界面中的视图元素适当下移,保证不会被刘海遮挡住,就 ok 了。

这里我们搞清楚:允许全屏界面内容显示到刘海区域的机型,才需要将全屏界面中的视图元素适当下移。

比如若只允许华为机型全屏界面内容显示到刘海区域,那只有华为的刘海屏机型才需要将全屏界面中的视图元素适当下移,其他厂商的刘海屏机型则不需要下移。

如果允许华为、小米、oppo、vivo 全屏界面内容显示到刘海区域,那么华为、小米、oppo、vivo 刘海屏机型需要将全屏界面中的视图元素适当下移。

另外也不一定要通过全屏界面中的视图元素适当下移方式来适配刘海屏,如果产品形态允许的话,你也可以让该界面显示状态栏啊。

至此刘海屏适配完毕,是不是很简单!?

最后代码奉上,拿走不谢:

允许全屏界面内容显示到刘海区域配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!--允许绘制到oppo、vivo刘海屏机型的刘海区域 -->
<meta-data
android:name="android.max_aspect"
android:value="2.2" />

<!-- 允许绘制到华为刘海屏机型的刘海区域 -->
<meta-data
android:name="android.notch_support"
android:value="true" />

<!-- 允许绘制到小米刘海屏机型的刘海区域 -->
<meta-data
android:name="notch.config"
android:value="portrait" />

上面在 AndroidManifest 的配置在 Android 9.0 之前有效,9.0 系统针对刘海屏制定了新的 api,默认保留一条黑边,即不允许绘制到刘海区域。所以如果你还没有适配 Android 9.0,那在判断是否是允许全屏界面内容显示到刘海区域的刘海屏机型时,就要加上版本判断。

判断是否是允许全屏界面内容显示到刘海区域的刘海屏机型:

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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
public class CutoutUtil {

private static Boolean sAllowDisplayToCutout;

/**
* 是否为允许全屏界面显示内容到刘海区域的刘海屏机型(与AndroidManifest中配置对应)
*/
public static boolean allowDisplayToCutout() {
if (sAllowDisplayToCutout == null) {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O_MR1) {
// 9.0系统全屏界面默认会保留黑边,不允许显示内容到刘海区域
return sAllowDisplayToCutoutDevice = false;
}
Context context = App.get();
if (hasCutout_Huawei(context)) {
return sAllowDisplayToCutout = true;
}
if (hasCutout_OPPO(context)) {
return sAllowDisplayToCutout = true;
}
if (hasCutout_VIVO(context)) {
return sAllowDisplayToCutout = true;
}
if (hasCutout_XIAOMI(context)) {
return sAllowDisplayToCutout = true;
}
return sAllowDisplayToCutout = false;
} else {
return sAllowDisplayToCutout;
}
}


/**
* 是否是华为刘海屏机型
*/
@SuppressWarnings("unchecked")
private static boolean hasCutout_Huawei(Context context) {
if (!Build.MANUFACTURER.equalsIgnoreCase("HUAWEI")) {
return false;
}
try {
ClassLoader cl = context.getClassLoader();
Class HwNotchSizeUtil = cl.loadClass("com.huawei.android.util.HwNotchSizeUtil");
if (HwNotchSizeUtil != null) {
Method get = HwNotchSizeUtil.getMethod("hasNotchInScreen");
return (boolean) get.invoke(HwNotchSizeUtil);
}
return false;
} catch (Exception e) {
return false;
}
}

/**
* 是否是oppo刘海屏机型
*/
@SuppressWarnings("unchecked")
private static boolean hasCutout_OPPO(Context context) {
if (!Build.MANUFACTURER.equalsIgnoreCase("oppo")) {
return false;
}
return context.getPackageManager().hasSystemFeature("com.oppo.feature.screen.heteromorphism");
}

/**
* 是否是vivo刘海屏机型
*/
@SuppressWarnings("unchecked")
private static boolean hasCutout_VIVO(Context context) {
if (!Build.MANUFACTURER.equalsIgnoreCase("vivo")) {
return false;
}
try {
ClassLoader cl = context.getClassLoader();
@SuppressLint("PrivateApi")
Class ftFeatureUtil = cl.loadClass("android.util.FtFeature");
if (ftFeatureUtil != null) {
Method get = ftFeatureUtil.getMethod("isFeatureSupport", int.class);
return (boolean) get.invoke(ftFeatureUtil, 0x00000020);
}
return false;
} catch (Exception e) {
return false;
}
}

/**
* 是否是小米刘海屏机型
*/
@SuppressWarnings("unchecked")
private static boolean hasCutout_XIAOMI(Context context) {
if (!Build.MANUFACTURER.equalsIgnoreCase("xiaomi")) {
return false;
}
try {
ClassLoader cl = context.getClassLoader();
@SuppressLint("PrivateApi")
Class SystemProperties = cl.loadClass("android.os.SystemProperties");
Class[] paramTypes = new Class[2];
paramTypes[0] = String.class;
paramTypes[1] = int.class;
Method getInt = SystemProperties.getMethod("getInt", paramTypes);
//参数
Object[] params = new Object[2];
params[0] = "ro.miui.notch";
params[1] = 0;
return (Integer) getInt.invoke(SystemProperties, params) == 1;
} catch (Exception e) {
return false;
}
}

}

上面提到,不一定要通过全屏界面中的视图元素适当下移方式来适配刘海屏,如果产品形态允许的话,也可以让该界面显示状态栏。

显示状态栏的方案是较为通用简单的,或者说,在一个应用中,一些全屏界面往往是允许使用显示状态栏的方案来适配的,如果你考虑使用这种方案,那便会是这种效果:

  • 在你的应用中,你期望某些全屏界面在刘海屏机型上必须全屏展示,那你就自行将界面元素适当下移,从而避免被刘海遮挡;而某些全屏界面不是非要全屏显示,允许在刘海屏机型显示状态栏,那就通过显示状态栏的方式,从而避免被刘海遮挡。

为了实现这种效果,我们需要标记区分哪些界面必须全屏展示、哪些界面允许显示状态栏。这里提供一种实现方式,让允许显示状态栏的界面 Activity 继承一个接口,比如:

1
2
public interface CutoutAdapt {
}

然后在 ActivityLifecycleCallbacks 回调,统一适配允许通过显示状态栏的全屏界面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public void onActivityStarted(Activity activity) {
// 如果是允许全屏显示到刘海屏区域的刘海屏机型
if (CutoutUtil.allowDisplayToCutout()) {
if (isFullScreen(activity)) {
// 如果允许通过显示状态栏方式适配刘海屏
if (activity instanceof CutoutAdapt) {
// 显示状态栏
StatusBarUtil.showStatusbar(activity.getWindow());
} else {
// 需自行将该界面视图元素下移,否则可能会被刘海遮挡
}
} else {
// 非全屏界面无需适配刘海屏
}
}
}