前言:作为安卓开发,也得承认苹果设计一直在引领潮流,所以自从 iPhone X 发布之后,”刘海屏” 就一直被热议,作为我自己必须先吐槽一下,真没觉得刘海屏好看。但是作为苦逼的开发者,还是要必须去适配刘海屏的,自然而然的在吐槽完毕以后还是有了这篇文章的诞生。
那么什么是刘海屏呢?
在手机屏幕的正上方居中位置(下图黑色区域)会被挖掉一个孔,屏幕被挖掉的区域无法正常显示内容,这种类型的屏幕就是刘海屏,也有其他叫法:挖孔屏、凹凸屏等等,这里统一按刘海屏命名。

今天主要是介绍一下Android P中刘海屏的适配以及Android P之前的适配。
为什么要分开呢?
因为Android P之前官方还没提供API来进行适配,都是由各家厂商来提供适配方案的。例如:华为 P20 就是采用的 Android P 标准 Api 的方式,而 vivo & OPPO 就不一样了,它有自己的适配 Api。
今天这篇作为对刘海的适配文章,先给大家看一张我找到的很直观的好图:

接下来就开始今天的适配内容啦,赶紧上车出发吧(Android P中的适配在最后面)。。。
华为适配请看下面
先看一张我拿人家的流程逻辑图片:

在上图可看到,华为系统对页面做偏移处理的有以下2种情况:
1.未设置meta-data值,页面横屏状态;
2.未设置meta-data值,页面竖屏状态,不显示状态栏;
下面是适配刘海屏主要的步骤:
1.配置meta-data :华为新增的Meta-data属性android.notch_support在应用的AndroidManifest.xml中增加meta-data属性,此属性不仅可以针对Application生效,也可以对Activity配置生效,具体方式如下所示:
<meta-data android:name="android.notch_support" android:value="true"/>
①对Application生效,意味着该应用的所有页面,系统都不会做竖屏场景的特殊下移或者是横屏场景的右移特殊处理:

② 对Activity生效,意味着可以针对单个页面进行刘海屏适配,设置了该属性的Activity系统将不会做特殊处理:

2.检测是否存在刘海屏请看下面的代码示例:
public static boolean hasNotchInScreen(Context context) {
boolean ret = false;
try {
ClassLoader cl = context.getClassLoader();
Class HwNotchSizeUtil = cl.loadClass("com.huawei.android.util.HwNotchSizeUtil");
Method get = HwNotchSizeUtil.getMethod("hasNotchInScreen");
ret = (boolean) get.invoke(HwNotchSizeUtil);
} catch (ClassNotFoundException e) {
Log.e("test", "hasNotchInScreen ClassNotFoundException");
} catch (NoSuchMethodException e) {
Log.e("test", "hasNotchInScreen NoSuchMethodException");
} catch (Exception e) {
Log.e("test", "hasNotchInScreen Exception");
} finally {
return ret;
}
}
3.获取刘海屏的参数请看下面的代码示例:
public static int[] getNotchSize(Context context) {
int[] ret = new int[]{0, 0};
try {
ClassLoader cl = context.getClassLoader();
Class HwNotchSizeUtil = cl.loadClass("com.huawei.android.util.HwNotchSizeUtil");
Method get = HwNotchSizeUtil.getMethod("getNotchSize");
ret = (int[]) get.invoke(HwNotchSizeUtil);
} catch (ClassNotFoundException e) {
Log.e("test", "getNotchSize ClassNotFoundException");
} catch (NoSuchMethodException e) {
Log.e("test", "getNotchSize NoSuchMethodException");
} catch (Exception e) {
Log.e("test", "getNotchSize Exception");
} finally {
return ret;
}
}
4. 进行UI适配 :
通过增加上面适配方案提到的配置(meta-data或者是Flag),应用在华为刘海屏手机上就能够默认使用刘海区显示了,但是为了避免出现UI被刘海区遮挡的问题,还是需要应用自己做一些额外的UI适配工作:
(1)判断是否刘海屏,通过华为刘海屏SDK的API判断,具体参考代码示例;
(2)如果是刘海屏手机需要应用自己调整布局避开刘海区,布局原则:保证重要的文字、图片和视频信息、可点击的控件和图标还有应用弹窗等等布局建议显示在状态栏区域以下(安全区域);不重要,遮挡不会出现问题的布局可以延伸到状态栏区域(危险区域)显示,按照这种布局原则修改,可以一次修改就能适配所有的刘海屏手机。

这里附带一个小小的知识点:(获取系统状态栏高度接口)
public static int getStatusBarHeight(Context context) {
int result = 0;
int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
result = context.getResources().getDimensionPixelSize(resourceId);
}
return result;
}
vivo适配请看下面
先补充一下:vivo在设置–显示与亮度–第三方应用显示比例中可以切换是否全屏显示还是安全区域显示。
1.检验是否有刘海屏请看下面代码:
public static final int VIVO_NOTCH = 0x00000020;//是否有刘海
public static final int VIVO_FILLET = 0x00000008;//是否有圆角
public static boolean hasNotchAtVivo(Context context) {
boolean ret = false;
try {
ClassLoader classLoader = context.getClassLoader();
Class FtFeature = classLoader.loadClass("android.util.FtFeature");
Method method = FtFeature.getMethod("isFeatureSupport", int.class);
ret = (boolean) method.invoke(FtFeature, VIVO_NOTCH);
} catch (ClassNotFoundException e) {
Log.e("Notch", "hasNotchAtVivo ClassNotFoundException");
} catch (NoSuchMethodException e) {
Log.e("Notch", "hasNotchAtVivo NoSuchMethodException");
} catch (Exception e) {
Log.e("Notch", "hasNotchAtVivo Exception");
} finally {
return ret;
}
}
2.获取刘海的尺寸:
可惜目前vivo不提供接口获取刘海尺寸,目前vivo的刘海宽为100dp,高为27dp。
OPPO适配请看下面
先补充一下:OPPO目前在设置 – 显示 – 应用全屏显示 – 凹形区域显示控制,里面有关闭凹形区域开关。
1.检验是否刘海屏:
public static boolean hasNotchAtOPPO(Context context) {
return context.getPackageManager().hasSystemFeature("com.oppo.feature.screen.heteromorphism");
}
2.获取刘海的尺寸:
OPPO不提供接口获取刘海尺寸,目前其有刘海屏的机型尺寸规格都是统一的。不排除以后机型会有变化。 其显示屏宽度为1080px,高度为2280px。刘海区域则都是宽度为324px, 高度为80px。
小米的适配请看下面
小米手机系统增加了 property ro.miui.notch,值为1时则是 Notch 屏手机。在小米8上有刘海效果。小米的状态栏高度会略高于刘海屏的高度,因此可以通过获取状态栏的高度来间接避开刘海屏。
其他手机也可以通过这个方法来间接避开刘海屏,但是有可能有些手机的刘海屏高度会高于状态栏的高度,所以这个方法获取到的结果并不一定安全
Android P中的刘海屏适配请往下看!!!
google从Android P开始为刘海屏提供支持,Android P 支持最新的全面屏以及为摄像头和扬声器预留空间的凹口屏幕。
目前提供了一个类和三种模式:
通过全新的 DisplayCutout 类,可以确定非功能区域的位置和形状,这些区域不应显示内容。 要确定这些凹口屏幕区域是否存在及其位置,请使用 getDisplayCutout() 方法。
DisplayCutout cutout = mContext.getDisplayCutout();
Google官方提供了三种模式,分别是:
- LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT:仅仅当系统提供的bar完全包含了刘海区时才允许window扩展到刘海区,否则window不会和刘海区重叠;
- LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES:允许window扩展到刘海区(原文说的是短边的刘海区, 目前有刘海的手机都在短边,所以就不纠结了);
-
LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER:不允许window扩展到刘海区。
举例,下面是可以设置是否允许window扩展到刘海区的代码:
WindowManager.LayoutParams lp =getWindow().getAttributes();
lp.layoutInDisplayCutoutMode=WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVE;
getWindow().setAttributes(lp);
你可以按如下方法在任何运行 Android P 的设备或模拟器上模拟屏幕缺口:
- 启用开发者选项。
- 在 Developer options(开发者选项) 屏幕中,向下滚动至 Drawing(绘图) 部分并选择 Simulate a display with a cutout(模拟具有凹口的显示屏)。
- 选择凹口屏幕的大小。

Android P中支持的凹口屏幕类型
目前Android支持了三类凹口屏幕类型:边角显示屏凹口(斜刘海)、双显示屏凹口(刘海+胡子)、长型显示屏凹口(刘海)。
目前的手机主要还是长型显示屏凹口,即刘海屏。
下面的几张图是对设置不同的模式的效果图展示:



Android P提供提供的刘海屏适配方案
- 对于有状态栏的页面,不会受到刘海屏特性的影响,因为刘海屏包含在状态栏中了;
- 全屏显示的页面,系统刘海屏方案会对应用界面做下移处理,避开刘海区显示,这时会看到刘海区域变成一条黑边,完全看不到刘海了;
- 已经适配Android P应用的全屏页面可以通过谷歌提供的适配方案使用刘海区,真正做到全屏显示。
Android P中凹口屏幕相关接口
友情提示:以下接口都是要Build.VERSION.SDK_INT >= 28才能调用到。
DisplayCutout类接口:主要用于获取凹口位置和安全区域的位置等。
| 方法 | 接口说明 |
|---|---|
| getBoundingRects() | 返回Rects的列表,每个Rects都是显示屏上非功能区域的边界矩形。 |
| getSafeInsetLeft () | 返回安全区域距离屏幕左边的距离,单位是px。 |
| getSafeInsetRight () | 返回安全区域距离屏幕右边的距离,单位是px。 |
| getSafeInsetTop () | 返回安全区域距离屏幕顶部的距离,单位是px。 |
| getSafeInsetBottom() | 返回安全区域距离屏幕底部的距离,单位是px。 |
示例:下面代码是将开发者选项中的模拟具有凹口的显示屏选项改为双显示屏凹口,即这里应当有两个刘海。
public class NotchActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//开局就一张背景图
setContentView(R.layout.notch);
getNotchParams();
}
@TargetApi(28)
public void getNotchParams() {
final View decorView = getWindow().getDecorView();
decorView.post(new Runnable() {
@Override
public void run() {
DisplayCutout displayCutout = decorView.getRootWindowInsets().getDisplayCutout();
Log.e("TAG", "安全区域距离屏幕左边的距离 SafeInsetLeft:" + displayCutout.getSafeInsetLeft());
Log.e("TAG", "安全区域距离屏幕右部的距离 SafeInsetRight:" + displayCutout.getSafeInsetRight());
Log.e("TAG", "安全区域距离屏幕顶部的距离 SafeInsetTop:" + displayCutout.getSafeInsetTop());
Log.e("TAG", "安全区域距离屏幕底部的距离 SafeInsetBottom:" + displayCutout.getSafeInsetBottom());
List<Rect> rects = displayCutout.getBoundingRects();
if (rects == null || rects.size() == 0) {
Log.e("TAG", "不是刘海屏");
} else {
Log.e("TAG", "刘海屏数量:" + rects.size());
for (Rect rect : rects) {
Log.e("TAG", "刘海屏区域:" + rect);
}
}
}
});
}
}
该测试输出结果为:
06-04 21:57:10.120 5698-5698/? E/TAG: 安全区域距离屏幕左边的距离 SafeInsetLeft:0
06-04 21:57:10.120 5698-5698/? E/TAG: 安全区域距离屏幕右部的距离 SafeInsetRight:0
06-04 21:57:10.120 5698-5698/? E/TAG: 安全区域距离屏幕顶部的距离 SafeInsetTop:112
06-04 21:57:10.120 5698-5698/? E/TAG: 安全区域距离屏幕底部的距离 SafeInsetBottom:112
06-04 21:57:10.120 5698-5698/? E/TAG: 刘海屏数量:2
06-04 21:57:10.120 5698-5698/? E/TAG: 刘海屏区域:Rect(468, 0 - 972, 112)
06-04 21:57:10.120 5698-5698/? E/TAG: 刘海屏区域:Rect(468, 2448 - 972, 2560)
根据 测试结果可以看到,即距离顶部和底部各112px的区域就是安全区域了。
设置凹口屏幕显示模式
Android P中新增了一个布局参数属性layoutInDisplayCutoutMode,包含了三种不同的模式,如下所示:
| 模式 | 模式说明 |
|---|---|
| LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT | 只有当DisplayCutout完全包含在系统栏中时,才允许窗口延伸到DisplayCutout区域。 否则,窗口布局不与DisplayCutout区域重叠。 |
| LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER | 该窗口决不允许与DisplayCutout区域重叠。 |
| LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES | 该窗口始终允许延伸到屏幕短边上的DisplayCutout区域。 |
下面的代码示例看下这三种模式的显示效果:
public class NotchActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//去掉标题
requestWindowFeature(Window.FEATURE_NO_TITLE);
//开局就一张背景图
setContentView(R.layout.notch);
//全屏显示
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
WindowManager.LayoutParams lp = getWindow().getAttributes();
//下面图1
lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
//下面图2
// lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
//下面图3
// lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
getWindow().setAttributes(lp);
}
}
图一:可以看到上面有黑边。

图二:可以看到有刘海。

图三:同样是黑边。

根据效果可以验证结论:
LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES模式会让屏幕到延申刘海区域中。LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER模式不会让屏幕到延申刘海区域中,会留出一片黑色区域。LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT模式在全屏显示下跟LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER一样。
接下来我们再来看看LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT模式在沉浸式状态栏下的效果,代码如下:
public class NotchActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//去掉标题
requestWindowFeature(Window.FEATURE_NO_TITLE);
//开局就一张背景图
setContentView(R.layout.notch);
//全屏显示
// getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
//沉浸式状态栏
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
WindowManager.LayoutParams lp = getWindow().getAttributes();
lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
getWindow().setAttributes(lp);
}
}
效果如下图所示:

所以:当刘海区域完全在系统的状态栏时,
LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT的显示效果与LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES一致。
最后, 那么刘海屏该如何适配呢?
1.如果页面存在状态栏:
- 那么很简单,不用适配,因为刘海区域会包含在状态栏中了。
- 如果不想看到刘海区域,可以使用
LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER将刘海区域变成一条黑色边。
2. 如果页面是全屏显示:
- 不适配的话将会留出一条黑色边。
- 要做到真正全屏的话,那么就先要获取到刘海的区域(危险区域),内容部分(操作按钮等)应当避开危险区域,保证在安全区域中展示。横屏的话两边都需要注意避开刘海(危险区域)。
好了,虽然对于刘海我是吐槽的,但是文章还是可以直接让需要的你使用的,see you
本文详细介绍了Android P中刘海屏的适配方法,包括华为、vivo、OPPO、小米的适配步骤,以及Android P提供的凹口屏幕支持和适配方案。适配关键涉及设置meta-data、检测刘海屏存在、获取刘海尺寸和UI布局调整。
305





