Android开发Android开发经验谈Android适配

Android齐刘海适配完全攻略

2018-09-14  本文已影响24人  啸天AskSky
齐刘海.jpeg

背景

Apple 一直在引领设计的潮流,自从 iPhone X 发布之后,"刘海屏" 就一直存在争议。但用户体验的提升,越来越多的Android厂家逐渐开始在自家旗舰机上使用刘海屏,尤其是 Android P 发布之后,也从系统级支持凹槽屏幕设计。

目前在国内比较常见的就是 OPPO x21 和 华为 P20。对于刘海屏,在享受其带来的视觉体验上的同事,我们也要对自家APP做相应的适配,才能最大化的利用刘海屏。

哪些页面需要单独适配

就算是增加了刘海屏,你也可以发现,大部分都是“切割”的状态栏的区域,所以就面临了三种情况。

  1. 有状态栏的页面,不会收到刘海屏的影响。
  2. 全屏未适配刘海屏的页面,系统会对刘海屏区域进行切割,让整体 UI 页面做下移处理,避开刘海屏的显示。
  3. 全屏已适配刘海屏的页面,可以兼容刘海屏,做到真正的全屏显示。

因此,目前我们需要做适配的页面只有全屏无状态栏的页面进行适配即可

技术适配刘海屏

概念说明

为了方便讨论,此处引用小米官方文档引用的概念:

1、基于Google提供的API进行适配

在全屏模式下,我们需要有办法获取到刘海屏凹槽的高度,才可以做到设计和布局的时候,留出安全距离。
虽然 Google 要求,刘海屏的凹槽,必须和刘海的高度保持一致,而刘海屏又被隐藏在状态栏了,所以有一个思路是直接获取状态栏的高度,来判断刘海之外,可布局的安全区域。
不过 Android P 已经预留出了标准的测量 刘海屏凹槽 的API:DisplayCutout

此方案仅支持Android9.0以上(API>28)

允许Window扩展到刘海区域

Google官方支持三种刘海屏支持模式:

如果开发者未作任何声明,则会按默认模式处理。以下将具体介绍这三种模式的表现。

默认模式 - (LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT)

为了在不影响操作的情况下,尽可能利用刘海屏的显示区域,有以下表现:

非全屏(normal mode) 全屏(fullscreen mode)
竖屏(portrait mode) 使用耳朵区 禁用耳朵区
横屏(landscape mode) 禁用耳朵区 禁用耳朵区

注:所谓全屏(fullscreen mode),指隐藏状态栏(status bar),即通过 SYSTEM_UI_FLAG_FULLSCREEN 实现的效果。

**刘海区绘制模式 ** - ( LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES)

如上所述,默认模式下某些场景会禁用耳朵区,那是因为这些场景下,系统无法判断开发者是否会把控件放置在耳朵区,所以只好默认禁用。如果开发者想要在那些场景下使用耳朵区,需要主动声明,即使用 LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES 来主动声明。

刘海区不绘制模式 - (LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER )

开发者选用这个模式后,意味着不绘制内容到耳朵区。如非必需,我们不建议采用这种模式,因为那样会浪费不少屏幕空间,用户体验不佳。

当开发者选用 NEVER 模式时, DisplayCutout 对象 的方法都会返回空值,因为 Google 认为既然开发者不使用耳朵区,就不需要关心刘海的大小了。

设置刘海区绘制模式:

注意:Android可能不允许内容视图与状态栏重叠,因此,需要使用View.setSystemUiVisibility(int) 来设置一下属性:

  • SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN //全屏
  • SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION //隐藏导航栏
  • SYSTEM_UI_FLAG_LAYOUT_STABLE //不变

通过代码规避重要内容

view.postDelayed(new Runnable() {
    @Override
    public void run() {
        DisplayCutout displayCutout = view.getRootWindowInsets().getDisplayCutout();
        Log.i("AskSky", "SafeInsetBottom:" + displayCutout.getSafeInsetBottom());
        Log.i("AskSky", "SafeInsetLeft:" + displayCutout.getSafeInsetLeft());
        Log.i("AskSky", "SafeInsetRight:" + displayCutout.getSafeInsetRight());
        Log.i("AskSky", "SafeInsetTop:" + displayCutout.getSafeInsetTop());
    }
}, 100);

使用官方API,可以获取到凹槽的上下左右的距离,这样就可以根据返回的尺寸,合理规划布局,规避无法显示的区域。

获取状态栏高度方法

由于 Notch 设备的状态栏高度与正常机器不一样,因此在需要使用状态栏高度时,不建议写死一个值,而应该改为读取系统的值。

以下是获取当前设备状态栏高度的方法:

int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
    result = context.getResources().getDimensionPixelSize(resourceId);
}

其他注意事项

默认裁切模式可能导致内容上下移动 显示状态栏时原点坐标的差异

同理,在处理MotionEvent事件时,尽量使用 MotionEvent.getX()MotionEvent.getY() 来避免坐标的问题,不要使用 MotionEvent.getRawX()MotionEvent.getRawY()

测试结果

如果没有刘海屏设备,可以使用Google提供的模拟器,在开发者选项中打开支持模拟具有凹口的显示屏(Simulate a display with a cutout)

模拟刘海屏

2、基于不同厂商的刘海屏适配方案

小米全面屏适配方案 - 官方文档

1、判断是否全面屏

系统增加了 property ro.miui.notch,值为1时则是 Notch 屏手机。

SystemProperties.getInt("ro.miui.notch", 0) == 1;

2、 应用页面设置使用刘海区显示

使用新增的Meta-data属性notch.config,在应用的AndroidManifest.xml中增加meta-data属性,此属性不仅可以针对Application生效,也可以对Activity配置生效

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

其中,value 的取值可以是以下4种:

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

注:一旦开发者声明了meta-data,系统就会优先遵从开发者的声明。

其中,extraFlags 有以下变量:

0x00000100 开启配置
0x00000200 竖屏配置
0x00000400 横屏配置

组合后表示 Window 的配置,如:

0x00000100 | 0x00000200 竖屏绘制到耳朵区
0x00000100 | 0x00000400 横屏绘制到耳朵区
0x00000100 | 0x00000200 | 0x00000400 横竖屏都绘制到耳朵区

控制 extraFlags 时注意只控制这几位,不要影响其他位。可以用 Window 的 addExtraFlags 和 clearExtraFlags 来修改, 这两个方法是 MIUI 增加的方法,需要反射调用。

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.");
}

3、获取挖孔区域宽高

获取当前设备刘海高度的方法:

int resourceId = context.getResources().getIdentifier("notch_height", "dimen", "android");
if (resourceId > 0) {
result = context.getResources().getDimensionPixelSize(resourceId);
}
Notch高度示意.png

获取当前设备刘海宽度的方法:

int resourceId = context.getResources().getIdentifier("notch_width", "dimen", "android");
if (resourceId > 0) {
result = context.getResources().getDimensionPixelSize(resourceId);
}
Notch宽度示意.png

4、“隐藏屏幕刘海”适配

MIUI 针对 Notch 设备,有一个“隐藏屏幕刘海”的设置项(设置-全面屏-隐藏屏幕刘海),具体表现是:系统会强制盖黑状态栏(无视应用的Notch使用声明),视觉上达到隐藏刘海的效果。但会给某些应用带来适配问题(控件/内容遮挡或过于靠边等)。

因此开发者在适配时,还需要检查开启“隐藏屏幕刘海”后,应用的页面是否显示正常。针对有问题的页面,做特殊适配。如有需要,可以通过查询以下 Global settings 来确定「隐藏屏幕刘海」是否开启了,然后再作针对性优化。

Settings.Global.getInt(mContext.getContentResolver(), "force_black", 0) == 1

华为全面屏适配方案 - 官方文档

1、判断是否全面屏

接口描述:

类文件 接口 接口说明
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("AskSky", "hasNotchInScreen ClassNotFoundException");
        } catch (NoSuchMethodException e) {
            Log.e("AskSky", "hasNotchInScreen NoSuchMethodException");
        } catch (Exception e) {
            Log.e("AskSky", "hasNotchInScreen Exception");
        }
        return ret;
    }

2、 应用页面设置使用刘海区显示

方案一:

使用新增的Meta-data属性android.notch_support,在应用的AndroidManifest.xml中增加meta-data属性,此属性不仅可以针对Application生效,也可以对Activity配置生效

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

<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:testOnly="false"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <meta-data android:name="android.notch_support" android:value="true"/>
    <activity android:name=".MainActivity">
        <intent-filter>
            <action android:name="android.intent.action.MAIN"/>
            <category android:name="android.intent.category.LAUNCHER"/>
        </intent-filter>
    </activity>
<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:testOnly="false"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity android:name=".MainActivity">
        <intent-filter>
            <action android:name="android.intent.action.MAIN"/>

            <category android:name="android.intent.category.LAUNCHER"/>
        </intent-filter>
    </activity>
    <activity android:name=".LandscapeFullScreenActivity" android:screenOrientation="sensor">
    </activity>
    <activity android:name=".FullScreenActivity">
        <meta-data android:name="android.notch_support" android:value="true"/>
    </activity>

方案二:

可以通过clearHwFlags接口清除添加的华为刘海屏Flag,恢复应用不使用刘海区显示。

接口描述:

类文件 接口 接口说明
com.huawei.android.view.LayoutParamsEx public void clearHwFlags (int hwFlags) 通过去除窗口FLAG的方式设置页面不使用刘海区显示:

调用方式:

/*刘海屏全屏显示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("AskSky", "hw clear notch screen flag api error");
    } catch (Exception e) {
        Log.e("AskSky", "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());
        }
    }
});

3、华为隐藏刘海区开关适配

华为系统提供隐藏刘海区开关供用户选择,当用户打开该开关后会出现以下几种情况

为了更好的适配刘海区开关功能,华为提供相关API来判断用户是否打开隐藏刘海区开关:

public static final String DISPLAY_NOTCH_STATUS = "display_notch_status";
int mIsNotchSwitchOpen = Settings.Secure.getInt(getContentResolver(),DISPLAY_NOTCH_STATUS, 0);  
// 0表示“默认”,1表示“隐藏显示区域”
华为刘海屏开关效果

OPPO全面屏适配方案 - 官方文档

1、判断是否全面屏

context.getPackageManager().hasSystemFeature(“com.oppo.feature.screen.heteromorphism”)

返回 true为凹形屏 ,可识别OPPO的手机是否为凹形屏。

2、 应用页面设置使用刘海区显示

方案一:

AndroidManifest.xml中配置支持最大高宽比

<meta-data android:name="android.max_aspect"  android:value="ratio_float" />
android:maxAspectRatio="ratio_float"   (Android8.0以上)
ratio_float = 屏幕高 / 屏幕宽 

(如oppo新机型屏幕分辨率为2280 x 1080, ratio_float = 2280 / 1080 = 2.11,建议设置 ratio_float为2.2或者更大)

方案二:

支持分屏,注意验证分屏下界面兼容性

android:resizeableActivity=”true” (Android7.0以上)

由于设置maxAspectRatioresizeableActivity 方案都有API版本限制,因此使用<meta-data android:name="android.max_aspect" android:value="ratio_float" />可以不需要考虑版本号兼容问题,但如果APP本身需要支持分屏,则使用方案二最佳

3、获取挖孔区域大小

获取ro.oppo.screen.heteromorphism属性值可获取凹形区域的范围,例 [ro.oppo.screen.heteromorphism]: [378,0:702,80],含义如下

调用方式:

String mProperty = ""; 
mProperty = SystemProperties.get("ro.oppo.screen.heteromorphism"); 
…… 
public static class SystemProperties { 
       public static String get(String key) { 
              String value = ""; 
              Class<?> cls = null; 
              try { 
                   cls = Class.forName("android.os.SystemProperties"); 
                   Method hideMethod = cls.getMethod("get", 

String.class); 
                   Object object = cls.newInstance(); 
                   value = (String) hideMethod.invoke(object, key); 
            } catch (ClassNotFoundException e) { 
                  Log.e("AskSky", "get error() ", e); 
            } catch (NoSuchMethodException e) { 
                  Log.e("AskSky", "get error() ", e); 
            } catch (InstantiationException e) { 
                  Log.e("AskSky", "get error() ", e); 
            } catch (IllegalAccessException e) { 
                  Log.e("AskSky", "get error() ", e); 
            } catch (IllegalArgumentException e) { 
                  Log.e("AskSky", "get error() ", e); 
            } catch (InvocationTargetException e) { 
                  Log.e("AskSky", "get error() ", e); 
            } 
            return value; 
      } 
}
凹槽区域尺寸示意图

VIVO全面屏适配方案 - 官方文档

1、判断是否全面屏

接口描述:

包名:android.util.FtFeature

接口:public static boolean isFtFeatureSupport (int mask)

参数说明:

    0x00000020  表示是否有凹槽

    0x00000008  表示是否有圆角

返回值: true表示有此特征

调用方式:

    public static final int HAS_GROOVE = 0x00000020;
    public static final int HAS_CIRCULAR = 0x00000008;

    private void initView() {
        boolean hasGroove = isFtFeatureSupport(HAS_GROOVE);
        boolean hasCircular = isFtFeatureSupport(HAS_CIRCULAR);
        Log.d("AskSky", "是否有刘海屏:" + hasGroove);
        Log.d("AskSky", "是否有圆角:" + hasCircular);
    }

    @SuppressLint("PrivateApi")
    private boolean isFtFeatureSupport(int tag) {
        boolean value = false;
        try {
            Class<?> cls = Class.forName("android.util.FtFeature");
            Method hideMethod = cls.getMethod("isFtFeatureSupport", int.class);
            Object object = cls.newInstance();
            value = (boolean) hideMethod.invoke(object, tag);
        } catch (ClassNotFoundException e) {
            Log.e("AskSky", "get error() ", e);
        } catch (NoSuchMethodException e) {
            Log.e("AskSky", "get error() ", e);
        } catch (InstantiationException e) {
            Log.e("AskSky", "get error() ", e);
        } catch (IllegalAccessException e) {
            Log.e("AskSky", "get error() ", e);
        } catch (IllegalArgumentException e) {
            Log.e("AskSky", "get error() ", e);
        } catch (InvocationTargetException e) {
            Log.e("AskSky", "get error() ", e);
        }
        return value;
    }

2、应用页面设置使用刘海区 与OPPO设置方式相同

方案一:

AndroidManifest.xml中配置支持最大高宽比

<meta-data android:name="android.max_aspect"  android:value="ratio_float" />
android:maxAspectRatio="ratio_float"   (Android8.0以上)
ratio_float = 屏幕高 / 屏幕宽 

(如oppo新机型屏幕分辨率为2280 x 1080, ratio_float = 2280 / 1080 = 2.11,建议设置 ratio_float为2.2或者更大)

方案二:

支持分屏,注意验证分屏下界面兼容性

android:resizeableActivity=”true” (Android7.0以上)

由于设置maxAspectRatioresizeableActivity 方案都有API版本限制,因此使用<meta-data android:name="android.max_aspect" android:value="ratio_float" />可以不需要考虑版本号兼容问题,但如果APP本身需要支持分屏,则使用方案二最佳

个人认为,VIVO官方文档是最坑爹的,仅告诉你方法名和参数,但没有具体Demo,只能自己闭着眼写

其次,VIVO没有提供获取挖孔屏位置的官方API,因此,在开发过程中只能获取状态栏高度,尽量避免在状态栏区域显示重要内容和文字

关于圆角边,VIVO官方建议是半径小于50dp的区域为安全区域,但官方又说了,尽量不要考虑圆角的具体半径,尽量只考虑圆角遮盖问题,尽量将内容放到安全区域。

对于全面屏适配的一些建议

1、声明 Maximum Aspect Ratio

Android 标准接口中,支持应用声明其支持的最大屏幕高宽比(maximum aspect ratio)。具体声明如下,其中的 ratio_float 被定义为是高除以宽,以 16:9 为例,ratio_float = 16/9 = 1.778 (18:9则为2.0)。

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

若开发者没有声明该属性,ratio_float 的默认值为1.86,小于2.0,因此这类应用在全面屏手机上,默认不会全屏显示,屏幕底部会留黑。考虑到将有更多 19.5:9 甚至更长的手机出现,建议开发者声明 Maximum Aspect Ratio ≥ 2.2 或更多。值得一提的是,如果应用的 android:resizeableActivity 已经设置为 true,就不必设置 Maximum Aspect Ratio 了。详见 Android 官方文档 Declaring maximum aspect ratio

2、避免内容拉伸、变形

从16:9变成18:9甚至更长的比例,图片往往被会拉伸变形,此问题常见于应用开屏图。开发者应使用更灵活的布局,以适应不同的屏幕比例。

3、充分利用屏幕空间

开发者应充分利用全面屏显示更多内容。如下图,王者荣耀已修改了 Maximum Aspect Ratio,在全面屏有更宽阔的游戏视野

王者荣耀对比2.png

4、虚拟按键适配

为了实现更高的屏占比,屏幕内的虚拟导航键就成了标准功能,如何让其应用界面在视觉上统一,同样需要开发者的积极适配。Android 已经有相关接口允许开发者自定义虚拟键的样式,以下是可供选择的样式。

虚拟键的样式.png

修改虚拟按键样式,Android 有标准的实现方式, 调用以下接口即可 window.setNavigationBarColor (int color)。在调用该接口时,还需要设置一些flag

/**
 * Sets the color of the navigation bar to {@param color}.
 *
 * For this to take effect,
 * the window must be drawing the system bar backgrounds with
 * {@link android.view.WindowManager.LayoutParams#FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS} and
 * {@link android.view.WindowManager.LayoutParams#FLAG_TRANSLUCENT_NAVIGATION} must not be set.
 *
 * If {@param color} is not opaque, consider setting
 * {@link android.view.View#SYSTEM_UI_FLAG_LAYOUT_STABLE} and
 * {@link android.view.View#SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION}.
 * <p>
 * The transitionName for the view background will be "android:navigation:background".
 * </p>
 */
public abstract void setNavigationBarColor(@ColorInt int color);

以上内容是我根据各大官方文档总结写的,如有遗漏欢迎指正。其中Android9.0适配,我废了一上午时间叭叭的全文翻译出来了,真心不容易,如有转载,请注明出处
QQ:1195211669

上一篇下一篇

猜你喜欢

热点阅读