全面屏/刘海屏及虚拟键适配--总结版

Posted ZhangQiang-

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了全面屏/刘海屏及虚拟键适配--总结版相关的知识,希望对你有一定的参考价值。

什么是刘海屏?

随着iPhone X发布,刘海屏手机大行其道 ,Google android P版本的发布,也引入了刘海屏的概念 即将发布的Android p也提供了对刘海屏的支持。 华为P20 pro, vivo X21,OPPO R15 华为nova 3e,红米note6等手机厂商也纷纷推出自己的刘海屏手机app也要提前做好适配。

屏幕的正上方居中位置(下图黑色区域)会被挖掉一个孔,屏幕被挖掉的区域无法正常显示内容,这种类型的屏幕就是刘海屏,也有其他叫法:挖孔屏、凹凸屏等等,这里统一按刘海屏命名。

如果我们的的app没有适配android p的刘海屏,那么在显示的时候变会出现问题, 

1)如没有状态栏,全屏显示的App,那么在Android P版本中显示如下 

后果:导航栏中title被遮挡

2)如果有状态栏,全屏显示的App,那么在Android P版本中显示如下,你会发现UI出现了黑边,页码也看不清了 

显示内容下移,头部出现黑条,底部出现遮挡

以上都是基于标准的刘海屏设计出现的情况,还有一些厂商自定义了刘海屏,即刘海屏的高度大于了状态栏,那么就会产生类似的问题,如下 

1)状态栏背景高度写死的问题 

 

如何适配刘海屏?

由于Android p正式版今日刚发布, 当前市面上的Android 刘海屏手机还不能用Android 官方提供的方案来解决,那怎么办呢?还好几个厂商自己给出了适配方案。

华为刘海屏适配方案 - 如 P20 pro

华为刘海屏适配官方文档

华为给出的文档最为详细,P20 pro预装系统对未做刘海屏适配处理的app有一定处理,处理逻辑如下

可见,会被华为系统做偏移处理的有2种情况:

1.未设置meta-data值,页面横屏状态

2.未设置meta-data值,页面竖屏状态,不显示状态栏

这2种情况都会出现后果二。如果你的app中页面没有这两种情况,例如都是竖屏且显示状态栏,你就可以淡定地不做处理。

现在我们知道原因了就可以对症下药了,这里给出我推荐的解决方案(官方给出的解决方案不止一种,可以根据自己的需要采用) 分为4步:

1.配置meta-data

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

①对Application生效,意味着该应用的所有页面,系统都不会做竖屏场景的特殊下移或者是横屏场景的右移特殊处理:

② 对Activity生效,意味着可以针对单个页面进行刘海屏适配,设置了该属性的Activity系统将不会做特殊处理:

2.检测是否存在刘海屏

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;

    



 

3.获取刘海屏的参数

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;

    

4. UI适配

没错,第1步仅仅是告诉EMUI系统不要瞎操作你的页面,真正适配的活还要你自己干。

①判断是否刘海屏,代码上面给出了

②如果是刘海屏手机需要应用自己调整布局避开刘海区,布局原则:保证重要的文字、图片和视频信息、可点击的控件和图标还有应用弹窗等等布局建议显示在状态栏区域以下(安全区域);不重要,遮挡不会出现问题的布局可以延伸到状态栏区域(危险区域)显示,按照这种布局原则修改,可以一次修改就能适配所有的刘海屏手机:

 

vivo & OPPO刘海屏适配

OPPO刘海屏适配官方文档

vivo刘海屏适配官方文档

vivo 和 OPPO官网仅仅给出了适配指导,没有给出具体方案,简单总结为:

如有是具有刘海屏的手机,竖屏显示状态栏,横屏不要在危险区显示重要信息或者设置点击事件。

那怎么知道是不是刘海屏手机呢?

OPPO判断方法:

public static boolean hasNotchInOppo(Context context)

 return context.getPackageManager().hasSystemFeature("com.oppo.feature.screen.heteromorphism");

 

vivo的判断方法:

public static final int NOTCH_IN_SCREEN_VOIO=0x00000020;//是否有凹槽

public static final int ROUNDED_IN_SCREEN_VOIO=0x00000008;//是否有圆角

public static boolean hasNotchInScreenAtVoio(Context context)

    boolean ret = false;

    try 

        ClassLoader cl = context.getClassLoader();

        Class FtFeature = cl.loadClass("com.util.FtFeature");

        Method get = FtFeature.getMethod("isFeatureSupport",int.class);

        ret = (boolean) get.invoke(FtFeature,NOTCH_IN_SCREEN_VOIO);



     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; 



google官方对刘海屏做的支持

首选要在build.gradle设置支持 , 其实只要注意如下两个属性就好,只有如下设置,才能正确使用Android P版本,其他的build.gradle的配置可以视自己的as版本自定义即可

compileSdkVersion ‘android-P’

targetSdkVersion ‘P’

在AS创建一个Android P(API 28)的模拟器,在设置中打开刘海屏

默认的Android P的模拟器是关闭刘海屏的,首先看下如何开启Android P的模拟器中的刘海屏,

1)启用开发者选项和调试 

-> 打开 Settings 应用。 

-> 选择 System。 

-> 滚动到底部,然后选择 About phone。 

-> 滚动到底部,点按 Build number 7 次。 

-> 返回上一屏幕,在底部附近可找到 Developer options

2)在 Developer options 屏幕中,向下滚动至 Drawing 部分并选择 Simulate a display with a cutout

3)选择屏幕缺口的大小

如图所示,Google提供了四种刘海屏选择方案 

一般选第四种

这里需要注意一个问题:使用Android P模拟器的时候,这里的模拟凹口屏的四个选项其实实质只改变了刘海屏的位置和宽度,高度(系统刘海屏不会超过状态栏),但是在真机中,这四个选项有可能对应不同的操作

 

google官方刘海屏适配方案

Google默认的适配刘海屏策略是这样的 

1) 如果应用未适配刘海屏,需要系统对全屏显示的应用界面做特殊移动处理(竖屏下移处理,横屏右移处),因此此处出现了黑边的现象;如果应用页面布局不能做到自适应,就会出现布局问题;如果应用布局能够做到自适应,也会有黑边无法全屏显示的体验问题 

2)如果有状态栏的App,则不受刘海屏的影响(有状态肯定不是全屏,那么就不会有下移的风险)

google从Android P开始为刘海屏提供支持,目前提供了一个类和三种模式:

所以我们可用这个类判断是否有刘海的存在以及刘海的位置

DisplayCutout cutout = mContext.getDisplayCutout();

三种模式

A new window layout attribute, layoutInDisplayCutoutMode, allows your app to lay out its content around a device's cutouts. You can set this attribute to one of the following values:

  • LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT

  • LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES

  • LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER

    第一种模式:

    LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT :全屏窗口不会使用到刘海区域,非全屏窗口可正常使用刘海区域 //  仅仅当系统提供的bar完全包含了刘海区时才允许window扩展到刘海区,否则window不会和刘海区重叠,默认情况下,

    第二种模式:

    LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES :允许window扩展到刘海区(原文说的是短边的刘海区, 目前有刘海的手机都在短边,所以就不纠结了)

    第三种模式:

    LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER: 不允许window扩展到刘海区,窗口不允许和刘海屏重叠

public class AndroidPDemoActivity extends Activity

    protected void onCreate(Bundle savedInstanceState) 

        super.onCreate(savedInstanceState);

        //给Activity设置为无title,全屏显示,这段代码在无刘海屏显示效果如下

        requestWindowFeature(Window.FEATURE_NO_TITLE);

        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,WindowManager.LayoutParams.FLAG_FULLSCREEN);

        WindowManager.LayoutParams lp = this.getWindow().getAttributes();

        lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;

        this.getWindow().setAttributes(lp);

        setContentView(R.layout.activity_android_p_demo_layout);

    

 

上述代码换成LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER, 可以看出显示效果和DEFAULT是一致的,LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER是不允许使用刘海屏区域,而LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT全屏窗口不允许使用刘海屏区域

来看LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES

可以看出LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES已经允许全屏app使用刘海屏了,只不过状态栏那边是白色。可以看出LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES已经允许全屏app使用刘海屏了,只不过状态栏那边是白色。

 

那我们要如何修复这个问题?采用沉浸式布局即可,即设置DecorView为SYSTEM_UI_FLAG_FULLSCREEN模式。

那何为沉浸式布局?这是Android 4.4 (API Level 19)引入一个新的概念,即真正的全屏模式:SystemUI(StatusBar和NavigationBar)也都被隐藏

布局的时候,你总不能在刘海屏区域内部放控件吧,所以这个时候我们就要计算状态栏高度了,在布局时候,要刻意避开这个区域

直接上代码: 

//主activity

public class AndroidPDemoActivity extends Activity

    protected void onCreate(Bundle savedInstanceState) 

        super.onCreate(savedInstanceState);

        DisplayCutoutDemo displayCutoutDemo = new DisplayCutoutDemo(this);

        displayCutoutDemo.openFullScreenModel();

        setContentView(R.layout.activity_android_p_demo_layout);

        displayCutoutDemo.controlView();

    

/**

* 功能描述: 刘海屏控制

*/

public class DisplayCutoutDemo 

    private Activity mAc;

    public DisplayCutoutDemo(Activity ac) 

        mAc = ac;
    

    //在使用LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES的时候,状态栏会显示为白色,这和主内容区域颜色冲突,

    //所以我们要开启沉浸式布局模式,即真正的全屏模式,以实现状态和主体内容背景一致

    public void openFullScreenModel()

        mAc.requestWindowFeature(Window.FEATURE_NO_TITLE);

        WindowManager.LayoutParams lp = mAc.getWindow().getAttributes();

        lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;

        mAc.getWindow().setAttributes(lp);

        View decorView = mAc.getWindow().getDecorView();

        int systemUiVisibility = decorView.getSystemUiVisibility();

        int flags = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN

                | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION

                | View.SYSTEM_UI_FLAG_FULLSCREEN;

        systemUiVisibility |= flags;

        mAc.getWindow().getDecorView().setSystemUiVisibility(systemUiVisibility);

    

    //获取状态栏高度

    public int getStatusBarHeight(Context context) 

        int result = 0;

        int resourceId = context.getResources().getIdentifier("status_bar_height","dimen", "android");

        if (resourceId > 0) 

            result = context.getResources().getDimensionPixelSize(resourceId);
        

        Log.d("hwj","**getStatusBarHeight**" + result);

        return result;
    

    public void controlView()

        View decorView = mAc.getWindow().getDecorView();

        if(decorView != null)

            Log.d("hwj", "**controlView**" + android.os.Build.VERSION.SDK_INT);

            Log.d("hwj", "**controlView**" + android.os.Build.VERSION_CODES.P);

            WindowInsets windowInsets = decorView.getRootWindowInsets();

            if(windowInsets != null)

                if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) 

                    DisplayCutout displayCutout = windowInsets.getDisplayCutout();

                    //getBoundingRects返回List<Rect>,没一个list表示一个不可显示的区域,即刘海屏,可以遍历这个list中的Rect,

                    //即可以获得每一个刘海屏的坐标位置,当然你也可以用类似getSafeInsetBottom的api

                    Log.d("hwj", "**controlView**" + displayCutout.getBoundingRects());

                    Log.d("hwj", "**controlView**" + displayCutout.getSafeInsetBottom());

                    Log.d("hwj", "**controlView**" + displayCutout.getSafeInsetLeft());

                    Log.d("hwj", "**controlView**" + displayCutout.getSafeInsetRight());

                    Log.d("hwj", "**controlView**" + displayCutout.getSafeInsetTop());

                
            
        
    


<!--布局文件-->

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout

    xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:app="http://schemas.android.com/apk/res-auto"

    xmlns:tools="http://schemas.android.com/tools"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    android:orientation="vertical"

    android:background="@android:color/holo_blue_bright"

    tools:context="com.hwj.android.learning.AndroidPDemoActivity">



    <!--假设我们已经通过DisplayCutoutDemo.java的controlView方法提前

     获得了刘海屏的坐标,则这里坐边距就可以设置了-->

    <!--在你实际使用android p模拟器的时候,你会发现android.os.Build.VERSION_CODES.P的值居然等于10000

     顾你是无法通过模拟器去实际使用DisplayCutoutDemo类的-->

    <Button

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:layout_marginLeft="800px"/>

    <TextView

        android:id="@+id/android_p_demo_tv"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:text="Hello World!"/>

</LinearLayout>

 

这里有一个需要关注的问题,在使用Android P模拟器的时,android.os.Build.VERSION_CODES.P的值居然等于10000,这说明在实际使用过程中你是无法通过模拟器去实际使用DisplayCutoutDemo类的,也就是无法去获取刘海屏的具体坐标

 

究其原因,我猜测是Android P版本还未正式发布,这只是一个debug版本,待正式发布,这个bug应该就会修复掉了。 

但是只要我们掌握了适配原理,那就不用担心了

但是我们应该遵循的一个原则就是:不要在刘海屏那一栏显示内容,那一块我们称为非安全区域,尽量在安全区域去绘制UI

 

综述刘海屏的适配: 

1)设置LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES模式 

2)设置沉浸式布局模式 

3)计算状态栏高度,进行布局;如果有特殊UI要求,则可以使用DisplayCutoutDemo类去获取刘海屏的坐标,完成UI

 

 

 

参考链接:

 

华为刘海屏适配官方文档 https://devcenter-test.huawei.com/consumer/cn/devservice/doc/50114

OPPO刘海屏适配官方文档 https://open.oppomobile.com/service/message/detail?id=61876

vivo刘海屏适配官方文档 https://dev.vivo.com.cn/documentCenter/doc/103

Android刘海屏适配方案 https://www.jianshu.com/p/62c6625db7ab

刘海屏适配 与WindowInsets https://blog.csdn.net/yi_master/article/details/80309757

google官方刘海屏适配方案 https://developer.android.com/about/versions/pie/android-9.0

android 沉浸式状态栏   https://blog.csdn.net/carlos1992/article/details/46773059# 


小技巧:

1. //去除title  

2. requestWindowFeature(Window.FEATURE_NO_TITLE);  

3. //去掉Activity上面的状态栏  getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);  

4. //去掉虚拟按键全屏显示  

5. getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);  

 

在AndroidMainfest.xml中application中显式声明支持的最大屏幕高宽比(maximum aspect ratio),目前全面屏屏幕比例,将value设置为2.1(或者更大)即可适配一众全面屏手机

<meta-data

            android:name="android.max_aspect"

            android:value="2.1" />

函虚拟键全屏方法--在onWindowFocusChanged中调用如下函数:

/**

     * 隐藏虚拟按键,并且全屏

     */

    protected void hideBottomUIMenu() 

        //隐藏虚拟按键,并且全屏

        if (Build.VERSION.SDK_INT > 11 && Build.VERSION.SDK_INT < 19)  // lower api

            View v = this.getWindow().getDecorView();

            v.setSystemUiVisibility(View.GONE);

         else if (Build.VERSION.SDK_INT >= 19) 

            //for new api versions.

            View decorView = getWindow().getDecorView();

            int uiOptions = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION

                    | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_FULLSCREEN;

            decorView.setSystemUiVisibility(uiOptions);

        

    

 

以上是关于全面屏/刘海屏及虚拟键适配--总结版的主要内容,如果未能解决你的问题,请参考以下文章

Android刘海屏适配

刘海屏时代来临,主流应用适配情况大调查

Android 全面屏处理(适配挖孔屏刘海屏) kotlin

Android刘海屏水滴屏全面屏适配详解大厂直通车!

沉浸式,状态栏高度,刘海屏怎么开启,怎么适配?看这!

Android 刘海屏适配