1、Android8.0以上的适配说明
主要说明国内外几大主流产商:小米、oppo、vivo、华为、三星、Google。
小米 MIUI Notch 屏适配说明
Notch 机型在界面上会带来两个问题:
- 顶部内容会被 Notch 遮挡
- 如何处理耳朵区的显示区域
MIUI对于Android O 和 Android P的适配存在差异,尽管小米表示对这种现象的存在表示抱歉,但是由于Android P发布时间短,还没有办法做好兼容。在存在差异的地方会额外标明。
如何判断设备为 Notch 机型
系统增加了 property ro.miui.notch,值为1时则是 Notch 屏手机。
SystemProperties.getInt("ro.miui.notch", 0) == 1;
差异
Android O
如何获取 Notch / 凹口 / 刘海 的高度和宽度(截至2018.6.26)
MIUI 10 新增了获取刘海宽和高的方法,需升级至8.6.26开发版及以上版本。
以下是Android O获取当前设备刘海高度的方法:
int resourceId = context.getResources().getIdentifier("notch_height", "dimen", "android");
if (resourceId > 0) {
result = context.getResources().getDimensionPixelSize(resourceId);
}
注意上述获取宽高的方法仅限于2018.6.26的以上的版本,那么如何判断小米的MIUI 的版本 ?
/**
* 判断是否大于 1529942400 ,2018.6.26 转为毫秒是 1529942400 ,MIUI10 的版本Version codetime
* @return
*/
public boolean isGreaterMIUI10() {
return (Long.parseLong(checkMIUI()) >= 1529942400);
}
public static String checkMIUI() {
String versionCode = "";
String manufacturer = Build.MANUFACTURER;
String model = Build.MODEL;
Logger.d(TAG,"Build.MANUFACTURER = " + manufacturer + " ,Build.MODEL = " + Build.MODEL);
if (!TextUtils.isEmpty(manufacturer) && manufacturer.equals("Xiaomi")) {
versionCode = getSystemProperty("ro.miui.version.code_time");
}
return versionCode;
}
2018.6.26的以下的版本参考1.3.
MIUI对刘海的控制分为两种:
Application 级别的控制
如果开发者认为应用的所有页面统一处理就行,可以使用该接口。在 Application 下增加一个 meta-data,用以声明该应用是否使用耳朵区
<meta-data
android:name="notch.config"
android:value="portrait|landscape"/>
//其中,value 的取值可以是以下4种:
"none" 横竖屏都不绘制耳朵区
"portrait" 竖屏绘制到耳朵区
"landscape" 横屏绘制到耳朵区
"portrait|landscape" 横竖屏都绘制到耳朵区
不过需要注意的是一旦开发者声明了meta-data,系统就会优先遵从开发者的声明。
Window 级别的控制
如果开发者希望对特定 Window 作处理,可以使用该接口。 在 WindowManager.LayoutParams 增加 extraFlags 成员变量,用以声明该 window 是否使用耳朵区。
/** extraFlags 有以下变量
* 0x00000100 开启配置
* 0x00000200 竖屏配置
* 0x00000400 横屏配置
* 组合后表示 Window 的配置
* 0x00000100 | 0x00000200 竖屏绘制到耳朵区
* 0x00000100 | 0x00000400 横屏绘制到耳朵区
* 0x00000100 | 0x00000200 | 0x00000400 横竖屏都绘制到耳朵区
*/
int flag = 0x00000100 | 0x00000200 | 0x00000400;
try {
Method method = Window.class.getMethod("addExtraFlags",
int.class);
method.invoke(getWindow(), flag);
} catch (Exception e) {
Log.i(TAG, "addExtraFlags not found.");
}
控制 extraFlags 时注意只控制这几位,不要影响其他位。可以用 Window 的 addExtraFlags 和 clearExtraFlags 来修改, 这两个方法是 MIUI 增加的方法,需要反射调用。
Android P
提供了 3 种显示模式供开发者选择,分别是:
- 默认模式(LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT)
- 刘海区绘制模式( LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES)
- 刘海区不绘制模式(LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER)
如果开发者未作任何声明,则会按默认模式处理。以下将具体介绍这三种模式的表现。
由于各个厂商的刘海或者凹口形状、位置不一, 开发者可以通过 WindowInsets.getDisplayCutout() 来获得 DisplayCutout object,里面包含了几个有用的方法:
- getBoundingRects():获取刘海 / Cutout 所在的矩形区域的位置,多个刘海则返回多个区域(单位:像素)。
- getSafeInsetLeft() / getSafeInsetTop() / getSafeInsetRight() / getSafeInsetBottom() :返回安全区上下左右的偏移值(单位:像素)。
开发者根据业务内容,自行判断是否需要根据不同的刘海形状做不同的布局调整。以小米8(刘海高度89px)为例,当开发者选用 SHORT_EDGES 模式时,以上接口会返回以下值:
| 竖屏 | 横屏(刘海在左边) |
(201, 0 - 879, 90) | (0, 201 - 90, 879) | |
0 | 90 | |
90 | 0 | |
0 | 0 | |
0 | 0 |
上述接口的返回值代表:
- 小米8有一个刘海,竖屏时,这个刘海所在的矩形区域的左上角、右下角的坐标分别为 (201, 0) 和 (879, 90) —— 左上角为 (0, 0) 原点;横屏时,这个刘海所在的矩形区域的左上角、右下角的坐标分别为 (0, 201) 和 (90, 879) —— 左上角为 (0, 0) 原点。
- 对于小米8,如果开发者需要将内容避开刘海区域,竖屏时就需要从顶部向下偏移 90 px,左、右和下无需要偏移。
原生 Android P 的规则和 MIUI Android O 的规则有什么区别
双方在默认模式下的表现是完全一致的,区别主要体现在:
- Android P 能通过 DisplayCutout object 获取刘海 / Notch / Cutout 的具体信息,但 MIUI Android O 只能获取刘海的高宽信息。
- Android P 不能控制仅竖屏(或横屏)使用耳朵区,但 MIUI Android O 可以分别配置横竖屏对耳朵区的使用策略。
共同
以下是获取当前设备状态栏高度的方法:
int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
result = context.getResources().getDimensionPixelSize(resourceId);
}
Oppo 凹形屏适配说明
简单粗暴的OPPO直接提供方法context.getPackageManager().hasSystemFeature(“com.oppo.feature.screen.heteromorphism”),返回 true为凹形屏 ,可识别OPPO的手机是否为凹形屏。
publicstaticbooleanhasNotchOPPO(Context context){
return context.getPackageManager().hasSystemFeature("com.oppo.feature.screen.heteromorphism");
}
如何获取 Notch / 凹口 / 刘海 的高度和宽度
OPPO不提供接口获取刘海尺寸,目前其有刘海屏的机型尺寸规格都是统一的。
采用宽度为1080px, 高度为2280px的圆弧显示屏。 屏幕顶部凹形区域不能显示内容,宽度为324px, 高度为80px。
如何适配全面屏手机
根据谷歌兼容性(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或者更大)
VIVO 全面屏应用适配指南
这里可以提前说一下,VIVO适配全面屏手机的方法与OPPO一致
如何判断是刘海屏机型
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;
}
}
华为 刘海屏手机安卓O版本适配指导
在我看来华为是最为拥抱Android原生的,华为的文档是最为标准的,更为贴近Android P官方提供的方案。
如何判断是否有刘海屏
文件 | 接口 | 接口说明 |
com.huawei.android.util.HwNotchSizeUtil | public static boolean hasNotchInScreen() | 是否是刘海屏手机: |
调用范例
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;
}
}
获取刘海尺寸
类文件 | 接口 | 接口说明 |
com.huawei.android.util.HwNotchSizeUtil | public static int[] getNotchSize() | 获取刘海尺寸:width、height int[0]值为刘海宽度 int[1]值为刘海高度。 |
调用范例
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;
}
}
应用页面设置使用刘海区显示
方案一
使用新增的Meta-data属性android.notch_support,在应用的AndroidManifest.xml中增加meta-data属性,此属性不仅可以针对Application生效,也可以对Activity配置生效。
对Application生效,意味着该应用的所有页面,系统都不会做竖屏场景的特殊下移或者是横屏场景的右移特殊处理
<meta-data android:name="android.notch_support" android:value="true"/>
方案二
使用给window添加新增的FLAG_NOTCH_SUPPORT
1. 接口1描述:应用通过增加华为自定义的刘海屏flag,请求使用刘海区显示:
类文件 | 接口 | 接口说明 |
com.huawei.android.view.LayoutParamsEx | public void addHwFlags(int hwFlags) | 通过添加窗口FLAG的方式设置页面使用刘海区显示: public static final int FLAG_NOTCH_SUPPORT=0x00010000; |
调用范例
/*刘海屏全屏显示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");
}
}
接口2描述:可以通过clearHwFlags接口清除添加的华为刘海屏Flag,恢复应用不使用刘海区显示。
类文件 | 接口 | 接口说明 |
com.huawei.android.view.LayoutParamsEx | public void clearHwFlags (int hwFlags) | 通过去除窗口FLAG的方式设置页面不使用刘海区显示: public static final int FLAG_NOTCH_SUPPORT=0x00010000; |
调用范例
/*刘海屏全屏显示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());
}
}
});
华为的刘海是可以提供用户选择是否打开
读取开关状态调用范例
public static final String DISPLAY_NOTCH_STATUS = "display_notch_status";
int mIsNotchSwitchOpen = Settings.Secure.getInt(getContentResolver(),DISPLAY_NOTCH_STATUS, 0);
// 0表示“默认”,1表示“隐藏显示区域”
三星
/**
* 三星手机SM-G8870 刘海屏适配
*
*/
@RequiresApi(api = Build.VERSION_CODES.O)
public void getSamsungCutOutInfo() {
try {
final View decorView = mActivity.getWindow().getDecorView();
WindowInsets windowInsets = decorView.getRootWindowInsets();
WindowInsetsWrapper wrapper = new WindowInsetsWrapper(windowInsets);
DisplayCutoutWrapper mLastDisplayCutoutWrapper = wrapper.getDisplayCutoutWrapper();
if (mLastDisplayCutoutWrapper == null) {
isSamsungCutOutMode = false;
return;
}
List<Rect> boundingRects = mLastDisplayCutoutWrapper.getBoundingRects();
if (boundingRects == null || boundingRects.size() == 0) {
Logger.d(TAG, "is not notch screen");
isSamsungCutOutMode = false;
} else {
isSamsungCutOutMode = true;
Logger.d(TAG, "Top=" + mLastDisplayCutoutWrapper.getSafeInsetTop());
Logger.d(TAG, "Bottom=" + mLastDisplayCutoutWrapper.getSafeInsetBottom());
Logger.d(TAG, "Left=" + mLastDisplayCutoutWrapper.getSafeInsetLeft());
Logger.d(TAG, "Right=" + mLastDisplayCutoutWrapper.getSafeInsetRight());
mSamsungSafeInsetHeight = mLastDisplayCutoutWrapper.getSafeInsetLeft() + mLastDisplayCutoutWrapper.getSafeInsetTop();
}
} catch (Exception e) {
e.printStackTrace();
}
}
谷歌P版本刘海屏适配方案
特性介绍
谷歌称刘海屏为凹口屏以及屏幕缺口支持, 下面的内容摘自:https://developer.android.com/preview/features#cutout
Android P 支持最新的全面屏以及为摄像头和扬声器预留空间的凹口屏幕。 通过全新的 DisplayCutout 类,可以确定非功能区域的位置和形状,这些区域不应显示内容。 要确定这些凹口屏幕区域是否存在及其位置,请使用 getDisplayCutout() 函数。
1. 全新的窗口布局属性 layoutInDisplayCutoutMode 让您的应用可以为设备凹口屏幕周围的内容进行布局。 您可以将此属性设为下列值之一:
(1)LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
(2)LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
(3)LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
2. 您可以按如下方法在任何运行 Android P 的设备或模拟器上模拟屏幕缺口:
(1)启用开发者选项。
(2)在 Developer options 屏幕中,向下滚动至 Drawing 部分并选择 Simulate a display with a cutout。
(3)选择凹口屏幕的大小。
注意:
我们建议您通过使用运行 Android P 的设备或模拟器测试凹口屏幕周围的内容显示。
接口介绍
1. 获取刘海尺寸相关接口:
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。 |
2. 设置是否延伸到刘海区显示接口:
类 | 属性 | 属性说明 |
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区域。 |
参考实现代码
1. 设置使用刘海区显示代码:
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。
2. 获取刘海屏安全显示区域和刘海尺寸信息:
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;
}
});
3. 说明:
1)通过windowInsets.getDisplayCutout()是否为null判断是否刘海屏手机,如果为null为非刘海屏
2)如果是刘海屏手机可以通过接口获取刘海信息
3)刘海个数可以是多个
在最后推荐一款适配方面的框架ImmersionBar,android 4.4以上沉浸式状态栏和沉浸式导航栏管理,包括状态栏字体颜色,适用于Activity、Fragment、DialogFragment、Dialog,并且适配刘海屏,适配软键盘弹出等问题,一句代码轻松实现,以及对bar的其他设置,个人觉得好用!
github地址:https://github.com/gyf-dev/ImmersionBar
感兴趣的话可自我前往阅读尝试,仅为个人喜好分享,不喜者请屏蔽。
参考文案