背景
刘海屏指的是手机屏幕正上方由于追求极致边框而采用的一种手机解决方案。因形似刘海儿而得名。也有一些其他叫法:挖孔屏、凹口屏等,统一命名为刘海屏。随着 Apple 发布 iPhone X 之后,各大手机厂商也开始模仿这种刘海屏的设计,而且市场上已经有越来越多的手机都支持这种屏幕形式。
现在市场上的情况来说,“刘海屏”主要分成两类:
- 一类是标准的 Android P Api。例如:华为 P20 就是采用的 Android P 标准 Api 的方式
- 一类就是厂商在 Android P 以下的系统,做的特殊适配。 例如:OPPO R15 它有自己的适配 Api。
UI显示问题
刘海屏手机因为比平常的手机多了一块顶部的遮挡性刘海,所以会造成顶部 Toolbar 以及搜索框的遮挡,而且有些厂商的手机(vivo、华为),默认是在「无状态栏」的界面将状态栏进行黑化显示,这时候会导致系统下移,从而导致底部的一些 UI 被截断。除此之外,一些控件的显示规则还会受到影响,如 PopupWindow 的显示高度会在「无状态栏」的界面中比普通手机低一个「刘海的高度」,从而遮挡住原先在 PopupWindow 周围的图标。
1. 系统下移方案导致布局问题(截断、错乱,按钮热区错位)
2. 状态栏高度写死问题
3. 沉浸式布局遮挡问题
4. PopupWindow 显示异常
适配方案
Android P版本提供了统一的刘海屏方案和三方适配刘海屏方案:
- 对于有状态栏的页面,不会受到刘海屏特性的影响;
- 全屏显示的页面,系统刘海屏方案会对应用界面做下移处理,避开刘海区显示;
- 已经适配Android P应用的全屏页面可以通过谷歌提供的适配方案使用刘海区,真正做到全屏显示。
刘海屏的适配流程
其中需要着重处理的是:
- 应用是否已经适配刘海屏
- 页面是否显示状态栏
应用是否已经适配刘海屏
现在国内的主流机型(华为、vivo、OPPO、小米)在刘海屏的显示上分为两个阵营:
- 当不显示状态栏时,直接将界面进行显示,「状态栏原先的位置也用于显示界面」,例如:OPPO
- 当不显示状态栏时,直接「将状态栏原先的位置进行黑化,界面整体下移」,例如:华为、vivo
所以,我们在进行刘海屏适配的时候,首先需要通过一些手段,统一各大厂商的显示方案,让所有的刘海屏手机都利用状态栏的界面,「告知系统」我们已经适配了刘海屏,确保系统不会下移我们的应用,保留原生体验。
这里主要有两种方式:
1. 设置屏幕高宽比例
因为刘海屏手机的「宽高比」比之前的手机大,如果不适配的话,Android 默认为最大的宽高比为 1.86, 小于刘海屏手机的宽高比,因此我们需要申明更高的宽高比来告诉系统,我们应用已经适配了刘海屏。
只要在 AndroidManifest.xml 中加入如下配置:
<meta-data android:name="android.max_aspect" android:value="2.1"/>
也可以在 Application 添加属性:
android:maxAspectRatio="ratio_float"
ps:这个属性需要 API 26 才支持
2. 设置应用支持 resize
我们还可以通过设置应用支持 resizeable,来告诉系统我们适配了刘海屏,而且这也是 Google 官方推荐的方式。不过需要注意的是,使用这个属性之后,应用也会跟着支持分屏模式。只需要在 AndroidManifest.xml 中添加:
android:resizeableActivity="true"
页面是否显示状态栏
对于刘海屏适配,我们将界面分为两种:
- 对于有状态栏的界面,不会受到刘海屏的影响
- 全屏显示的界面(无状态栏),需要根据界面的显示进行一些控件的下移
因此,我们进行刘海屏适配,其实针对的就是没有状态栏的界面,而有状态栏的界面显示是正常的。对于没有状态栏的界面,主要是将对被刘海遮挡到的控件,设置对应刘海高度的 MarginTop,从而避免控件被遮挡。而对于底部可能被截断的界面,可以考虑将底部做成 ScrollView 的形式。
Google给的刘海屏适配方案支持
下面的内容摘自:https://developer.android.com/preview/features#cutout
Android P 支持最新的全面屏以及为摄像头和扬声器预留空间的凹口屏幕。 通过全新的 DisplayCutout 类,可以确定非功能区域的位置和形状,这些区域不应显示内容。 要确定这些凹口屏幕区域是否存在及其位置,请使用 getDisplayCutout() 函数。
- 全新的窗口布局属性 layoutInDisplayCutoutMode 让您的应用可以为设备凹口屏幕周围的内容进行布局。 您可以将此属性设为下列值之一:
(1)LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
(2)LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
(3)LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
- 您可以按如下方法在任何运行 Android P 的设备或模拟器上模拟屏幕缺口:
(1)启用开发者选项。
(2)在 Developer options 屏幕中,向下滚动至 Drawing 部分并选择 Simulate a display with a cutout。
(3)选择凹口屏幕的大小。
注意:
我们建议您通过使用运行 Android P 的设备或模拟器测试凹口屏幕周围的内容显示。
接口介绍
- 获取刘海尺寸相关接口:
https://developer.android.com/reference/android/view/DisplayCutout
所属类 | 方法 | 接口说明 |
---|---|---|
android.view.DisplayCutout | List<Rect> getBoundingRects() | 返回Rects的列表,每个Rects都是显示屏上非功能区域的边界矩形。设备的每个短边最多只有一个非功能区域,而长边上则没有。 |
android.view.DisplayCutout | int getSafeInsetBottom() | 返回安全区域距离屏幕底部的距离,单位是px。 |
android.view.DisplayCutout | int getSafeInsetLeft () | 返回安全区域距离屏幕左边的距离,单位是px。 |
android.view.DisplayCutout | int getSafeInsetRight () | 返回安全区域距离屏幕右边的距离,单位是px。 |
android.view.DisplayCutout | int getSafeInsetTop () | 返回安全区域距离屏幕顶部的距离,单位是px。 |
- 设置是否延伸到刘海区显示接口:
类 | 属性 | 属性说明 |
---|---|---|
android.view.WindowManager.LayoutParams | int layoutInDisplayCutoutMode | 默认值: LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT 其他可能取值: LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER |
类 | 常量 | 常量说明 |
---|---|---|
android.view.WindowManager.LayoutParams | int LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT | 只有当DisplayCutout完全包含在系统状态栏中时,才允许窗口延伸到DisplayCutout区域显示。 |
android.view.WindowManager.LayoutParams | int LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER | 该窗口决不允许与DisplayCutout区域重叠。 |
android.view.WindowManager.LayoutParams | int LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES | 该窗口始终允许延伸到屏幕短边上的DisplayCutout区域。 |
参考实现代码
- 设置使用刘海区显示代码:
getSupportActionBar().hide();
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
//设置页面全屏显示
WindowManager.LayoutParams lp = getWindow().getAttributes();
lp.layoutInDisplayCutoutMode =
windowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
//设置页面延伸到刘海区显示
getWindow().setAttributes(lp);
注意:如果需要应用的布局延伸到刘海区显示,需要设置SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN。
属性:
- 获取刘海屏安全显示区域和刘海尺寸信息:
contentView = getWindow().getDecorView().findViewById(android.R.id.content).getRootView();
contentView.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
@Override
public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) {
DisplayCutout cutout = windowInsets.getDisplayCutout();
if (cutout == null) {
Log.e(TAG, "cutout==null, is not notch screen");//通过cutout是否为null判断是否刘海屏手机
} else {
List<Rect> rects = cutout.getBoundingRects();
if (rects == null || rects.size() == 0) {
Log.e(TAG, "rects==null || rects.size()==0, is not notch screen");
} else {
Log.e(TAG, "rect size:" + rects.size());//注意:刘海的数量可以是多个
for (Rect rect : rects) {
Log.e(TAG, "cutout.getSafeInsetTop():" + cutout.getSafeInsetTop()
+ ", cutout.getSafeInsetBottom():" + cutout.getSafeInsetBottom()
+ ", cutout.getSafeInsetLeft():" + cutout.getSafeInsetLeft()
+ ", cutout.getSafeInsetRight():" + cutout.getSafeInsetRight()
+ ", cutout.rects:" + rect
);
}
}
}
return windowInsets;
}
});
- 说明:
(1)通过windowInsets.getDisplayCutout()是否为null判断是否刘海屏手机,如果为null为非刘海屏:
Line 6203: 05-24 11:16:46.766 11036 11036 E Cutout_test: cutout==null, is not notch screen
(2)如果是刘海屏手机可以通过接口获取刘海信息:
Line 6211: 05-24 11:11:16.839 10733 10733 E Cutout_test: cutout.getSafeInsetTop():126, cutout.getSafeInsetBottom():0,
cutout.getSafeInsetLeft():0, cutout.getSafeInsetRight():0, cutout.rects:Rect(414, 0 - 666, 126)
(3)刘海个数可以是多个:
Line 6291: 05-24 11:27:04.517 11036 11036 E Cutout_test: rect size:2
Line 6292: 05-24 11:27:04.517 11036 11036 E Cutout_test: cutout.getSafeInsetTop():84,
cutout.getSafeInsetBottom():84, cutout.getSafeInsetLeft():0, cutout.getSafeInsetRight():0, cutout.rects:Rect(351, 0 - 729, 84)
Line 6293: 05-24 11:27:04.517 11036 11036 E Cutout_test: cutout.getSafeInsetTop():84,
cutout.getSafeInsetBottom():84, cutout.getSafeInsetLeft():0, cutout.getSafeInsetRight():0, cutout.rects:Rect(351, 1836 - 729, 1920)
各厂商适配方案
现在 Android P 的接口还没法用,但各手机厂商都制定了自己的 API,对此我们需要对各大机型进行特殊的适配,这里主要介绍 vivo、OPPO、华为 这三种主流手机的适配方案。
华为:
以下来自:华为刘海屏适配官方技术指导
1.背景
谷歌在安卓P版本中已经提供了统一的适配方案,可是在安卓O版本上如何适配呢?本文将详细介绍华为安卓O版本刘海屏适配方案。使用华为提供的刘海屏SDK进行适配,此方案也会继承到华为安卓P版本手机上。在华为P版本手机中将同时支持两种方案:华为O版本方案+谷歌P版本方案。另外因为安卓O版本的刘海屏手机已经在市场上大量上市,这些手机在市场上会存续2~3年。所以建议大家现在要同时适配华为O版本方案以及谷歌P版本方案。
2. 华为刘海屏手机安卓O版本适配方案
设计理念:尽量减少APP的开发工作量
处理逻辑:
3 华为刘海屏API接口
3.1 判断是否刘海屏
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.2 获取刘海尺寸
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;
}
}
3.3 应用页面设置使用刘海区显示
3.3.1 方案一
使用新增的Meta-data属性android.notch_support,在应用的AndroidManifest.xml中增加meta-data属性,此属性不仅可以针对Application生效,也可以对Activity配置生效。
- 具体方式如下所示:
<meta-data android:name="android.notch_support" android:value="true"/>
- 对Application生效,意味着该应用的所有页面,系统都不会做竖屏场景的特殊下移或者是横屏场景的右移特殊处理:
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:testOnly="false"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<meta-data android:name="android.notch_support" android:value="true"/>
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
- 对Activity生效,意味着可以针对单个页面进行刘海屏适配,设置了该属性的Activity系统将不会做特殊处理:
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:testOnly="false"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name=".LandscapeFullScreenActivity" android:screenOrientation="sensor">
</activity>
<activity android:name=".FullScreenActivity">
<meta-data android:name="android.notch_support" android:value="true"/>
</activity>
3.3.2 方案二
使用给window添加新增的FLAG_NOTCH_SUPPORT
- 应用通过增加华为自定义的刘海屏flag,请求使用刘海区显示:
对Application生效,意味着该应用的所有页面,系统都不会做竖屏场景的特殊下移或者是横屏场景的右移特殊处理:
/*刘海屏全屏显示FLAG*/
public static final int FLAG_NOTCH_SUPPORT=0x00010000;
/**
* 设置应用窗口在华为刘海屏手机使用刘海区
* @param window 应用页面window对象
*/
public static void setFullScreenWindowLayoutInDisplayCutout(Window window) {
if (window == null) {
return;
}
WindowManager.LayoutParams layoutParams = window.getAttributes();
try {
Class layoutParamsExCls = Class.forName("com.huawei.android.view.LayoutParamsEx");
Constructor con=layoutParamsExCls.getConstructor(LayoutParams.class);
Object layoutParamsExObj=con.newInstance(layoutParams);
Method method=layoutParamsExCls.getMethod("addHwFlags", int.class);
method.invoke(layoutParamsExObj, FLAG_NOTCH_SUPPORT);
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException |InstantiationException
| InvocationTargetException e) {
Log.e("test", "hw add notch screen flag api error");
} catch (Exception e) {
Log.e("test", "other Exception");
}
}
- 可以通过clearHwFlags接口清除添加的华为刘海屏Flag,恢复应用不使用刘海区显示。
/*刘海屏全屏显示FLAG*/
public static final int FLAG_NOTCH_SUPPORT=0x00010000;
/**
* 设置应用窗口在华为刘海屏手机使用刘海区
* @param window 应用页面window对象
*/
public static void setNotFullScreenWindowLayoutInDisplayCutout (Window window) {
if (window == null) {
return;
}
WindowManager.LayoutParams layoutParams = window.getAttributes();
try {
Class layoutParamsExCls = Class.forName("com.huawei.android.view.LayoutParamsEx");
Constructor con=layoutParamsExCls.getConstructor(LayoutParams.class);
Object layoutParamsExObj=con.newInstance(layoutParams);
Method method=layoutParamsExCls.getMethod("clearHwFlags", int.class);
method.invoke(layoutParamsExObj, FLAG_NOTCH_SUPPORT);
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException |InstantiationException
| InvocationTargetException e) {
Log.e("test", "hw clear notch screen flag api error");
} catch (Exception e) {
Log.e("test", "other Exception");
}
}
- 华为刘海屏flag动态添加和删除代码:
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if(isAdd) {//add flag
isAdd = false;
NotchSizeUtil.setFullScreenWindowLayoutInDisplayCutout(getWindow());
getWindowManager().updateViewLayout(getWindow().getDecorView(),getWindow().getDecorView().getLayoutParams());
} else{//clear flag
isAdd = true;
NotchSizeUtil.setNotFullScreenWindowLayoutInDisplayCutout(getWindow());
getWindowManager().updateViewLayout(getWindow().getDecorView(),getWindow().getDecorView().getLayoutParams());
}
}
});
3.4 获取默认和隐藏刘海区开关值接口
隐藏开关打开之后,显示规格
读取开关状态调用范例
public static final String DISPLAY_NOTCH_STATUS = "display_notch_status";
// 0表示“默认”,1表示“隐藏显示区域”
int mIsNotchSwitchOpen = Settings.Secure.getInt(getContentResolver(),DISPLAY_NOTCH_STATUS, 0);
详细完整的文档介绍请查看华为刘海屏手机安卓O版本适配指导
OPPO:
根据OPPO的凹型屏适配说明
如何适配全面屏手机
根据谷歌兼容性(CTS)标准要求,应用必须按以下方式中的任意一种,在AndroidManifest.xml中配置方可全屏显示,否则将以非全屏显示。
方式一:配置支持最大高宽比
* <meta-data android:name="android.max_aspect" android:value="ratio_float" />
* android:maxAspectRatio="ratio_float" (API LEVEL 26)
说明:以上两种接口可以二选一,ratio_float = 屏幕高 / 屏幕宽 (如oppo新机型屏幕分辨率为2280 x 1080, ratio_float = 2280 / 1080 = 2.11,建议设置 ratio_float为2.2或者更大)
方式二:支持分屏,注意验证分屏下界面兼容性
android:resizeableActivity="true"
建议采用方式二适配支持全面屏。
详见官方文档:https://source.android.google.cn/compatibility/cdd?hl=zh-cn
1. 判断该 OPPO 手机是否为刘海屏手机
public static boolean hasNotchInOppo(Context context) {
return context.getPackageManager().hasSystemFeature("com.oppo.feature.screen.heteromorphism");
}
2. 获取刘海屏的高度
对于 OPPO 刘海屏手机的刘海高度,OPPO 官方的文档没有提供相关的 API,但官方文档表示 OPPO 手机的刘海高度和状态栏的高度是一致的,而且我也对此进行了验证,确实如此。所以我们可以直接获取状态栏的高度,作为 OPPO 手机的刘海高度。
public static int getStatusBarHeight(Context context) {
int statusBarHeight = 0;
int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
statusBarHeight = context.getResources().getDimensionPixelSize(resourceId);
}
return statusBarHeight ;
}
获取刘海参数
private int[] getNotchSizeFromSystemProperties(){
String mNotchSize = SystemProperties.get("ro.oppo.screen.heteromorphism");
if(TextUtils.isEmpty(mNotchSize)) return null;
try {
String[] split = mNotchSize.replace("[", "").replace("]", "").split(":");
if(split.length == 2){
String[] splitLeftTop = split[0].split(",");
String[] splitRightBottom = split[1].split(",");
int[] pointLeftTop = new int[]{Integer.parseInt(splitLeftTop[0]), Integer.parseInt(splitLeftTop[1])};
int[] pointRightBottom = new int[]{Integer.parseInt(splitRightBottom[0]), Integer.parseInt(splitRightBottom[1])};
int notchWidth = pointRightBottom[0] - pointLeftTop[0];
int notchHeight = pointRightBottom[1] - pointLeftTop[1];
return new int[]{notchWidth, notchHeight};
}
}catch (Exception e){
LogUtil.e(DeviceUtils.getManufacturer() + " getNotchSizeFromSystemProperties: " + e.getMessage());
}
return null;
}
vivo
下文来自vivo给出的刘海屏适配指导
如何适配
■ 怎么判断是否刘海支持与否?
View.getRootWindowInsets().getDisplayCutout()
返回DisplayCutout对象,为刘海的描述信息,不支持则返回空
■ 怎么获取刘海信息?
DisplayCutout.getSafeInsets() 返回安全区域Rect
DisplayCutout.getBounds() 返回刘海的区域的Region
■ 如何控制应用的布局显示?
通过控制窗口布局的layoutInDisplayCutoutMode属性可以达到控制应用在刘海屏下布局区域,具体可以设置以下值
LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
没有设置的情况下默认此标记,只有状态栏区域可见的情况下,才意味着加了FLAG_FULLSCREEN or SYSTEM_UI_FLAG_FULLSCREEN标记的应用窗口都会被切边放在刘海以下
LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
此flag下不进行应用窗口的限制,窗口可以显示到刘海区域
LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
此flag下应用窗口一直被限制在刘海区域以下
应用也可以通过theme来达到同样的效果
<item name="android:windowLayoutInDisplayCutoutMode">default</item>
对应LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
对应LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
<item name="android:windowLayoutInDisplayCutoutMode">never</item>
对应LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
应用遇到窗口显示被限制在刘海以下区域的可以调整
layoutInDisplayCutoutMode 设置为LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES,再通过获取DisplayCutout 关于刘海区域信息,调整UI达到更好的满屏体验。
vivo的判断方法:
protected boolean isHardwareNotchEnable(Activity activity) {
boolean notchEnable = false;
try {
ClassLoader classLoader = activity.getClassLoader();
Class FtFeature = classLoader.loadClass("android.util.FtFeature");
Method method = FtFeature.getMethod("isFeatureSupport", int.class);
notchEnable = (boolean) method.invoke(FtFeature, VIVO_NOTCH) | (boolean) method.invoke(FtFeature, VIVO_FILLET);
} catch (ClassNotFoundException e) {
LogUtil.e("hasNotchAtVivo ClassNotFoundException");
} catch (NoSuchMethodException e) {
LogUtil.e( "hasNotchAtVivo NoSuchMethodException");
} catch (Exception e) {
LogUtil.e("hasNotchAtVivo Exception");
} finally {
LogUtil.i("Vivo hardware enable: "+notchEnable);
return notchEnable;
}
}
参考文章: