为什么要做耳朵屏的适配?
- 适应更长的屏幕
- 防止内容被刘海遮挡
适配思路
- 刘海屏适配仅针对全屏模型下的适配
- 是否有刘海
- 设置绘制区域 延伸到耳朵区域
- 获取耳朵屏刘海高度
- 对相关控件位置调整
- 各手机厂商的适配
遇到的问题
-
低版本编译环境 (本人项目最高仅支持 targetSdkVersion 26 、compileSdkVersion 26),无法正常调用 Android P 相关Api
-
Android P 适配方案 Android P 在不设置绘制区域 延伸到耳朵区域时 无法获取耳朵高度, 且在获取 notchInHeight 时 必须是当前Activity onWindowFocusChanged =true 时才可以拿到正确的高度
-
Android P 通过status_bar_height 获取耳朵高度并不牢靠,因为没有刘海的手机 status_bar_height 同样不为0
-
各厂商在 Android P后 都改用 Google 官方Api
问题解决方案
问题一:
通过 lib 依赖的方式解决 低版本编译问题;新建一个notchmodel ,hostmodel 对其进行依赖
compile project(path: ':notch')
原 hostmodel 编译环境 不变(targetSdkVersion 26 、compileSdkVersion 26) notchmodel 编译环境 改为 (targetSdkVersion 28 、compileSdkVersion 28) 这样notchmodel 就可以编写相关的适配代码 且不影响原 hostmodel 编译环境
问题二:
适配耳朵区域前,必须先全屏模式且允许内容绘制到耳朵区,由于是通过mDecorView.getRootWindowInsets 获取刘海高度,为了确保能正确获取 notchInHeight 通过view.post方式处理
view.post(new Runnable() {
@Override
public void run() {
int notchInHeight = NotchUtils.getNotchInHeight(SplashActivity.this);
FxLog.d("notchInHeight = " + notchInHeight );
}
});
问题三:
Android P 在获取的刘海高度时, 必须是全屏模式且允许内容绘制到耳朵区 才会读取正确的Notch 否则都为0
问题四:
Android P 如果设备版本大于等于Android P 则用 Google 官方的Api 进行适配
适配方案
1.Google 官方 Android P 耳朵屏适配方案
public class DisplayCutoutActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//1.设置全屏
requestWindowFeature(Window.FEATURE_NO_TITLE);
Window window = getWindow();
window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
WindowManager.LayoutParams params = window.getAttributes();
/**
* * @see #LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT 全屏模式,内容下移,非全屏不受影响
* * @see #LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES 允许内容去延伸进刘海区
* * @see #LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER 不允许内容延伸进刘海区
*/
params.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
window.setAttributes(params);
//3.设置成沉浸式
int flags = View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
int visibility = window.getDecorView().getSystemUiVisibility();
visibility |= flags; //追加沉浸式设置
window.getDecorView().setSystemUiVisibility(visibility);
}
setContentView(R.layout.activity_main);
// Button button = findViewById(R.id.button);
// RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) button.getLayoutParams();
// layoutParams.topMargin = heightForDisplayCutout();
// button.setLayoutParams(layoutParams);
RelativeLayout layout = findViewById(R.id.container);
layout.setPadding(layout.getPaddingLeft(), heightForDisplayCutout(), layout.getPaddingRight(), layout.getPaddingBottom());
}
}
各厂商的适配方案 华为、小米、vivo、oppo
/**
* Author : CharlesChen
* Time : 2019/7/1.
* Email :
* Desc :
* version : v1.0
*/
public class NotchUtils {
private static String TAG = "Notch_TAG";
@NotchType
private static int NOTCH_TYPE = 0;
@interface NotchType {
int NO_NOTCH = -1;
int P = 1;
int HUAWEI = 2;
int OPPP = 3;
int VIVO = 4;
int MIUI = 5;
}
public static @NotchType
int getNotchType(Context context) {
if (NOTCH_TYPE != 0) {
return NOTCH_TYPE;
}
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
NOTCH_TYPE = 1;
} else if (hasNotchInHuawei(context)) {
NOTCH_TYPE = 2;
} else if (hasNotchInOppo(context)) {
NOTCH_TYPE = 3;
} else if (hasNotchInVivo(context)) {
NOTCH_TYPE = 4;
} else if (hasNotchInMIUI()) {
NOTCH_TYPE = 5;
} else {
NOTCH_TYPE = -1;
}
} catch (Exception e) {
e.printStackTrace();
}
return NOTCH_TYPE;
}
/**
* 获取耳朵屏 凸出高度
*
* @param context
* @return
*/
public static int getNotchInHeight(Activity context) {
@NotchType int notchType = getNotchType(context);
int height = 0;
switch (notchType) {
case NotchType.HUAWEI:
height = getNotchSizeAtHuawei(context)[1];
break;
case NotchType.MIUI:
height = getNotchHeightInMIUI(context);
break;
case NotchType.NO_NOTCH:
height = 0;
break;
case NotchType.P:
height = getNotchHeightInP(context.getWindow());
break;
case NotchType.OPPP:
case NotchType.VIVO:
height = getStatusBarHeight(context);
break;
}
return height;
}
/**
* 设置绘制区域 延伸到耳朵区域
* Oppo、Vivo 默认 延伸到耳朵区域
* 小米、华为 需要手动设置
*/
public static void setDisplayInNotch(Activity context) {
try {
@NotchType int notchType = getNotchType(context);
switch (notchType) {
case NotchType.HUAWEI:
setDisplayInNotchAtHuawei(context.getWindow());
break;
case NotchType.MIUI:
setDisplayInNotchAtMIUI(context);
break;
case NotchType.NO_NOTCH:
break;
case NotchType.OPPP:
case NotchType.VIVO:
//Oppo、Vivo 默认 延伸到耳朵区域
case NotchType.P:
setDisplayInNotchInP(context);
break;
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static boolean hasNotch(Context context){
boolean hasNotch = false;
try {
@NotchType int notchType = getNotchType(context);
switch (notchType) {
case NotchType.HUAWEI:
hasNotch = hasNotchInHuawei(context);
break;
case NotchType.MIUI:
hasNotch = hasNotchInMIUI();
break;
case NotchType.NO_NOTCH:
break;
case NotchType.OPPP:
hasNotch = hasNotchInOppo(context);
break;
case NotchType.VIVO:
hasNotch = hasNotchInVivo(context);
break;
case NotchType.P:
hasNotch = hasNotchInP((Activity) context);
break;
}
} catch (Exception e) {
e.printStackTrace();
}
return hasNotch;
}
/********************************************* Android P *********************************************/
/**
* 必须是全屏模式且允许内容绘制到耳朵区 才会读取正确的Notch 否则都为null
*/
@TargetApi(Build.VERSION_CODES.P)
private static boolean hasNotchInP(Activity activity) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
return false;
}
Window window = activity.getWindow();
DisplayCutout displayCutout;
View rootView = window.getDecorView();
WindowInsets insets = rootView.getRootWindowInsets();
if (insets != null) {
displayCutout = insets.getDisplayCutout();
if (displayCutout != null) {
if (displayCutout.getBoundingRects() != null && displayCutout.getBoundingRects().size() > 0 && displayCutout.getSafeInsetTop() > 0) {
return true;
}
}
}
return true;
}
/**
* 必须是全屏模式且允许内容绘制到耳朵区 才会读取正确的Notch 否则都为0
*/
@TargetApi(Build.VERSION_CODES.P)
private static int getNotchHeightInP(Window window){
View rootView = window.getDecorView();
WindowInsets insets = rootView.getRootWindowInsets();
if (insets != null) {
DisplayCutout displayCutout = insets.getDisplayCutout();
if (displayCutout != null) {
if (displayCutout.getBoundingRects() != null && displayCutout.getBoundingRects().size() > 0 && displayCutout.getSafeInsetTop() > 0) {
return displayCutout.getSafeInsetTop();
}
}
}
return 0;
}
public static void setDisplayInNotchInP(Activity activity) {
Window window = activity.getWindow();
// 延伸显示区域到耳朵区
WindowManager.LayoutParams lp = window.getAttributes();
lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
window.setAttributes(lp);
// 允许内容绘制到耳朵区
final View decorView = window.getDecorView();
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
}
/********************************************* 华为 *********************************************/
/*刘海屏全屏显示FLAG*/
private static final int FLAG_NOTCH_SUPPORT = 0x00010000;
/**
* 是否刘海
*/
private static boolean hasNotchInHuawei(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(TAG, "hasNotchInScreen ClassNotFoundException");
} catch (NoSuchMethodException e) {
Log.e(TAG, "hasNotchInScreen NoSuchMethodException");
} catch (Exception e) {
Log.e(TAG, "hasNotchInScreen Exception");
}
return ret;
}
/**
* 获取刘海尺寸:width、height,int[0]值为刘海宽度 int[1]值为刘海高度。
*/
private 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(TAG, "getNotchSize ClassNotFoundException");
} catch (NoSuchMethodException e) {
Log.e(TAG, "getNotchSize NoSuchMethodException");
} catch (Exception e) {
Log.e(TAG, "getNotchSize Exception");
}
return ret;
}
/**
* 设置使用刘海区域
*/
private static void setDisplayInNotchAtHuawei(Window window) {
if (window == null) {
return;
}
try {
WindowManager.LayoutParams layoutParams = window.getAttributes();
Class layoutParamsExCls = Class.forName("com.huawei.android.view.LayoutParamsEx");
Constructor con = layoutParamsExCls.getConstructor(WindowManager.LayoutParams.class);
Object layoutParamsExObj = con.newInstance(layoutParams);
Method method = layoutParamsExCls.getMethod("addHwFlags", int.class);
method.invoke(layoutParamsExObj, FLAG_NOTCH_SUPPORT);
} catch (Exception e) {
Log.e(TAG, "other Exception");
}
}
/**
* 设置应用窗口在华为刘海屏手机不使用刘海
*/
private static void setNotDisplayInNotchAtHuawei(Window window) {
if (window == null) {
return;
}
try {
WindowManager.LayoutParams layoutParams = window.getAttributes();
Class layoutParamsExCls = Class.forName("com.huawei.android.view.LayoutParamsEx");
Constructor con = layoutParamsExCls.getConstructor(WindowManager.LayoutParams.class);
Object layoutParamsExObj = con.newInstance(layoutParams);
Method method = layoutParamsExCls.getMethod("clearHwFlags", int.class);
method.invoke(layoutParamsExObj, FLAG_NOTCH_SUPPORT);
} catch (Exception e) {
Log.e(TAG, "hw clear notch screen flag api error");
}
}
/********************************************* OPPO *********************************************/
/**
* 判断该 OPPO 手机是否为刘海屏手机
*/
private static boolean hasNotchInOppo(Context context) {
return context.getPackageManager().hasSystemFeature("com.oppo.feature.screen.heteromorphism");
}
/**
* 刘海高度和状态栏的高度是一致的
*
* @param context
* @return
*/
private static int getStatusBarHeight(Context context) {
int resId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resId > 0) {
return context.getResources().getDimensionPixelSize(resId);
}
return 0;
}
/********************************************* VIVO *********************************************/
/**
* Vivo判断是否有刘海, Vivo的刘海高度小于等于状态栏高度
*/
private static final int VIVO_NOTCH = 0x00000020;//是否有刘海
private static final int VIVO_FILLET = 0x00000008;//是否有圆角
private static boolean hasNotchInVivo(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", "hasNotchInVivo ClassNotFoundException");
} catch (NoSuchMethodException e) {
Log.e("Notch", "hasNotchInVivo NoSuchMethodException");
} catch (Exception e) {
Log.e("Notch", "hasNotchInVivo Exception");
} finally {
return ret;
}
}
/********************************************* 小米 *********************************************/
/**
* 是否有刘海
*
* @return
*/
private static boolean hasNotchInMIUI() {
try {
Method getInt = Class.forName("android.os.SystemProperties").getMethod("getInt", String.class, int.class);
int notch = (int) getInt.invoke(null, "ro.miui.notch", 0);
return notch == 1;
} catch (Throwable ignore) {
}
return false;
}
/**
* 设置显示到刘海区域
* 0x00000100 | 0x00000200 竖屏绘制到耳朵区
* 0x00000100 | 0x00000400 横屏绘制到耳朵区
* 0x00000100 | 0x00000200 | 0x00000400 横竖屏都绘制到耳朵区
*/
private static void setDisplayInNotchAtMIUI(Activity activity) {
int flag = 0x00000100 | 0x00000200 | 0x00000400;
try {
Method method = Window.class.getMethod("addExtraFlags",
int.class);
method.invoke(activity.getWindow(), flag);
} catch (Exception ignore) {
}
}
/**
* 获取刘海宽高
*/
private static int getNotchHeightInMIUI(Context context) {
int resourceId = context.getResources().getIdentifier("notch_height", "dimen", "android");
if (resourceId > 0) {
return context.getResources().getDimensionPixelSize(resourceId);
}
return 0;
}
}
其他手机厂商(华为,小米,oppo,vivo)适配 华为:devcenter-test.huawei.com/consumer/cn… 小米:dev.mi.com/console/doc… Oppo:open.oppomobile.com/service/mes… Vivo:dev.vivo.com.cn/documentCen…