本文将会从以下3个方面介绍刘海屏适配,先了解刘海屏在android平台的历史背景,然后全面了解国内的差异化适配和Android P 版本出来后的适配,最后通过实战的例子让我们更深刻了解刘海屏的适配过程。
- 历史背景
- 如何适配刘海屏
- 接入实战和难点设计
1、历史背景
刘海屏对于android平台的影响我认为分为2个阶段:
- ios最早提出的刘海屏的概念,并发布首台带刘海屏的手机。国产手机厂商已经等不及google对刘海屏的支持更新,纷纷推出了自己的刘海屏手机,并提出了很多的概念:刘海屏,水滴屏等。
- 而google已经发现这个趋势已不能避免,开始在Android P(即9.0版本)开始提供获取刘海缺口(cutout)尺寸的接口了。国内的四大厂商也都明确的表明自己在Android9.0版本将遵循google的接口协议,而对于8.0版本中各大厂商自定义的获取刘海尺寸的接口,有的厂商表示在9.0版本不再兼容(例如:小米),有的则表示也会继续兼容。
划重点就是:
- 国内很多大厂商(例如:Oppo, Vivio, 华为,小米)在android8.0提供了各自的刘海屏相关的接口,出现了不统一的问题
- Android9.0版本统一了刘海屏相关的接口,解决了不统一的问题
2、如何适配刘海屏
2.1、适配的建议
适配的建议:内容部分需要避开凹槽区域,由背景填充凹槽区域。刘海区域是被遮挡的区域,这一块区域我们称为“危险区域”,那么与它相邻的两个区域我们形象的称之为“耳朵区”,那么原则就是即便我们拿到了“耳朵区”的位置和尺寸,我们还是建议不要把关键的内容或者按钮放在这块区域,更不能放在“危险区域”。
2.2、各大厂商在8.0的处理思路
我整理完国内四大厂商对刘海屏的各自的接口协议,他们对刘海屏的处理主要包含2个部分:
1)提供刘海区域的宽高信息(通常它们都是居中的,所以位置需要自己去计算)。提供的方式包括:系统属性,反射,或者告诉你屏幕的高宽比对应的刘海区域的固定尺寸这3种方式
2)设置使用“耳朵区”的方式。也包括两种方式:
- 可以在manifest针对app, activity或者代码里面通过window flag方式,分别在全局应用级别,activity级别,单个window级别设置是否使用耳朵区
- 或者在系统设置里面针对某个app设置是否使用耳朵区
3)获取是否有刘海,和提供刘海区的宽高信息的方式类似,提供了反射,固定宽高比对应固定刘海尺寸,系统资源常量这3种方式
2.3、如何适配耳朵区
这是这篇文章的第一个重点,分为两个部分:
- 国内主流厂商的android8.0系统版本适配
- android9.0适配
2.3.1、适配前准备
不管哪个系统版本的手机,它们有一个默认统一的共性是(不包括三星的挖孔屏):
- 刘海在手机顶部水平居中
- 刘海的高度略小于系统状态栏的高度
- 默认不使用“耳朵区”,需要手动设置
记住这3个点,对我们接下来具体实现会有帮助。这里也引申出一个点是应用要想使用“耳朵区”那么它首先得是全屏显示的。那么如何设置app全屏显示呢,可以这样做:
1 2 3 4 5 |
//去掉标题栏失效,原因可能是创建Activity时继承的是AppCompatActivity而非Activity //这行代码必须写在setContentView()方法的前面 requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.activity_main); getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); |
View.SYSTEM_UI_FLAG_FULLSCREEN
> 隐藏状态栏,点击屏幕区域不会出现,需要从状态栏位置下拉才会出现。
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
> 将布局内容拓展到状态栏(status bar)的后面。
2.3.2、国内主流厂商 android 8.0 系统版本适配
首先写这篇文章的时候是2019年3月19日,此时Android已经除了Q(10.0)版本的beta版,相信此时即使这四大厂商再出8.0版本的新刘海屏手机可能性也是非常小的。
先来一张图,有个整体的认识
2.3.2.1、华为
设置使用耳朵区
使用新增的Meta-data属性android.notch_support,在应用的AndroidManifest.xml中增加meta-data属性,此属性不仅可以针对Application生效,也可以对Activity配置生效。
1. 具体方式如下所示:
1 |
<meta-data android:name="android.notch_support" android:value="true"/> |
其它的更详细的方式,请参考上面贴出的官方详细文档
判断是否有刘海屏
华为有3个接口获取刘海的信息:是否有刘海,刘海的尺寸,设置里面是否开启使用刘海区域
华为的刘海屏处理特性:系统设置里面的开关为总开关,即便你在manifest里面申明了需要使用耳朵区,但如果在系统设置里面设置了关闭使用耳朵区,则应用顶部会被刘海挤下去,留出一条刘海高度的黑边。
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 |
private static final String DISPLAY_NOTCH_STATUS = "display_notch_status"; /** * 使用耳朵区 */ private static final int NOTCH_STATUS_DEFAULT = 0; /** * 不使用耳朵区 */ private static final int NOTCH_STATUS_HIDE = 1; @Override protected boolean hasNotchInScreen(Activity activity) { boolean ret = false; try { ClassLoader cl = activity.getClassLoader(); Class HwNotchSizeUtil = cl.loadClass("com.huawei.android.util.HwNotchSizeUtil"); Method get = HwNotchSizeUtil.getMethod("hasNotchInScreen"); ret = (boolean) get.invoke(HwNotchSizeUtil); } catch (ClassNotFoundException e) { LogUtil.e(getTag(), "hasNotchInScreen ClassNotFoundException"); } catch (NoSuchMethodException e) { LogUtil.e(getTag(), "hasNotchInScreen NoSuchMethodException"); } catch (Exception e) { LogUtil.e(getTag(), "hasNotchInScreen Exception"); } int isNotchSwitchOpen = NOTCH_STATUS_DEFAULT; if(ret) { try { isNotchSwitchOpen = Settings.Secure.getInt(activity.getContentResolver(), DISPLAY_NOTCH_STATUS, 0); } catch (Exception ex) { ex.printStackTrace(); LogUtil.w(getTag(), "get notch status failed:"+ex.getMessage()); } } return ret && (isNotchSwitchOpen == NOTCH_STATUS_DEFAULT); } |
获取刘海的尺寸
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
@Override protected int[] getNotchSize(Activity activity) { int[] ret = new int[]{0, 0}; try { ClassLoader cl = activity.getClassLoader(); Class HwNotchSizeUtil = cl.loadClass("com.huawei.android.util.HwNotchSizeUtil"); Method get = HwNotchSizeUtil.getMethod("getNotchSize"); ret = (int[]) get.invoke(HwNotchSizeUtil); } catch (ClassNotFoundException e) { LogUtil.e(getTag(), "getNotchSize ClassNotFoundException"); } catch (NoSuchMethodException e) { LogUtil.e(getTag(), "getNotchSize NoSuchMethodException"); } catch (Exception e) { LogUtil.e(getTag(), "getNotchSize Exception"); } finally { return ret; } } |
2.3.2.2、 ViVO
设置使用耳朵区
应用级别无法设置是否使用耳朵区,需要用户在设置>显示与亮度>第三方应用显示比例(VIVO X21)中对这个应用开启才行,如下图所示勾选全屏显示:
如果想要让应用在VIVO手机默认勾选“全屏显示”,我咨询过VIVO的技术同学,他们这么回复:
适配好刘海屏机型的包上传商店时,备注:已适配刘海屏机型,请验证。
如果商店测试通过,会通知相关人员开启默认全屏,开启的时间需要走单,所以要等
判断是否有刘海屏
需要特别注意的是VIVO有圆角的概念,它的影响和刘海屏是一样的。我们适配时也需要一起考虑。VIVO提供了判断手机设备是否有圆角和刘海屏的接口:
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 |
private static final int MASK_HAS_NOTCH = 0x00000020; private static final int MASK_HAS_ROUND_CORNER = 0x00000008; @Override protected boolean hasNotchInScreen(Activity activity) { return hasFeatureInVivo(activity, MASK_HAS_NOTCH); } protected boolean hasRoundCornerInScreen(Activity activity) { return hasFeatureInVivo(activity, MASK_HAS_ROUND_CORNER); } private boolean hasFeatureInVivo(Activity activity, int featureMask) { boolean ret = false; try { ClassLoader cl = activity.getClassLoader(); Class vivoNotchSizeUtil = cl.loadClass("android.util.FtFeature"); Method get = vivoNotchSizeUtil.getMethod("isFeatureSupport", int.class); ret = (boolean) get.invoke(vivoNotchSizeUtil, featureMask); } catch (ClassNotFoundException e) { LogUtil.e(getTag(), "hasNotchInScreen ClassNotFoundException"); } catch (NoSuchMethodException e) { LogUtil.e(getTag(), "hasNotchInScreen NoSuchMethodException"); } catch (Exception e) { LogUtil.e(getTag(), "hasNotchInScreen Exception"); } finally { return ret; } } |
获取刘海的尺寸
vivo的刘海尺寸是固定的:
屏幕高宽占比:19:9
屏幕圆角:24dp
凹槽区域:宽100dp, 高27dp
状态栏高度:32dp
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 |
SparseArray<int[]> vivoNotchSizeArr = new SparseArray<>(); SparseArray<Integer> vivoCornerSizeArr = new SparseArray<>(); /** * vivo 屏幕高宽比为19:9的刘海尺寸为: 宽 100dp, 高 27dp, 圆角尺寸:24dp */ { int aspectRatio = (int)Math.floor(19/9); vivoNotchSizeArr.put(aspectRatio, new int[]{100, 27}); vivoCornerSizeArr.put(aspectRatio, 24); } @Override protected int[] getNotchSize(Activity activity) { int[] notchSizeInDp = vivoNotchSizeArr.get(getAspectRatio(activity)); int[] notchSizeInPx = null; if(notchSizeInDp != null) { notchSizeInPx = new int[2]; notchSizeInPx[0] = dpToPx(activity, notchSizeInDp[0]); notchSizeInPx[1] = dpToPx(activity, notchSizeInDp[1]); } else if(mHasNotch) { //如果有刘海,但无法获取刘海的尺寸,则以状态栏的尺寸做为刘海的尺寸 notchSizeInPx = getStatusBarSize(activity); } return notchSizeInPx; } private CutoutInfo getCutoutInfoWithCorner(Activity activity) { int cornerSizeInDp = vivoCornerSizeArr.get(getAspectRatio(activity), 0); if(cornerSizeInDp > 0) { WinSizeInfo winSizeInfo = getWindowSize(activity); int cornerSizePx = dpToPx(activity, cornerSizeInDp); int[] cornerCutupSize = new int[2]; int deviceWidth = Math.min(winSizeInfo.width, winSizeInfo.height); cornerCutupSize[0] = deviceWidth; cornerCutupSize[1] = cornerSizePx; //4个圆角,所以可以认为上下两个圆角半径区域为危险区域 List<int[]> notchSizeList = new ArrayList<>(2); notchSizeList.add(cornerCutupSize); notchSizeList.add(cornerCutupSize); return calculateCutoutWithRotation(activity, notchSizeList); } else { return null; } } |
2.3.2.3、OPPO
设置使用耳朵区
按照Oppo官方描述:目前在设置 — 显示 — 应用全屏显示 — 凹形区域显示控制,里面有关闭凹形区域开关,用户可通过这个关闭凹形区域避免遮挡
判断是否有刘海屏
1 2 3 4 |
@Override protected boolean hasNotchInScreen(Activity activity) { return activity.getPackageManager().hasSystemFeature("com.oppo.feature.screen.heteromorphism"); } |
获取刘海的尺寸
Oppo刘海的尺寸也是固定的,它的规则为:
采用宽度为1080px, 高度为2280px的圆弧显示屏。 屏幕顶部凹形区域不能显示内容,宽度为324px, 高度为80px。
所以我们可以这样获取:
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 |
private Map<String, int[]> screenSizeMap = new HashMap<>(); /** * oppo 刘海屏的尺寸按照屏幕分辨率固定:https://open.oppomobile.com/wiki/doc#id=10159 * 采用宽度为1080px, 高度为2280px的圆弧显示屏。 屏幕顶部凹形区域不能显示内容,宽度为324px, 高度为80px。 */ { screenSizeMap.put("1080_2280", new int[]{324, 80}); } /** * 获取刘海尺寸:width、height * int[0]值为刘海宽度, int[1]值为刘海高度。 * * @param activity * @return */ @Override public int[] getNotchSize(Activity activity) { WindowManager windowManager = (WindowManager) activity.getSystemService(Context.WINDOW_SERVICE); DisplayMetrics metrics = new DisplayMetrics(); windowManager.getDefaultDisplay().getMetrics(metrics); int deviceWidth = Math.min(metrics.widthPixels, metrics.heightPixels); int deviceHeight = Math.max(metrics.widthPixels, metrics.heightPixels); String key = deviceWidth + "_" + deviceHeight; int[] notchSize = screenSizeMap.get(key); //如果有刘海,但是无法获取刘海的尺寸,为保险起见将状态栏尺寸定位刘海的尺寸 if(notchSize == null && mHashNotch) { notchSize = getStatusBarSize(activity); } return notchSize; } |
2.3.2.4、小米
小米刘海屏水滴屏 Android O 适配
设置使用耳朵区
和华为的处理方式类似,可以在application和window级别设置使用“耳朵区”的方式。例如在application级别设置的方式如下:
1 2 3 |
<meta-data android:name="notch.config" android:value="portrait|landscape"/> |
详细请参考官方文档。
判断是否有刘海屏
通过反射获取
小米刘海设置的特性:和华为相反,虽然小米的系统设置里面可以可以控制应用是否使用刘海屏,一旦开发者声明了meta-data,系统就会优先遵从开发者的声明。
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 |
@Override protected boolean hasNotchInScreen(Activity activity) { boolean hasNotch = (getInt(activity, "ro.miui.notch", 0) == 1); boolean forceBlack = false; if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { try { forceBlack = Settings.Global.getInt(activity.getContentResolver(), "force_black", 0) == 1; } catch (Exception ex) { ex.printStackTrace(); LogUtil.w(getTag(), "get force_black failed:"+ex.getMessage()); } } return hasNotch && !forceBlack; } /** * Get the value for the given key, and return as an integer. * @param key the key to lookup * @param def a default value to return * @return the key parsed as an integer, or def if the key isn't found or * cannot be parsed * @throws IllegalArgumentException if the key exceeds 32 characters */ public static Integer getInt(Context context, String key, int def) throws IllegalArgumentException { Integer ret= def; try{ ClassLoader cl = context.getClassLoader(); @SuppressWarnings("rawtypes") Class SystemProperties = cl.loadClass("android.os.SystemProperties"); //Parameters Types @SuppressWarnings("rawtypes") Class[] paramTypes= new Class[2]; paramTypes[0]= String.class; paramTypes[1]= int.class; Method getInt = SystemProperties.getMethod("getInt", paramTypes); //Parameters Object[] params= new Object[2]; params[0]= new String(key); params[1]= new Integer(def); ret= (Integer) getInt.invoke(SystemProperties, params); }catch(IllegalArgumentException iAE){ ret = def; }catch(Exception e){ ret = def; } return ret; } |
获取刘海的尺寸
从系统的res资源中获取
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@Override protected int[] getNotchSize(Activity activity) { Resources res = activity.getResources(); int[] notchSize = new int[2]; int widthResId = res.getIdentifier("notch_width", "dimen", "android"); if (widthResId > 0) { notchSize[0] = res.getDimensionPixelSize(widthResId); } int heightResId = res.getIdentifier("notch_height", "dimen", "android"); if (heightResId > 0) { notchSize[1] = res.getDimensionPixelSize(heightResId); } return notchSize; } |
2.3.3、android 9.0 适配
总结来说Android 官方提供了国内厂商自定义的所有方式:
- 设置使用“耳朵区”的方式:LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT,LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES,LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
- 判断是否有刘海屏:使用 DisplayCutout 的getBoundingRects()方法,如果返回null或者size为0,我们可以认为没有刘海
- 获取刘海的尺寸:android提供了获取刘海尺寸,同时也返回了安全区域的信息。
举个例子:
2.3.4、其它机型
目前国内市场上4大厂商的手机占了65%左右,再排除掉8.0版本,剩下的手机占比会比较小。如果需要继续适配还是有一些空间的,例如:三星的挖孔屏,魅族的新款刘海屏手机等。如果简单做的话,我们的兜底处理是将状态栏的高度认为是刘海的高度,屏幕在竖屏情况下的宽度视为刘海的宽度。要记住这些是有前提的,首先应用是全屏,且是8.0系统版本,不是4大厂商。
3、接入实战和难点设计
通过上面的分析我们已经非常细的了解了刘海屏的现状,可以完整梳理整个刘海屏尺寸的获取流程如下:
从图中我们可以看出,除了机型和系统版本的适配,我们还需要考虑:是否全屏和屏幕方向处理:
3.1、是否全屏
判断应用是否全屏并没有想象中那么简单,如上面提到的对于刘海屏一些手机厂商默认是不让你的应用内容延伸到“耳朵区”的,也就是说即便你的应用设置了全屏,而用户在设置里面关闭了全面屏使用,系统也会把你的应用界面挤在刘海区域以下,而上面留出一条包含刘海高度黑色的边。如下图所示:
所以我们需要另一种判断全屏的方式,我提供一种方法,如果不妥的地方欢迎大家留言:
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 |
/** * 判断手机顶部刘海区域与界面是否有间距 * * @param activity * @return */ public static boolean isFullScreen(Activity activity) { Rect rectgle = new Rect(); Window window = activity.getWindow(); window.getDecorView().getWindowVisibleDisplayFrame(rectgle); WindowManager windowManager = (WindowManager) activity.getSystemService(Context.WINDOW_SERVICE); DisplayMetrics metrics = new DisplayMetrics(); if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { windowManager.getDefaultDisplay().getRealMetrics(metrics); } else { windowManager.getDefaultDisplay().getMetrics(metrics); } boolean isFullScreen = false; int orientation = ScreenUtil.getOrientation(activity); switch (orientation) { case ActivityInfo.SCREEN_ORIENTATION_PORTRAIT: isFullScreen = rectgle.top == 0; break; case ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT: isFullScreen = rectgle.bottom == metrics.heightPixels + rectgle.top; break; case ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE: isFullScreen = rectgle.left == 0; break; case ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE: isFullScreen = rectgle.right == metrics.widthPixels + rectgle.left; break; } LogUtil.d(TAG, "isFullScreen: "+isFullScreen); return isFullScreen; } |
3.2、屏幕方向
另一个难点是屏幕方向,我们知道当手机横竖屏切换时宽变成了高,高变成了宽,系统会自动帮我们转换。而刘海区域就不同了,它固定在手机的顶部,随着用户360度旋转会出现在不同的方向上,而应用界面仍然会以正确的方向面向用户。这意味着刘海的区域会遮挡住应用左边,上边,右边,下面的内容。我们在返回刘海的位置和尺寸的时候需要特别考虑这种情况。
一种思路是:首先正确的获取当前手机旋转的方向,然后可以知道当前刘海在哪个方位(还得考虑有上下两个危险区域的情况,例如VIVO的圆角适配),这样就可以根据刘海的尺寸和国内刘海普遍居中这样一个特性得出刘海的具体位置和尺寸信息了。
具体实现思路:先判断当前应用是否固定设置了屏幕方向,如果有则直接使用。如果无则根据屏幕旋转的角度判断具体的旋转方向。
手机有四个旋转方位:
1 2 3 4 |
{@link ActivityInfo#SCREEN_ORIENTATION_PORTRAIT} 垂直方向 {@link ActivityInfo#SCREEN_ORIENTATION_REVERSE_PORTRAIT} 倒转 {@link ActivityInfo#SCREEN_ORIENTATION_LANDSCAPE} 向左横屏 {@link ActivityInfo#SCREEN_ORIENTATION_REVERSE_LANDSCAPE} 向右横屏 |
代码示例:
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 |
/** * 返回ActivityInfo里面的orientation常量 * {@link ActivityInfo#SCREEN_ORIENTATION_PORTRAIT} * {@link ActivityInfo#SCREEN_ORIENTATION_REVERSE_PORTRAIT} * {@link ActivityInfo#SCREEN_ORIENTATION_LANDSCAPE} * {@link ActivityInfo#SCREEN_ORIENTATION_REVERSE_LANDSCAPE} * * @param activity * @return */ public static int getOrientation(Activity activity) { int requestedOrientation = activity.getRequestedOrientation(); if(isRequestOrientationRecognized(requestedOrientation)) { return requestedOrientation; } else { int activityOrientation = activity.getResources().getConfiguration().orientation; int rotation = activity.getWindowManager().getDefaultDisplay().getRotation(); if(activityOrientation == Configuration.ORIENTATION_PORTRAIT) { if(rotation == Surface.ROTATION_0) { return ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; } else { return ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT; } } else { if(rotation == Surface.ROTATION_90) { return ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; } else { return ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE; } } } } private static boolean isRequestOrientationRecognized(int orientation) { return orientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || orientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT || orientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || orientation == ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE; } |
根据方向计算刘海的位置:
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 115 116 117 118 119 120 121 122 123 124 |
/** * 计算cutinfo * * @param activity * @param notchSizeList, notchSize数组列表,一个数组中: index 0: 宽度, index 1: 高度 * @return */ protected CutoutInfo calculateCutoutWithRotation(Activity activity, List<int[]> notchSizeList) { CutoutInfo.Region topRegion = new CutoutInfo.Region(); List<CutoutInfo.Region> regionList = new ArrayList<>(); regionList.add(topRegion); int[] safeArea = new int[4]; WinSizeInfo winSizeInfo = getWindowSize(activity); int activityOrientation = ScreenUtil.getOrientation(activity); //按照google对刘海的定义,缺口最多有两个,且必须在手机的顶部和底部,不允许在侧边 //官方文档:https://developer.android.com/guide/topics/display-cutout/ boolean hasBottomNotch = false; int notchListLen = notchSizeList.size(); CutoutInfo.Region bottomRegion = null; if(notchListLen == 2) { hasBottomNotch = true; bottomRegion = new CutoutInfo.Region(); regionList.add(bottomRegion); } int[] notchSizeTop = notchSizeList.get(0); int[] notchSizeBottom = null; if(hasBottomNotch) { notchSizeBottom = notchSizeList.get(1); } switch (activityOrientation) { case ActivityInfo.SCREEN_ORIENTATION_PORTRAIT: //top region LogUtil.d(getTag(), "SCREEN_ORIENTATION_PORTRAIT"); topRegion.x = winSizeInfo.left + (winSizeInfo.width - notchSizeTop[0]) / 2; topRegion.y = winSizeInfo.top; topRegion.width = notchSizeTop[0]; topRegion.height = notchSizeTop[1]; //bottom region if(hasBottomNotch) { bottomRegion.x = winSizeInfo.left + (winSizeInfo.width - notchSizeBottom[0]) / 2; bottomRegion.y = winSizeInfo.top + winSizeInfo.height - notchSizeBottom[1]; bottomRegion.width = notchSizeBottom[0]; bottomRegion.height = notchSizeBottom[1]; } //safe area safeArea[0] = winSizeInfo.left; safeArea[1] = winSizeInfo.top + notchSizeTop[1]; safeArea[2] = winSizeInfo.right; safeArea[3] = hasBottomNotch ? winSizeInfo.bottom + notchSizeBottom[1] : winSizeInfo.bottom; break; case ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE: LogUtil.d(getTag(), "SCREEN_ORIENTATION_LANDSCAPE"); topRegion.x = winSizeInfo.left; topRegion.y = winSizeInfo.top + (winSizeInfo.height - notchSizeTop[0]) / 2; topRegion.width = notchSizeTop[1]; topRegion.height = notchSizeTop[0]; //bottom region if(hasBottomNotch) { bottomRegion.x = winSizeInfo.left + winSizeInfo.width - notchSizeBottom[1]; bottomRegion.y = winSizeInfo.top + (winSizeInfo.height - notchSizeBottom[0]) / 2; bottomRegion.width = notchSizeBottom[1]; bottomRegion.height = notchSizeBottom[0]; } //safe area safeArea[0] = winSizeInfo.left + notchSizeTop[1]; safeArea[1] = winSizeInfo.top; safeArea[2] = hasBottomNotch ? winSizeInfo.right + notchSizeBottom[1] : winSizeInfo.right; safeArea[3] = winSizeInfo.bottom; break; case ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT: LogUtil.d(getTag(), "SCREEN_ORIENTATION_REVERSE_PORTRAIT"); topRegion.x = winSizeInfo.left + (winSizeInfo.width - notchSizeTop[0]) / 2; topRegion.y = winSizeInfo.top + winSizeInfo.height - notchSizeTop[1]; topRegion.width = notchSizeTop[0]; topRegion.height = notchSizeTop[1]; //bottom region if(hasBottomNotch) { bottomRegion.x = winSizeInfo.left + (winSizeInfo.width - notchSizeBottom[0]) / 2; bottomRegion.y = winSizeInfo.top; bottomRegion.width = notchSizeBottom[0]; bottomRegion.height = notchSizeBottom[1]; } //safe area safeArea[0] = winSizeInfo.left; safeArea[1] = hasBottomNotch ? winSizeInfo.top + notchSizeBottom[1] : winSizeInfo.top; safeArea[2] = winSizeInfo.right; safeArea[3] = winSizeInfo.bottom + notchSizeTop[1]; break; case ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE: LogUtil.d(getTag(), "SCREEN_ORIENTATION_REVERSE_LANDSCAPE"); topRegion.x = winSizeInfo.left + winSizeInfo.width - notchSizeTop[1]; topRegion.y = winSizeInfo.top + (winSizeInfo.height - notchSizeTop[0]) / 2; topRegion.width = notchSizeTop[1]; topRegion.height = notchSizeTop[0]; if(hasBottomNotch) { bottomRegion.x = winSizeInfo.left; bottomRegion.y = winSizeInfo.top + (winSizeInfo.height - notchSizeBottom[0]) / 2; bottomRegion.width = notchSizeBottom[1]; bottomRegion.height = notchSizeBottom[0]; } //safe area safeArea[0] = hasBottomNotch ? winSizeInfo.left + notchSizeBottom[1] : winSizeInfo.left; safeArea[1] = winSizeInfo.top; safeArea[2] = notchSizeTop[1]; safeArea[3] = winSizeInfo.bottom; break; } LogUtil.d(getTag(), "after calculate, region: x"+topRegion.x+", y:"+topRegion.y+", w:"+topRegion.width+", h:"+topRegion.height +", safe area: left:"+safeArea[0]+", top:"+safeArea[1]+", right:"+safeArea[2]+", bottom:"+safeArea[3]); return new CutoutInfo(CutoutInfo.CUTOUT_TYPE_EXIST, regionList, safeArea); } |
3.3、判断手机厂商
收集了4个手机厂商以及他们旗下品牌手机的参数:
特别注意标红标粗的字符串的大小写不能改变,因为这些都是各个厂商自己约定且保证不会改变的。
华为mate: BRAND: HUAWEI, manufacturer: HUAWEI
华为荣耀:BRAND:HONOR, manufacturer: HUAWEI
OPPO: BRAND: OPPO, manufacturer: OPPO
VIVO : BRAND: vivo, manufacturer: vivo
小米:brand: xiaomi, manufacturer: Xiaomi
4、结束语
总揽了一下,这篇文章应该是目前全网最良心的了,喜欢的朋友欢迎讨论和点赞