Android 刘海屏的适配

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,用以声明该应用是否使用耳朵区



//其中,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 模式时,以上接口会返回以下值:

 

竖屏

横屏(刘海在左边)

getBoundingRects()

(201, 0 - 879, 90)

(0, 201 - 90, 879)

getSafeInsetLeft() 

0

90

getSafeInsetTop()

90

0

getSafeInsetRight() 

0

0

getSafeInsetBottom()

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中配置方可全屏显示,否则将以非全屏显示。

配置支持最大高宽比

* 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一致

如何判断是刘海屏机型

Android 刘海屏的适配_第1张图片

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()

是否是刘海屏手机:
true:是刘海屏;false:非刘海屏。

 

 

 

调用范例

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生效,意味着该应用的所有页面,系统都不会做竖屏场景的特殊下移或者是横屏场景的右移特殊处理


方案二

使用给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());
        }
    }
});

华为的刘海是可以提供用户选择是否打开

Android 刘海屏的适配_第2张图片

读取开关状态调用范例

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 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 刘海屏的适配_第3张图片

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 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. 设置是否延伸到刘海区显示接口:

https://developer.android.com/reference/android/view/WindowManager.LayoutParams#layoutInDisplayCutoutMode

属性 属性说明
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

 

 

 

 

 

https://developer.android.com/reference/android/view/WindowManager.LayoutParams.html#LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES

常量 常量说明
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 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

感兴趣的话可自我前往阅读尝试,仅为个人喜好分享,不喜者请屏蔽。

 

参考文案

小米适配

华为适配

Oppo 适配

VIVO 适配

你可能感兴趣的:(技术)