先炫富!
三种情况会涉及到刘海的适配:
1.有状态栏的情况(不是全屏FULLSCREEN状态)不需要适配刘海,如果没有设置状态栏颜色默认状态栏为黑色背景白色字体和图标,这种情况不会受到刘海的影响,因为刘海的高度就是状态栏的高度,只要有状态栏的app不需要进行刘海适配,当然可以相应的进行状态栏背景或者字体颜色的修改使app色调更加协调,下图为vivo x21的状态栏可以看到电量时间网络状态等图标显示在左右两侧,中间空白就是刘海挡住的位置,所以不设置全屏是没有影响的。
2.没有状态栏-->全屏状态,没有适配刘海的界面。系统会对刘海区域进行横向切割,整体ui下移,全屏效果就没了,效果非常丑,状态栏黑色且无文字图标,效果如下图
3.没有状态栏-->全屏状态,已适配刘海屏的页面。可以兼容刘海屏全屏显示
对于第三点我门要进行刘海适配,刘海的适配又分为两种情况
3.1使用Android P进行刘海适配。 注:2018年3月份Android P发布后才提供相应的适配代码(此种方式基本可以忽略,因为大多数手机还没有到安卓p版本就已经使用了刘海屏设计,可以直接看下面针对各个主流机型的适配)
以下接口都是要Build.VERSION.SDK_INT >= 28
才能调用到。(compileSdkVersion 28 ,且minSdkVersion也为 28不然报错,修 改minSdkVersion 28 就好了,错误信息如下图)
3.1.1获取凹口位置和安全区域的位置等。主要接口如下所示
方法 | 接口说明 |
getBoundingRects() | 返回Rects的列表,每个Rects都是显示屏上非功能区域的边界矩形。 |
getSafeInsetLeft () | 返回安全区域距离屏幕左边的距离,单位是px。 |
getSafeInsetRight () | 返回安全区域距离屏幕右边的距离,单位是px。 |
getSafeInsetTop () | 返回安全区域距离屏幕顶部的距离,单位是px。 |
getSafeInsetBottom() | 返回安全区域距离屏幕底部的距离,单位是px。 |
例:当屏幕发生反转时可调用此方法来获取安全边距,然后进行相应的距离修改
<span style="color:#333333"> @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);
}
}
}
});
}</span>
3.1.2设置凹口显示模式
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区域。 |
-
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
一样。
当我们进行刘海屏的适配时,请根据实际情况去使用不同的layoutInDisplayCutoutMode
。如果我们做音视频方面的全屏页面一般会使用LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES模式延伸到刘海区域进行相应的安全区域处理
上面是Android P才有的解决方案,在P之前呢,上面的代码通通都没用。然而我们伟大的国产厂商在Android P之前(基本都是Android O)就用上了高档大气上档次的刘海屏,所以,这也造就了各大厂商在Android P之前的解决方案百花齐放。下面,我们来看下主流厂商:华为、vivo、OPPO、小米等所提供的方案。
3.2 Android P之前的适配。因为Android P之前官方还没提供API来进行适配,都是由各家厂商来提供适配方案
华为
使用刘海区显示
使用新增的meta-data
属性android.notch_support
。
在应用的AndroidManifest.xml
中增加meta-data
属性,此属性不仅可以针对Application
生效,也可以对Activity
配置生效。
如下所示:
<meta-data android:name="android.notch_support" android:value="true"/>
- 对
Application
生效,意味着该应用的所有页面,系统都不会做竖屏场景的特殊下移或者是横屏场景的右移特殊处理。 - 对
Activity
生效,意味着可以针对单个页面进行刘海屏适配,设置了该属性的Activity
系统将不会做特殊处理。
是否有刘海屏
通过以下代码即可知道华为手机上是否有刘海屏了,true
为有刘海,false
则没有。
public static boolean hasNotchAtHuawei(Context context) {
boolean ret = false;
try {
ClassLoader classLoader = context.getClassLoader();
Class HwNotchSizeUtil = classLoader.loadClass("com.huawei.android.util.HwNotchSizeUtil");
Method get = HwNotchSizeUtil.getMethod("hasNotchInScreen");
ret = (boolean) get.invoke(HwNotchSizeUtil);
} catch (ClassNotFoundException e) {
Log.e("Notch", "hasNotchAtHuawei ClassNotFoundException");
} catch (NoSuchMethodException e) {
Log.e("Notch", "hasNotchAtHuawei NoSuchMethodException");
} catch (Exception e) {
Log.e("Notch", "hasNotchAtHuawei Exception");
} finally {
return ret;
}
}
刘海尺寸
华为提供了接口获取刘海的尺寸,如下:
//获取刘海尺寸:width、height
//int[0]值为刘海宽度 int[1]值为刘海高度
public static int[] getNotchSizeAtHuawei(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("Notch", "getNotchSizeAtHuawei ClassNotFoundException");
} catch (NoSuchMethodException e) {
Log.e("Notch", "getNotchSizeAtHuawei NoSuchMethodException");
} catch (Exception e) {
Log.e("Notch", "getNotchSizeAtHuawei Exception");
} finally {
return ret;
}
}
vivo
vivo在设置–显示与亮度–第三方应用显示比例中可以切换是否全屏显示还是安全区域显示。
是否有刘海屏
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;
}
}
刘海尺寸
vivo不提供接口获取刘海尺寸,目前vivo的刘海宽为100dp,高为27dp。
OPPO
OPPO目前在设置 – 显示 – 应用全屏显示 – 凹形区域显示控制,里面有关闭凹形区域开关。
是否有刘海屏
public static boolean hasNotchAtOPPO(Context context) {
return context.getPackageManager().hasSystemFeature("com.oppo.feature.screen.heteromorphism");
}
刘海尺寸
OPPO不提供接口获取刘海尺寸,目前其有刘海屏的机型尺寸规格都是统一的。不排除以后机型会有变化。
其显示屏宽度为1080px,高度为2280px。刘海区域则都是宽度为324px, 高度为80px。
小米
是否有刘海屏
系统增加了 property ro.miui.notch
,值为1时则是 Notch 屏手机。
Application 级别的控制接口
如果开发者认为应用的所有页面统一处理就行,可以使用该接口。在 Application 下增加一个 meta-data,用以声明该应用是否使用耳朵区。示例如下:
<meta-data
android:name="notch.config"
android:value="portrait|landscape"/>
设置此属性之后才会在小米的全屏应用中查看到自己的应用已经处于针对全面屏做优化 的列表中
小米8->设置->全面屏->应用全屏运行设置 可查看应用是否适配,设置以上属性之后全屏应用就必须要处理哦
米8->设置->全面屏->应用全屏运行设置
其中,value 的取值可以是以下4种:
"none" 横竖屏都不绘制耳朵区
"portrait" 竖屏绘制到耳朵区
"landscape" 横屏绘制到耳朵区
"portrait|landscape" 横竖屏都绘制到耳朵区
注:一旦开发者声明了meta-data,系统就会优先遵从开发者的声明。
刘海尺寸
小米的状态栏高度会略高于刘海屏的高度,因此可以通过获取状态栏的高度来间接避开刘海屏,获取状态栏的高度代码如下:
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;
}
其他手机也可以通过这个方法来间接避开刘海屏,但是有可能有些手机的刘海屏高度会高于状态栏的高度,所以这个方法获取到的结果并不一定安全。
下面是参考资料,小米、华为、vivo、oppo 都有相应的适配指导,这里我写了一个小demo 是集成了这四种主流手机的适配方案的详细demo ,包括屏幕旋转之后的适配,有需要的可以拿 ,没积分的给我发私信~
注意 manifest里的设置,设置之后才会启用全面屏配置
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<meta-data
android:name="android.max_aspect"
android:value="2.2"/>
<!--小米手机开启刘海适配-->
<meta-data
android:name="notch.config"
android:value="portrait|landscape"/>
<!--华为手机开启刘海适配-->
<meta-data android:name="android.notch_support" android:value="true"/>
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
NotchPhoneUtils 全部代码
package dpi.myapplication;
import android.app.Activity;
import android.content.Context;
import android.os.SystemProperties;
import android.util.Log;
import android.view.Surface;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import java.lang.reflect.Method;
/**
* @version V
* @类描述:
* @项目名称:MyApplicationliuhai
* @包名称:dpi.myapplication
* @创建人:cc
* @创建时间:2018/7/17
* @修改人:
* @修改时间:
* @修改备注:TODO
*/
public class NotchPhoneUtils {
/**
* 华为手机判断是不是刘海手机
*
* @param context
* @return
*/
public static boolean hasNotchAtHuawei(Context context) {
boolean ret = false;
try {
ClassLoader classLoader = context.getClassLoader();
Class HwNotchSizeUtil = classLoader.loadClass("com.huawei.android.util.HwNotchSizeUtil");
Method get = HwNotchSizeUtil.getMethod("hasNotchInScreen");
ret = (boolean) get.invoke(HwNotchSizeUtil);
} catch (ClassNotFoundException e) {
Log.e("Notch", "hasNotchAtHuawei ClassNotFoundException");
} catch (NoSuchMethodException e) {
Log.e("Notch", "hasNotchAtHuawei NoSuchMethodException");
} catch (Exception e) {
Log.e("Notch", "hasNotchAtHuawei Exception");
} finally {
return ret;
}
}
/**
* 华为手机获取刘海的宽高
* int[0]值为刘海宽度 int[1]值为刘海高度
*/
public static int[] getNotchSizeAtHuawei(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("Notch", "getNotchSizeAtHuawei ClassNotFoundException");
} catch (NoSuchMethodException e) {
Log.e("Notch", "getNotchSizeAtHuawei NoSuchMethodException");
} catch (Exception e) {
Log.e("Notch", "getNotchSizeAtHuawei Exception");
} finally {
return ret;
}
}
/**
* OPPO判断是不是刘海手机,
* OPPO不提供接口获取刘海尺寸,目前其有刘海屏的机型尺寸规格都是统一的。不排除以后机型会有变化。
* 刘海区域则都是宽度为324px, 高度为80px。
*
* @param context
* @return
*/
public static boolean HasNotchOPPO(Context context) {
return context.getPackageManager().hasSystemFeature("com.oppo.feature.screen.heteromorphism");
}
public static final int VIVO_NOTCH = 0x00000020;//是否有刘海
/**
* vivo判断是不是刘海手机
*/
public static boolean HasNotchVivo(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;
}
}
/**
* 小米手机判断是不是刘海手机
*
* @return
*/
public static boolean HasNotchXiaoMi() {
return SystemProperties.getInt("ro.miui.notch", 0) == 1 ? true : false;
}
/**
* 小米手机获取刘海的高度
*/
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;
}
/**
* 屏幕旋转会走的方法
*
* @param activity
* @param isNotch
* @param type
* @param viewGroup
*/
public static void onConfigurationChanged(Activity activity, Boolean isNotch, int type, ViewGroup viewGroup) {
if (getDisplayRotation(activity) == 0) {
if (isNotch) {
switch (type) {
case 1: //vivo
FrameLayout.LayoutParams lpvivo = (FrameLayout.LayoutParams) viewGroup.getLayoutParams();
lpvivo.topMargin = dp2px(activity, 32);
lpvivo.leftMargin = 0;
lpvivo.rightMargin = 0;
viewGroup.setLayoutParams(lpvivo);
break;
case 2: //HUAWEI
int[] sizeAtHuawei = NotchPhoneUtils.getNotchSizeAtHuawei(activity);
FrameLayout.LayoutParams lphuawei = (FrameLayout.LayoutParams) viewGroup.getLayoutParams();
lphuawei.topMargin = sizeAtHuawei[1];
lphuawei.leftMargin = 0;
lphuawei.rightMargin = 0;
viewGroup.setLayoutParams(lphuawei);
break;
case 3: //OPPO 目前都为 80px
FrameLayout.LayoutParams lpOppo = (FrameLayout.LayoutParams) viewGroup.getLayoutParams();
lpOppo.topMargin = 80;
lpOppo.leftMargin = 0;
lpOppo.rightMargin = 0;
viewGroup.setLayoutParams(lpOppo);
break;
case 4: //Xiaomi
int sizeAtXiaomi = NotchPhoneUtils.getStatusBarHeight(activity);
FrameLayout.LayoutParams lpXiaomi = (FrameLayout.LayoutParams) viewGroup.getLayoutParams();
lpXiaomi.topMargin = sizeAtXiaomi;
lpXiaomi.leftMargin = 0;
lpXiaomi.rightMargin = 0;
viewGroup.setLayoutParams(lpXiaomi);
break;
}
}
} else if (getDisplayRotation(activity) == 90) {
leftAndRightChange(activity, isNotch, type, viewGroup);
} else if (getDisplayRotation(activity) == 180) {
} else if (getDisplayRotation(activity) == 270) {
leftAndRightChange(activity, isNotch, type, viewGroup);
}
}
/**
* 左右横屏都是让 leftMargin 和rightMargin 空出一个刘海的距离
*
* @param activity
* @param isNotch
* @param type
* @param viewGroup
*/
private static void leftAndRightChange(Activity activity, Boolean isNotch, int type, ViewGroup viewGroup) {
if (isNotch) {
switch (type) {
case 1: //vivo
FrameLayout.LayoutParams lpvivo = (FrameLayout.LayoutParams) viewGroup.getLayoutParams();
lpvivo.leftMargin = dp2px(activity, 32);
lpvivo.rightMargin = dp2px(activity, 32);
lpvivo.topMargin = 0;
lpvivo.bottomMargin = 0;
viewGroup.setLayoutParams(lpvivo);
break;
case 2: //HUAWEI
int[] sizeAtHuawei = NotchPhoneUtils.getNotchSizeAtHuawei(activity);
FrameLayout.LayoutParams lphuawei = (FrameLayout.LayoutParams) viewGroup.getLayoutParams();
lphuawei.leftMargin = sizeAtHuawei[1];
lphuawei.rightMargin = sizeAtHuawei[1];
lphuawei.topMargin = 0;
lphuawei.bottomMargin = 0;
viewGroup.setLayoutParams(lphuawei);
break;
case 3: //OPPO 目前都为 80px
FrameLayout.LayoutParams lpOppo = (FrameLayout.LayoutParams) viewGroup.getLayoutParams();
lpOppo.leftMargin = 80;
lpOppo.rightMargin = 80;
lpOppo.topMargin = 0;
lpOppo.bottomMargin = 0;
viewGroup.setLayoutParams(lpOppo);
break;
case 4: //Xiaomi
int sizeAtXiaomi = NotchPhoneUtils.getStatusBarHeight(activity);
FrameLayout.LayoutParams lpXiaomi = (FrameLayout.LayoutParams) viewGroup.getLayoutParams();
lpXiaomi.leftMargin = sizeAtXiaomi;
lpXiaomi.rightMargin = sizeAtXiaomi;
lpXiaomi.topMargin = 0;
lpXiaomi.bottomMargin = 0;
viewGroup.setLayoutParams(lpXiaomi);
break;
}
}
}
/**
* 获取当前屏幕旋转角度
*
* @param activity
* @return 0表示是竖屏; 90表示是左横屏; 180表示是反向竖屏; 270表示是右横屏
*/
public static int getDisplayRotation(Activity activity) {
if (activity == null)
return 0;
int rotation = activity.getWindowManager().getDefaultDisplay()
.getRotation();
switch (rotation) {
case Surface.ROTATION_0:
return 0;
case Surface.ROTATION_90:
return 90;
case Surface.ROTATION_180:
return 180;
case Surface.ROTATION_270:
return 270;
}
return 0;
}
/**
* px转dp
*
* @param context
* @param dipValue
*/
public static int dp2px(Context context, float dipValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dipValue * scale + 0.5f);
}
/**
* 获取手机厂商
*
* @return 手机厂商 Xiaomi HUAWEI vivo
*/
public static String getDeviceBrand() {
return android.os.Build.BRAND;
}
}
MainActivity 使用Utils的Activity
package dpi.myapplication;
import android.content.res.Configuration;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.Window;
import android.view.WindowManager;
import android.widget.RelativeLayout;
public class MainActivity extends AppCompatActivity {
private Boolean isNotch = false;// 是否为刘海屏
private int type;
RelativeLayout mRlall;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);//状态栏黑色字体
requestWindowFeature(Window.FEATURE_NO_TITLE);// 隐藏标题
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);// 设置全屏
setContentView(R.layout.activity_main);
mRlall = findViewById(R.id.rl_all);
String deviceBrand = NotchPhoneUtils.getDeviceBrand(); //获取手机厂商
if ("vivo".equals(deviceBrand)) {
isNotch = NotchPhoneUtils.HasNotchVivo(MainActivity.this);
type = 1;
} else if ("HUAWEI".equals(deviceBrand)) {
isNotch = NotchPhoneUtils.hasNotchAtHuawei(MainActivity.this);
type = 2;
} else if ("OPPO".equals(deviceBrand)) {
isNotch = NotchPhoneUtils.HasNotchOPPO(MainActivity.this);
type = 3;
} else if ("Xiaomi".equals(deviceBrand)) {
isNotch = NotchPhoneUtils.HasNotchXiaoMi();
type = 4;
}
NotchPhoneUtils.onConfigurationChanged(MainActivity.this, isNotch, type, mRlall);
}
//屏幕方向发生改变的回调方法
@Override
public void onConfigurationChanged(Configuration newConfig) {
NotchPhoneUtils.onConfigurationChanged(MainActivity.this, isNotch, type, mRlall);
super.onConfigurationChanged(newConfig);
}
}
下载地址:
gitHub地址:https://github.com/WuMaoQiang/NotchPhone
参考资料:
四月葡萄Android刘海屏适配全攻略