Android 浅谈适配全面屏、刘海屏、水滴屏

对刘海屏、水滴屏做适配前,先在此给出一个基本概念:何谓刘海屏?何谓水滴屏?

Android 浅谈适配全面屏、刘海屏、水滴屏

上述两种屏幕都可以统称为刘海屏,不过对于右侧较小的刘海,业界一般称为水滴屏或美人尖。

目前国内流行的手机厂商主要有:vivo、oppo、华为、小米。各厂商对刘海屏的适配都大不相同,各自有各自对刘海屏的适配API,具体的适配方法可以阅读相应的官网:

VIVO:https://dev.vivo.com.cn/documentCenter/doc/103

OPPO:https://open.oppomobile.com/wiki/doc#id=10159

小米:https://dev.mi.com/console/doc/detail?pId=1293

华为:https://developer.huawei.com/consumer/cn/devservice/doc/50114?from=timeline

具体的适配方法这里不作一一介绍,按照以上四大厂商官网所给出的适配方法,这里给出四大厂商判断/获取刘海屏的工具类:

/**
 * xiaomi、huawei、vivo、oppo流行机型异型屏判断工具类
 */
public class NotchScreenTool {

    //刘海屏、水滴屏等异型屏支持的Android系统版本:8.0-》全面屏  8.0以上-》刘海屏、水滴屏等异型屏
    public static boolean isNotchSupportVersion(){
        int curApiVersion = Build.VERSION.SDK_INT;
        if(curApiVersion > 26){
            return true;
        }
        return false;
    }

    //获取手机屏幕的旋转角度
    public static int getScreenAngle(Context context){
        return ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getRotation();
    }

    //检查流行机型是否存在刘海屏
    public static boolean isNotch(Context context){
        return isNotch_VIVO(context) || isNotch_OPPO(context) || isNotch_HUAWEI(context) || isNotch_XIAOMI(context);
    }

    //检查vivo是否存在刘海屏、水滴屏等异型屏
    public static boolean isNotch_VIVO(Context context){
        boolean isNotch = false;
        try {
            ClassLoader cl = context.getClassLoader();
            Class cls = cl.loadClass("android.util.FtFeature");
            Method method = cls.getMethod("isFeatureSupport", int.class);
            isNotch = (boolean) method.invoke(cls,0x00000020);//0x00000020:是否有刘海  0x00000008:是否有圆角
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }finally {
            return isNotch;
        }
    }

    //检查oppo是否存在刘海屏、水滴屏等异型屏
    public static boolean isNotch_OPPO(Context context){
        boolean isNotch = false;
        try {
            isNotch = context.getPackageManager().hasSystemFeature("com.oppo.feature.screen.heteromorphism");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            return isNotch;
        }
    }

    //检查huawei是否存在刘海屏、水滴屏等异型屏
    public static boolean isNotch_HUAWEI(Context context) {
        boolean isNotch = false;
        try {
            ClassLoader cl = context.getClassLoader();
            Class cls = cl.loadClass("com.huawei.android.util.HwNotchSizeUtil");
            Method method = cls.getMethod("hasNotchInScreen");
            isNotch = (boolean) method.invoke(cls);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            return isNotch;
        }
    }

    //检查xiaomi是否存在刘海屏、水滴屏等异型屏
    public static boolean isNotch_XIAOMI(Context context){
        boolean isNotch = false;
        try {
            ClassLoader cl = context.getClassLoader();
            Class cls = cl.loadClass("android.os.SystemProperties");
            Method method = cls.getMethod("getInt", String.class, int.class);
            isNotch = ((int) method.invoke(null, "ro.miui.notch", 0) == 1);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }finally {
            return isNotch;
        }
    }

    //获取huawei刘海屏、水滴屏的宽度和高度:int[0]值为刘海宽度 int[1]值为刘海高度
    public static int[] getNotchSize_HUAWEI(Context context) {
        int[] notchSize = new int[]{0, 0};
        try {
            ClassLoader cl = context.getClassLoader();
            Class cls = cl.loadClass("com.huawei.android.util.HwNotchSizeUtil");
            Method method = cls.getMethod("getNotchSize");
            notchSize = (int[]) method.invoke(cls);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            return notchSize;
        }
    }

    //获取xiaomi刘海屏、水滴屏的宽度和高度:int[0]值为刘海宽度 int[1]值为刘海高度
    public static int[] getNotchSize_XIAOMI(Context context){
        int[] notchSize = new int[]{0,0};
        if(isNotch_XIAOMI(context)) {
            int resourceWidthId = context.getResources().getIdentifier("notch_width", "dimen", "android");
            if (resourceWidthId > 0) {
                notchSize[0] = context.getResources().getDimensionPixelSize(resourceWidthId);
            }
            int resourceHeightId = context.getResources().getIdentifier("notch_height", "dimen", "android");
            if (resourceHeightId > 0) {
                notchSize[1] = context.getResources().getDimensionPixelSize(resourceHeightId);
            }
        }
        return notchSize;
    }

    //获取vivo、oppo刘海屏、水滴屏的高度:官方没有给出标准的获取刘海高度的API,由于大多情况是:状态栏≥刘海,因此此处获取刘海高度采用状态栏高度
    public static int getNotchHeight(Context context){
        int notchHeight = 0;
        if(isNotch_VIVO(context) || isNotch_OPPO(context)) {
            //若不想采用状态栏高度作为刘海高度或者可以采用官方给出的刘海固定高度:vivo刘海固定高度:27dp(need dp2px)  oppo刘海固定高度:80px
            int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
            if (resourceId > 0) {
                notchHeight = context.getResources().getDimensionPixelSize(resourceId);
            }
        }
        return notchHeight;
    }

    //dp转px
    private int dp2px(Context context,float dpValue){
        float scale=context.getResources().getDisplayMetrics().density;
        return (int)(dpValue*scale+0.5f);
    }
}

若需要对厂商进行判断可以使用:

//判断手机厂商:华为、小米、oppo、vivo
String brand =android.os.Build.BRAND.toLowerCase();
if("huawei".equals(brand)){
    //...
}else if("xiaomi".equals(brand)){
    //...
}else if("vivo".equals(brand)){
    //...
}else if("oppo".equals(brand)){
    //...
}

根据四大厂商官网所提供的适配方案,其中需要在AndroidManifest中添加标签(具体说明请浏览官网):

<!-- 适配全面屏 Android O vivo&oppo-->
<meta-data android:name ="android.max_aspect" android:value ="2.2" />
<!-- 适配刘海屏、水滴屏 Android O 小米 -->
<meta-data android:name="notch.config" android:value="portrait|landscape"/>
<!-- 适配刘海屏、水滴屏 Android O 华为 -->
<meta-data android:name="android.notch_support" android:value="true"/>

在对于Android P的适配中Google给出了统一的方案(基于Android API 28):

1、AndroidManifest中添加标签:

<!--适配刘海屏、水滴屏 Android P -->
<meta-data android:name="android.vendor.full_screen" android:value="true"/>

2、把TargetVersion提到28,miniVersion提到23

3、刘海屏判别方法

在Build.VERSION.SDK_INT >= 28中提供了以下接口:

DisplayCutout类接口:主要用于获取凹口位置和安全区域的位置等。

方法 接口说明
getBoundingRects() 返回Rects的列表,每个Rects都是显示屏上非功能区域的边界矩形。
getSafeInsetLeft() 返回安全区域距离屏幕左边的距离,单位是px。
getSafeInsetRight()   返回安全区域距离屏幕右边的距离,单位是px。
getSafeInsetTop() 返回安全区域距离屏幕顶部的距离,单位是px。
getSafeInsetBottom() 返回安全区域距离屏幕底部的距离,单位是px。
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);
            }
        }
     }
 });

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区域。
if ((Build.VERSION.SDK_INT >= 28)) {
    // 使用官方api判断
    WindowManager.LayoutParams lp = getWindow().getAttributes();
    lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
    getWindow().setAttributes(lp);    
    //不显示状态栏             
    getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
    getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,WindowManager.LayoutParams.FLAG_FULLSCREEN);
    getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
    //初始化时先写适配刘海屏的方法,再判断是否有刘海屏
    getCutoutLengthAndroidP(context,window);
    return;
}