简洁明了的刘海屏适配方案

作者: 木木玩Android | 来源:发表于2020-08-05 17:02 被阅读0次

    网上关于刘海屏适配的文章不少,可讲清楚的却没几篇,大多是拷贝文档、长篇大论,甚至热情的贴图告诉你什么是刘海屏,到最后你仍不确定到底是怎样的一个适配方案,才能让你的 app 真正的适配所有的刘海屏机型。
    看到这篇文章你就无需再怨恨各大厂商的跟风“刘海”了,因为刘海屏的适配十分简单。

    ok,废话说完了,开始适配。
    首先要清楚的是哪些界面需要适配刘海屏:

    有状态栏的界面:刘海区域会显示状态栏,无需适配
    全屏界面:刘海区域可能遮挡内容,需要适配

    如果你的应用里所有界面都有状态栏,那么恭喜你,你不用做任何操作,状态栏就那么自然的显示在刘海区域,毫无违和,刘海屏已适配完毕,可以点叉出去了。
    不幸的是,你的应用中很大几率会有全屏界面,所谓的刘海屏适配,也正是针对这些全屏界面。
    如果你什么都不做,默认规则不允许全屏界面内容显示到刘海区域,即刘海屏区域会保留一条黑边,你的全屏界面会在刘海下方展示,这看起来好像也是可以接受的,然后你竟说服产品达成共识,“无为而治”才是最强大的刘海屏适配方案!
    但有些手机厂商(譬如oppo)不开心了,我辛辛苦苦研发的刘海屏手机,你们这些开发者竟直接放弃刘海区域!然后就在你的全屏界面下方加了一条提示:“全屏显示”,当用户点击开启后,强行把你的全屏界面显示到刘海区域,然后一切都乱套了...
    嗯~ “无为而治”行不通。
    只能允许全屏界面内容显示到刘海区域了,参考各大厂商的适配文档,我们可以知道如何允许,比如华为机型只需在 AndroidManifest 中配置:

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

    配置后,华为机型上的全屏界面就会显示到刘海区域了,但这个刘海,是可能挡住我们全屏界面中的内容的。这时需要将全屏界面中的视图元素适当下移,保证不会被刘海遮挡住,就 ok 了。
    这里我们搞清楚:允许全屏界面内容显示到刘海区域的机型,才需要将全屏界面中的视图元素适当下移。
    比如若只允许华为机型全屏界面内容显示到刘海区域,那只有华为的刘海屏机型才需要将全屏界面中的视图元素适当下移,其他厂商的刘海屏机型则不需要下移。
    如果允许华为、小米、oppo、vivo 全屏界面内容显示到刘海区域,那么华为、小米、oppo、vivo 刘海屏机型需要将全屏界面中的视图元素适当下移。
    另外也不一定要通过全屏界面中的视图元素适当下移方式来适配刘海屏,如果产品形态允许的话,你也可以让该界面显示状态栏啊。
    至此刘海屏适配完毕,是不是很简单!?
    最后代码奉上,拿走不谢:
    允许全屏界面内容显示到刘海区域配置:

     <!--允许绘制到oppo、vivo刘海屏机型的刘海区域 -->
            <meta-data
                android:name="android.max_aspect"
                android:value="2.2" />
    
            <!-- 允许绘制到华为刘海屏机型的刘海区域 -->
            <meta-data
                android:name="android.notch_support"
                android:value="true" />
    
            <!-- 允许绘制到小米刘海屏机型的刘海区域 -->
            <meta-data
                android:name="notch.config"
                android:value="portrait" />
    

    上面在 AndroidManifest 的配置在 Android 9.0 之前有效,9.0 系统针对刘海屏制定了新的 api,默认保留一条黑边,即不允许绘制到刘海区域。所以如果你还没有适配 Android 9.0,那在判断是否是允许全屏界面内容显示到刘海区域的刘海屏机型时,就要加上版本判断。
    判断是否是允许全屏界面内容显示到刘海区域的刘海屏机型:

    public class CutoutUtil {
    
        private static Boolean sAllowDisplayToCutout;
    
        /**
         * 是否为允许全屏界面显示内容到刘海区域的刘海屏机型(与AndroidManifest中配置对应)
         */
        public static boolean allowDisplayToCutout() {
            if (sAllowDisplayToCutout == null) {
                if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O_MR1) {
                    // 9.0系统全屏界面默认会保留黑边,不允许显示内容到刘海区域
                    return sAllowDisplayToCutoutDevice = false;
                }
                Context context = App.get();
                if (hasCutout_Huawei(context)) {
                    return sAllowDisplayToCutout = true;
                }
                if (hasCutout_OPPO(context)) {
                    return sAllowDisplayToCutout = true;
                }
                if (hasCutout_VIVO(context)) {
                    return sAllowDisplayToCutout = true;
                }
                if (hasCutout_XIAOMI(context)) {
                    return sAllowDisplayToCutout = true;
                }
                return sAllowDisplayToCutout = false;
            } else {
                return sAllowDisplayToCutout;
            }
        }
    
        /**
         * 是否是华为刘海屏机型
         */
        @SuppressWarnings("unchecked")
        private static boolean hasCutout_Huawei(Context context) {
            if (!Build.MANUFACTURER.equalsIgnoreCase("HUAWEI")) {
                return false;
            }
            try {
                ClassLoader cl = context.getClassLoader();
                Class HwNotchSizeUtil = cl.loadClass("com.huawei.android.util.HwNotchSizeUtil");
                if (HwNotchSizeUtil != null) {
                    Method get = HwNotchSizeUtil.getMethod("hasNotchInScreen");
                    return (boolean) get.invoke(HwNotchSizeUtil);
                }
                return false;
            } catch (Exception e) {
                return false;
            }
        }
    
        /**
         * 是否是oppo刘海屏机型
         */
        @SuppressWarnings("unchecked")
        private static boolean hasCutout_OPPO(Context context) {
            if (!Build.MANUFACTURER.equalsIgnoreCase("oppo")) {
                return false;
            }
            return context.getPackageManager().hasSystemFeature("com.oppo.feature.screen.heteromorphism");
        }
    
        /**
         * 是否是vivo刘海屏机型
         */
        @SuppressWarnings("unchecked")
        private static boolean hasCutout_VIVO(Context context) {
            if (!Build.MANUFACTURER.equalsIgnoreCase("vivo")) {
                return false;
            }
            try {
                ClassLoader cl = context.getClassLoader();
                @SuppressLint("PrivateApi")
                Class ftFeatureUtil = cl.loadClass("android.util.FtFeature");
                if (ftFeatureUtil != null) {
                    Method get = ftFeatureUtil.getMethod("isFeatureSupport", int.class);
                    return (boolean) get.invoke(ftFeatureUtil, 0x00000020);
                }
                return false;
            } catch (Exception e) {
                return false;
            }
        }
    
        /**
         * 是否是小米刘海屏机型
         */
        @SuppressWarnings("unchecked")
        private static boolean hasCutout_XIAOMI(Context context) {
            if (!Build.MANUFACTURER.equalsIgnoreCase("xiaomi")) {
                return false;
            }
            try {
                ClassLoader cl = context.getClassLoader();
                @SuppressLint("PrivateApi")
                Class SystemProperties = cl.loadClass("android.os.SystemProperties");
                Class[] paramTypes = new Class[2];
                paramTypes[0] = String.class;
                paramTypes[1] = int.class;
                Method getInt = SystemProperties.getMethod("getInt", paramTypes);
                //参数
                Object[] params = new Object[2];
                params[0] = "ro.miui.notch";
                params[1] = 0;
                return (Integer) getInt.invoke(SystemProperties, params) == 1;
            } catch (Exception e) {
                return false;
            }
        }
    
    }
    

    上面提到,不一定要通过全屏界面中的视图元素适当下移方式来适配刘海屏,如果产品形态允许的话,也可以让该界面显示状态栏。
    显示状态栏的方案是较为通用简单的,或者说,在一个应用中,一些全屏界面往往是允许使用显示状态栏的方案来适配的,如果你考虑使用这种方案,那便会是这种效果:

    在你的应用中,你期望某些全屏界面在刘海屏机型上必须全屏展示,那你就自行将界面元素适当下移,从而避免被刘海遮挡;而某些全屏界面不是非要全屏显示,允许在刘海屏机型显示状态栏,那就通过显示状态栏的方式,从而避免被刘海遮挡。

    为了实现这种效果,我们需要标记区分哪些界面必须全屏展示、哪些界面允许显示状态栏。这里提供一种实现方式,让允许显示状态栏的界面 Activity 继承一个接口,比如:

    public interface CutoutAdapt {
    }
    

    然后在 ActivityLifecycleCallbacks 回调,统一适配允许通过显示状态栏的全屏界面:

    @Override
    public void onActivityStarted(Activity activity) {
        // 如果是允许全屏显示到刘海屏区域的刘海屏机型
        if (CutoutUtil.allowDisplayToCutout()) {
            if (isFullScreen(activity)) {
                // 如果允许通过显示状态栏方式适配刘海屏
                if (activity instanceof CutoutAdapt) {
                    // 显示状态栏
                    StatusBarUtil.showStatusbar(activity.getWindow());
                } else {
                    // 需自行将该界面视图元素下移,否则可能会被刘海遮挡
                }
            } else {
                // 非全屏界面无需适配刘海屏
            }
        }
    }
    

    写在最后


    我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在IT学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。但苦于知识传播途径有限,很多程序员朋友无法获得正确的资料得到学习提升,故此将并将重要的Android进阶资料包括自定义view、性能优化、MVC与MVP与MVVM三大框架的区别、NDK技术、阿里面试题精编汇总、常见源码分析等学习资料免费分享出来。

    知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。包含知识脉络 + 诸多细节,由于篇幅有限,下面只是以图片的形式给大家展示一部分。

    Android学习PDF+学习视频+面试文档+知识点笔记

    【Android高级架构视频学习资源】

    Android部分精讲视频领取学习后更加是如虎添翼!进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!

    【Android进阶学习视频】、【全套Android面试秘籍】可以简信我【学习】查看免费领取方式!

    作者:Android面试官
    链接:https://juejin.im/post/6857128163035365390
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    相关文章

      网友评论

        本文标题:简洁明了的刘海屏适配方案

        本文链接:https://www.haomeiwen.com/subject/podorktx.html