Android刘海屏适配方案总结

前言
目前市面上的刘海屏和水滴屏手机越来越多了,颜值方面是因人而异,有的人觉得很好看,也有人觉得丑爆了,我个人觉得是还可以。但是作为移动开发者来说,这并不是一件好事,越来越多异形屏手机的出现意味着我们需要投入大量精力在适配上(就不提之后会出的折叠屏手机了)。本文总结了当下主流手机的刘海屏适配方案,鉴于目前Android碎片化的情况,想要覆盖所有的机型是不可能的,但是能适配一些是一些,总比什么都不做要好。

所谓刘海屏,指的是手机屏幕正上方由于追求极致边框而采用的一种手机解决方案。因形似刘海儿而得名——来自百度百科,水滴屏也是类似,为了简单起见,下文就统称这两种为刘海屏了。

刘海屏手机

什么时候需要适配

这里先上一张官方的图

刘海屏手机危险区域和安全区域示意图

从图中可以看出,刘海区域是镶嵌在状态栏内部的,刘海区域的高度一般是不超过状态栏高度的。因此,当我们的应用布局需要占据状态栏来显示时,就需要考虑到刘海区域是否会遮挡住页面上的控件或者背景,这就是为什么将状态栏区域称为危险区域。如果应用不需要占据状态栏显示,全部显示在安全区域内,那么恭喜你,不需要做任何适配处理。总结来说,只有当应用需要全屏显示时才需要进行适配。
全屏显示无非就是两种情况:第一种是我们常说的沉浸式状态栏,也就是状态栏透明,页面的布局延伸到状态栏显示,这种情况下状态栏依然可见;第二种是类似应用的闪屏页风格,页面全屏显示,状态栏不可见。这两种情况下如果不进行适配处理都会产生一些问题。
先来看第一种情况,沉浸式风格。需要将状态栏设置为透明,需要注意只有在Android 4.4(API Level 19)以上才支持设置透明状态栏。有两种设置方法:
方法一:为Activity设置style,添加一个属性:

<item name="android:windowTranslucentStatus">true</item>

方法二:在Activity的onCreate()中为Window添加Flag

public class ImmersiveActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_immersive);
        // 透明状态栏
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            getWindow().addFlags(
                    WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
        }
    }
}

页面的布局很简单,只包含一个按钮,为了明显,我为根布局设置了一个背景。
activity_immersive.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/ll_root"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@mipmap/bg"
    android:orientation="vertical">

    <Button
        android:layout_width="150dp"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal" />

</LinearLayout>

运行之后发现按钮会被刘海区域所遮挡,如图所示:

控件被刘海区域遮挡

再说第二种情况,全屏风格,状态栏不可见。同样有两种设置方法:
方法一:为Activity设置style,添加属性:

<item name="android:windowFullscreen">true</item>
<!-- 这里为了简单,直接从style中指定一个背景 -->
<item name="android:windowBackground">@mipmap/bg</item>

方法二:在Activity的OnCreate()中添加代码:

public class FullScreenActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 全屏显示
        getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN |
                View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
    }
}

补充说明一点,现在的手机屏幕高宽比例越来越大,我们还需要额外做一下适配才能使应用在所有手机上都能全屏显示,具体方式有两种:
方式一:在AndroidManifest.xml中配置支持最大高宽比

<meta-data android:name="android.max_aspect"  
    android:value="ratio_float" />

或者

 android:maxAspectRatio="ratio_float" (API LEVEL 26)

说明:以上两种接口可以二选一,ratio_float = 屏幕高 / 屏幕宽 (如oppo新机型屏幕分辨率为2280 x 1080, ratio_float = 2280 / 1080 = 2.11,建议设置 ratio_float为2.2或者更大)
方式二:在AndroidManifest.xml中配置支持分屏,注意验证分屏下界面兼容性

android:resizeableActivity="true"  

也可以通过设置targetSdkVersion>=24(即Android 7.0),该属性的值会默认为true,就不需要在AndroidManifest.xml中配置了。
运行之后,我们发现状态栏的部分留出了一条黑边,看上起很奇怪,这显然不是我们想要的效果。

状态栏显示黑边

如何适配

上文中已经展示了刘海屏中全屏显示带来的问题,那么如何去解决呢?

1.沉浸式状态栏的适配

其实沉浸式状态栏带来的遮挡问题与刘海屏无关,本质上是由于设置了透明状态栏导致布局延伸到了状态栏中,就算是不具有刘海屏,一定程度上也会造成布局的遮挡。不过既然刘海屏是处在状态栏当中的,那么我们就把这种情况也包含在刘海屏的适配中。清楚了原因之后,解决起来就很简单了,我们只需要让控件或布局避开状态栏显示就可以了,具体的解决方法有三种。
方法一.利用fitsSystemWindows属性
当我们给最外层View设置了android:fitsSystemWindows="true"属性后,当设置了透明状态栏或者透明导航栏后,就会自动给View添加paddingTop或paddingBottom属性,这样就在屏幕上预留出了状态栏的高度,我们的布局就不会占用状态栏来显示了。
activity_immersive.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/ll_root"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@mipmap/bg"
    android:fitsSystemWindows="true"
    android:orientation="vertical">

    <Button
        android:layout_width="150dp"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal" />

</LinearLayout>

方法二.根据状态栏高度手动设置paddingTop
这种方法的实现本质上和设置fitsSystemWindows是一样的,首先获取状态栏高度,然后设置根布局的paddingTop等于状态栏高度就可以了,代码如下:

public class ImmersiveActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_immersive);
        // 透明状态栏
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            getWindow().addFlags(
                    WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
        }
        LinearLayout llRoot = findViewById(R.id.ll_root);
        // 设置根布局的paddingTop
        llRoot.setPadding(0, getStatusBarHeight(this), 0, 0);
    }

    /**
     * 获取状态栏高度
     *
     * @param context
     * @return
     */
    public 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;
    }
}

方法三.在布局中添加一个和状态栏高度相同的View
和前两种方法原理类似,同样是让屏幕预留出状态栏的高度,这里在根布局中添加了一个透明的View,高度和状态栏高度相同。这种方法的好处是可以自定义填充状态栏View的背景,更灵活地实现我们想要的效果。

public class ImmersiveActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_immersive);
        // 透明状态栏
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            getWindow().addFlags(
                    WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
        }
        LinearLayout llRoot = findViewById(R.id.ll_root);
        View statusBarView = new View(this);
        statusBarView.setBackgroundColor(Color.TRANSPARENT);
        ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                getStatusBarHeight(this));
        // 在根布局中添加一个状态栏高度的View
        llRoot.addView(statusBarView, 0, lp);
    }

    /**
     * 获取状态栏高度
     *
     * @param context
     * @return
     */
    public 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;
    }
}

适配之后成功地将控件避开了状态栏(危险区域),如下图所示:

沉浸式状态栏适配后

2.全屏显示的适配

对于全屏显示的情况,处理起来要相对麻烦一些,下面重点说一下这种情况下的适配方案。

1.Android P及以上

谷歌官方从Android P开始给开发者提供了刘海屏相关的API,可以通过直接调用API来进行刘海屏的适配处理。
通过DisplayCutout类可以获得安全区域的范围以及刘海区域(官方的叫法是缺口)的信息,需要注意只有API Level在28及以上才可以调用。

/**
 * 获得刘海区域信息
 */
@TargetApi(28)
public void getNotchParams() {
    final View decorView = getWindow().getDecorView();
    if (decorView != null) {
        decorView.post(new Runnable() {
            @Override
            public void run() {
                WindowInsets windowInsets = decorView.getRootWindowInsets();
                if (windowInsets != null) {
                    // 当全屏顶部显示黑边时,getDisplayCutout()返回为null
                    DisplayCutout displayCutout = windowInsets.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);
                        }
                    }
                }
            }
        });
    }
}

这里我在测试时也发现了一个问题,就是如果是在style中设置了全屏模式,在适配之前,顶部状态栏区域显示一条黑边,这时候调用getDisplayCutout()获取DisplayCutout对象返回的结果是null,其实这也不难理解,因为这时候是看不出刘海区域的,但是这样会导致在适配之前无法通过DisplayCutout判断是否存在刘海屏,只能在适配后才能获取到刘海区域信息,因此只能对于所有设备都添加适配代码。
那么接下来如何进行适配呢,Android P中增加了一个窗口布局参数属性layoutInDisplayCutoutMode,该属性有三个值可以取:

LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT:默认的布局模式,仅当刘海区域完全包含在状态栏之中时,才允许窗口延伸到刘海区域显示,也就是说,如果没有设置为全屏显示模式,就允许窗口延伸到刘海区域,否则不允许。
LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER:永远不允许窗口延伸到刘海区域。
LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES:始终允许窗口延伸到屏幕短边上的刘海区域,窗口永远不会延伸到屏幕长边上的刘海区域。

还有一个LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS模式,目前已经被LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES所取代,不允许使用了,这里就不提了。
这么看可能还是有些不理解,接下来我们在一个全屏显示的页面分别设置三种布局模式,看看有什么区别。

public class FullScreenActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            WindowManager.LayoutParams lp = getWindow().getAttributes();
            // 仅当缺口区域完全包含在状态栏之中时,才允许窗口延伸到刘海区域显示
//            lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
            // 永远不允许窗口延伸到刘海区域
//            lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
            // 始终允许窗口延伸到屏幕短边上的刘海区域
            lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
            getWindow().setAttributes(lp);
        }
    }
}

三种模式下的显示效果如下图所示:

LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES

可以看出,当在全屏显示情况下,LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULTLAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER的效果是一样的,都是在状态栏显示一条黑边,也就是不允许窗口布局延伸到刘海区域,而LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES则允许窗口布局延伸到了刘海区域,这里需要注意是短边刘海区域,不过一般市面上的手机刘海区域都是在短边上的,我是没见过刘海长在“腰”上的,因此利用这个模式就实现适配了。
通过之前沉浸式状态栏的显示效果可以看出,LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT在此时是允许窗口布局延伸到刘海区域的,因此更证实了只有在全屏显示的情况下该模式才不允许窗口布局延伸到刘海区域。
适配后效果如下,现在看起来就很舒服了:

全屏显示适配后

我这里为了简单没有添加任何控件,实际开发中在全屏显示后我们仍然需要考虑刘海区域是否会遮挡显示的内容和控件,同样需要避开危险区域来显示。做法和沉浸式状态栏的适配相同,原理同样是将布局下移,预留出状态栏的高度,这里就不一一列举了。

2.Android P以下

目前市面上的刘海屏手机可以说是琳琅满目,各大厂商都在追求极致的屏占比,推出的新机型也基本上都有刘海屏,针对Android P以下的手机,我们只能依照各个厂商提供的适配方案来进行适配。我也查阅了网上的一些适配文章,主要还是针对目前主流的手机品牌,本文总结了华为、小米、Vivo和Oppo的适配方案,其他品牌的手机之后有时间的话可能会再考虑。

华为适配方案

华为官方提供的适配文档:华为刘海屏手机安卓O版本适配指导
文档中提供了很多刘海屏相关的方法,这里就不一一列举了,着重看一下我们需要用到的方法。
判断是否有刘海屏

 /**
 * 判断是否有刘海屏
 *
 * @param context
 * @return true:有刘海屏;false:没有刘海屏
 */
public static boolean hasNotch(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;
    }
}

应用页面设置使用刘海区显示
官方提供了两种适配方案:
方案一.使用新增的meta-data属性android.notch_support,在应用的AndroidManifest.xml中增加meta-data属性,此属性不仅可以针对Application生效,也可以对Activity配置生效。
使用方式如下:

<meta-data android:name="android.notch_support" android:value="true"/>

可以在Application下添加,意味着该应用的所有页面,系统都不会做竖屏场景的特殊下移或者是横屏场景的右移特殊处理。

<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.notch_support"
        android:value="true" />
     ...
</application>

也可以针对指定的Activity添加,意味着可以针对单个页面进行刘海屏适配,设置了该属性的Activity系统将不会做特殊处理。

<!-- 全屏显示页面 -->
<activity
    android:name=".ui.FullScreenActivity"
    android:screenOrientation="portrait"
    android:theme="@style/FullScreenTheme">
    <meta-data
        android:name="android.notch_support"
        android:value="true" />
</activity>

方案二.使用给window添加新增的FLAG_NOTCH_SUPPORT
代码如下:

/**
 * 设置应用窗口在刘海屏手机使用刘海区
 * <p>
 * 通过添加窗口FLAG的方式设置页面使用刘海区显示
 *
 * @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(WindowManager.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");
    }
}

官方提供的所有方法我已经放到了工具类HwNotchUtils里,可以根据需求来使用。

小米适配方案

小米官方提供的适配文档:小米刘海屏水滴屏 Android O 适配
我们同样看一下关键方法。
判断是否有刘海屏

/**
 * 判断是否有刘海屏
 *
 * @param context
 * @return true:有刘海屏;false:没有刘海屏
 */
public static boolean hasNotch(Context context) {
    boolean ret = false;
    try {
        ClassLoader cl = context.getClassLoader();
        Class SystemProperties = cl.loadClass("android.os.SystemProperties");
        Method get = SystemProperties.getMethod("getInt", String.class, int.class);
        ret = (Integer) get.invoke(SystemProperties, "ro.miui.notch", 0) == 1;
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        return ret;
    }
}

应用页面设置使用刘海区显示
小米提供的适配方案同样有两种(meta-data和Flag),使用方法和华为类似。
方案一.Application级别的控制接口
在 Application 下增加一个 meta-data,用以声明该应用窗口是否可以延伸到状态栏。

<meta-data
    android:name="notch.config"
    android:value="portrait|landscape"/>

其中,value的值可以是以下四种

"none" 横竖屏都不绘制耳朵区

"portrait" 竖屏绘制到耳朵区

"landscape" 横屏绘制到耳朵区

"portrait|landscape" 横竖屏都绘制到耳朵区

这里的耳朵区指的就是刘海区两侧的状态栏区域

虽然官方文档上说的是Application级别的,但是我觉得也可以针对某一个Activity来配置,不过由于手头上的手机条件不满足,我并没有验证,如果有小伙伴测试过的话可以反馈一下,我再修正一下这里的说法。
方案二.Window级别的控制接口
通过给Window添加Flag也可以实现将窗口布局延伸到状态栏中显示。

/*刘海屏全屏显示FLAG*/
public static final int FLAG_NOTCH_SUPPORT = 0x00000100; // 开启配置
public static final int FLAG_NOTCH_PORTRAIT = 0x00000200; // 竖屏配置
public static final int FLAG_NOTCH_HORIZONTAL = 0x00000400; // 横屏配置

/**
 * 设置应用窗口在刘海屏手机使用刘海区
 * <p>
 * 通过添加窗口FLAG的方式设置页面使用刘海区显示
 *
 * @param window 应用页面window对象
 */
public static void setFullScreenWindowLayoutInDisplayCutout(Window window) {
    // 竖屏绘制到耳朵区
    int flag = FLAG_NOTCH_SUPPORT | FLAG_NOTCH_PORTRAIT;
    try {
        Method method = Window.class.getMethod("addExtraFlags",
                int.class);
        method.invoke(window, flag);
    } catch (Exception e) {
        Log.e("test", "addExtraFlags not found.");
    }
}

官方提供的所有方法我已经放到了工具类XiaomiNotchUtils里,可以根据需求来使用。
这里说一下我的测试情况,我是用小米8测试的,系统版本已经升到了Android P,利用小米官方提供的适配方法没有效果,只能用谷歌官方针对Android P的适配方案,这一点小米的官方文档也提到了。

小米Android P刘海屏的适配

至于Android P以下版本的小米手机,我并没有测试,如果有哪位大佬测试过了发现有问题可以反馈一下。

Vivo、Oppo适配方案

Vivo官方提供的适配文档:Vivo全面屏应用适配指南
Oppo官方提供的适配文档:Oppo凹形屏适配指南
这里把Vivo和Oppo放在一起说,官方提供的资料不像华为和小米那么详细,只是提供了判断是否有刘海屏的方法。
Vivo判断是否有刘海屏

public static final int VIVO_NOTCH = 0x00000020; // 是否有刘海
public static final int VIVO_FILLET = 0x00000008; // 是否有圆角

/**
 * 判断是否有刘海屏
 *
 * @param context
 * @return true:有刘海屏;false:没有刘海屏
 */
public static boolean hasNotch(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;
    }
}

Oppo判断是否有刘海屏

/**
 * 判断是否有刘海屏
 *
 * @param context
 * @return true:有刘海屏;false:没有刘海屏
 */
public static boolean hasNotch(Context context) {
    return context.getPackageManager().hasSystemFeature("com.oppo.feature.screen.heteromorphism");
}

至于全屏显示的适配方案,通过阅读官方文档和网上的其他适配文章,我个人总结一下就是这两种品牌的手机在设置全屏显示时都无需做任何处理(前提是适配了全面屏,上文中提到过如何配置),也就是不会产生黑边,我们只需要避免布局中的内容或控件不被刘海区域所遮挡就可以了。具体的做法和沉浸式状态栏的适配相同,基本原理还是将窗口布局下移,预留出状态栏的高度。
注:由于手头没有这两种厂商的手机,因此并没有验证,这一点确实是我做得不够严谨,有好心的大佬验证之后欢迎指正。
其实我本来也想列出魅族的适配方案的,但是实在是没找到官方文档。。。如果有知道的大佬可以提供一下,我后面会把适配方案补上。
适配时的基本逻辑就是先判断手机的品牌,这里我利用了一个开源工具类项目AndroidUtilCode,提供了一个获取手机Rom信息的工具类RomUtils,用起来很方便,然后判断是否是刘海屏,针对刘海屏手机添加适配代码。完整的适配代码如下所示:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
    // Android P利用官方提供的API适配
    WindowManager.LayoutParams lp = getWindow().getAttributes();
    // 始终允许窗口延伸到屏幕短边上的缺口区域
    lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
    getWindow().setAttributes(lp);
} else {
    // Android P以下根据手机厂商的适配方案进行适配
    if (RomUtils.isHuawei() && HwNotchUtils.hasNotch(this)) {
        HwNotchUtils.setFullScreenWindowLayoutInDisplayCutout(getWindow());
    } else if (RomUtils.isXiaomi() && XiaomiNotchUtils.hasNotch(this)) {
        XiaomiNotchUtils.setFullScreenWindowLayoutInDisplayCutout(getWindow());
    }
}

还有一点需要注意的地方,如果目标页是应用的启动页面,采用这种添加代码的适配方案(我是在Activity的onCreate()中添加的),会先显示出黑边,然后变为全屏显示,就像下图这样:

如果我们使用在AndroidManifest.xml中增加meta-data属性的方案呢,上文也提到了,这种方案对于Android P版本的小米手机是没有效果的,当然了,谷歌官方提供的适配方案也无法通过这种方式配置,因此我们无法避免通过代码来适配。出现这种问题的原因是app在点击图标启动后,会先执行创建进程、应用初始化等操作,虽然时间可能很短,但还是会耗时,这会让用户认为点击应用图标没有反应,为了提升用户体验,在这期间系统会根据AndroidManifest.xml指定的主题来展示出一个Preview Window(预览窗口),常见的应用启动白屏/黑屏就是该原因导致的。
我想到的解决方案有两种:
1.设置android:windowIsTranslucent属性
在启动页的主题下添加一个属性

<item name="android:windowIsTranslucent">true</item>

设置该属性表示该窗口是半透明的,这样就不会在启动页还未加载之前显示出预览窗口,但是这样会导致点击了应用图标后不会马上显示出应用程序窗口,相当于禁用了官方的优化方案,如果应用初始化进行了过多操作,会延迟几秒才显示应用窗口,用户体验非常不好。
2.设置android:windowDisablePreview属性
在启动页的主题下添加一个属性

<item name="android:windowDisablePreview">true</item>

该属性的作用很明显,就是禁用预览窗口,也就是说系统不会使用窗口的主题来显示一个Preview Window,和windowIsTranslucent属性的效果相同,该属性同样会影响用户体验。
其实这两个属性正常情况下都不推荐使用,我也是没有想到其他的解决方案,如果大家有更好的方案,欢迎提出。

总结

虽然文中介绍了很多适配的内容,但其实在开发中需要我们适配刘海屏的情况并不多,只有两种情况需要我们进行考虑:
1.沉浸式状态栏,窗口布局延伸到了状态栏中,是否会遮挡必要的内容或控件(处在危险区域)。适配方案就是将窗口布局下移,预留出状态栏的空间。
2.全屏显示模式,不做适配的话状态栏会呈现一条黑边。适配方案是首先判断系统版本,是Android P及以上就按照官方的API来适配,否则根据手机厂商的适配方案进行适配。鉴于目前市面上Android P还没有普及,为了带来更好的用户体验,我们还是需要多花一些精力来适配各个手机厂商的刘海屏手机。
最后提示一下,本文只列出了四个当下主流手机厂商的适配方案,我自己验证过的只有华为和小米(只验证了Android P)的方案,对于Vivo和Oppo的一些结论我可能说得不对,欢迎大家指正。当然,如果大家还需要其他厂商的适配方案,也欢迎提出,我会尽力补上。
相关的代码和工具类我已经上传到了github,可以下载Demo来查看,大家一起交流
Demo地址

参考资料

Android 刘海屏适配全攻略
华为刘海屏手机安卓O版本适配指导
小米刘海屏水滴屏 Android O 适配
Vivo全面屏应用适配指南
Oppo凹形屏适配指南

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容

  • 背景 刘海屏指的是手机屏幕正上方由于追求极致边框而采用的一种手机解决方案。因形似刘海儿而得名。也有一些其他叫法:挖...
    _九卿_阅读 5,889评论 0 26
  • 网上关于刘海屏适配的文章不少,可讲清楚的却没几篇,大多是拷贝文档、长篇大论,甚至热情的贴图告诉你什么是刘海屏,到最...
    王英豪阅读 3,900评论 3 35
  • 我欧赏可以宏辱不惊的人,坚持否极泰来的人,坚信生活是彩色而又绚烂的人。我也一直想做一个宠辱泰然,得失不惊的人,但在...
    潇洒的三毛阅读 244评论 0 0
  • 从6月21日至今半个月,给非洲第四大经济体 阿尔及利亚的朋友们上了几次课,无比充实啊。虽说只有几次课,但是感觉...
    ffcerise阅读 191评论 0 0
  • 什么叫做明知吃了会胖,还要拼命吃。这种事情在我身上已经发生了N多次了!每次告诫自己这次吃完下次不能这样了,可是悲剧...
    完善中的Me阅读 168评论 0 0