Android 屏幕适配—刘海屏适配

1、Google官方适配方案

  • 非全屏模式下,刘海屏的高度等于状态栏的高度,此时我们不需要适配刘海屏,
  • 全屏模式下,假如不做特殊适配,那么内容区域会往下移,刘海区域会有一条黑边,需要进行特殊适配

1.1、刘海屏适配的流程:

(1)判断手机厂商
(2)判断是否有刘海屏
(3)获取刘海屏的高度
(4)根据开发需要,做指定的适配。如:将内容区域填充到刘海区域,内容往下移动刘海屏高度距离等等

1.2、 Google官方适配方案示例

package com.example.wangyiyunclass;

import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;

import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.DisplayCutout;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.Window;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.RelativeLayout;

public class MainActivity extends Activity {
    private  Button button;
    
    /**
     * 判断是否有刘海屏
     *
     * @param window
     * @return
     */
    private boolean hasDiplayCutout(Window window) {
        DisplayCutout displayCutout;
        View rootView = window.getDecorView();
        WindowInsets insets = null;

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            insets = rootView.getRootWindowInsets();
            if (insets != null) {
                displayCutout = insets.getDisplayCutout();
                if (displayCutout != null) {
                    //判断刘海屏的个数 和高度
                    if (displayCutout.getBoundingRects() != null
                            && displayCutout.getBoundingRects().size() > 0
                            && displayCutout.getSafeInsetTop() > 0) {
                        return true;
                    }
                }
            }

        }
        return false;

    }

    /**
     * 获取刘海屏的高度
     * 一般情况下 状态栏的高度 就是 刘海屏的高度
     *
     * @return
     */
    private int getDisplayCuoutHeight() {
        int resId = getResources().getIdentifier("status_bar_height","dimen","android");
        if(resId>0){
            return getResources().getDimensionPixelSize(resId);
        }
        return 96;
    }



    @RequiresApi(api = Build.VERSION_CODES.P)
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //1.设置全屏
        Window window = getWindow();
        // 去掉窗口标题
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        // 隐藏顶部的状态栏
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);

        //2.判断是否有刘海屏
        boolean hasDiplayCutout = hasDiplayCutout(window);
        //如果有刘海屏,则对刘海屏进行适配
        if (hasDiplayCutout) {
            //3.将内容区域延伸进刘海区域
            WindowManager.LayoutParams params = window.getAttributes();
            /**LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT 全屏模式,内容下移,非全屏不受影响
             *
             *LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES 允许内容延伸进刘海区域
             *
             * LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER  不允许内容延伸进刘海区域
             */
            params.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
            window.setAttributes(params);
            //4.设置成 沉寖式
            int flags = View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
            int visivility = window.getDecorView().getSystemUiVisibility();
            visivility |= flags;
            window.getDecorView().setSystemUiVisibility(visivility);
        }
        //5.获取刘海屏高度
        int height = getDisplayCuoutHeight();
        setContentView(R.layout.activity_main);
        button = findViewById(R.id.button);
        RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) button.getLayoutParams();
        params.topMargin = height;
        button.setLayoutParams(params);

    }

}

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

    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/frameLayout"

    tools:context=".MainActivity">
    <ImageView
        android:scaleType="fitXY"
        android:src="@mipmap/image_no_data"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
    
    <Button
        android:id="@+id/button"
        android:background="#F00"
        android:layout_centerHorizontal="true"
        android:layout_width="50dp"
        android:layout_height="50dp"/>
</RelativeLayout>

运行结果,将图片全屏显示,并把内容区域延伸进刘海屏,按钮的位置往下移动的距离,刚好是刘海屏的高度。

2、各个手机厂商的适配

从Google的适配方案我们知道,刘海屏适配只针对全屏的情况下,非全屏的时候,状态栏的高度一般等于刘海的高度不用适配。适配的主要流程基本上是:
(1)判断是否有刘海屏
(2)获取刘海屏的高度
(3)根据开发需要,做指定的适配。如:将内容区域填充到刘海区域,内容往下移动刘海屏高度距离等等
但是不同的厂商可能会存在刘海的高度不等于状态栏高度的情况下等等,所以各大厂商也对刘海的适配提供了API,在我们开发的时候去查阅各大手机厂商的API即可,参考网站
其他手机厂商(华为,小米,oppo,vivo)适配
华为:https://devcenter-test.huawei.com/consumer/cn/devservice/doc/50114
小米:https://dev.mi.com/console/doc/detail?pId=1293
Oppo:https://open.oppomobile.com/service/message/detail?id=61876
Vivo:https://dev.vivo.com.cn/documentCenter/doc/103

  • 以下是总结出的一些厂商的核心API:
package com.example.wangyiyunclass;

import android.content.Context;
import android.os.Build;
import android.util.Log;
import android.view.DisplayCutout;
import android.view.View;
import android.view.Window;
import android.view.WindowInsets;
import android.view.WindowManager;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class Utils {

    /**
     * Google 官方判断是否有刘海屏
     *
     * @param window
     * @return
     */
    private boolean hasDiplayCutout(Window window) {
        DisplayCutout displayCutout;
        View rootView = window.getDecorView();
        WindowInsets insets = null;

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
            insets = rootView.getRootWindowInsets();
            if (insets != null) {
                displayCutout = insets.getDisplayCutout();
                if (displayCutout != null) {
                    //判断刘海屏的个数 和高度
                    if (displayCutout.getBoundingRects() != null
                            && displayCutout.getBoundingRects().size() > 0
                            && displayCutout.getSafeInsetTop() > 0) {
                        return true;
                    }
                }
            }

        }
        return false;

    }

    /**
     * Google官方  获取刘海屏的高度
     * 一般情况下 状态栏的高度 就是 刘海屏的高度
     *
     * @return
     */
    private int getDisplayCuoutHeight(Context context) {
        int resId = context.getResources().getIdentifier("status_bar_height","dimen","android");
        if(resId>0){
            return context.getResources().getDimensionPixelSize(resId);
        }
        return 96;
    }



    /**
     * 华为手机 是否刘海
     * @param context
     * @return
     */
    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");
        }
        return ret;
    }

    /**
     * 华为手机 获取刘海尺寸:width、height,int[0]值为刘海宽度 int[1]值为刘海高度。
     * @param context
     * @return
     */
    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");
        }
        return ret;
    }

    /**
     *华为手机 设置使用刘海区域
     * @param window
     */
    public static void setFullScreenWindowLayoutInDisplayCutout(Window window) {
        if (window == null) {
            return;
        }

        try {
            WindowManager.LayoutParams layoutParams = window.getAttributes();
            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 (Exception e) {
            Log.e("test", "other Exception");
        }
    }

    /*刘海屏全屏显示FLAG*/
    public static final int FLAG_NOTCH_SUPPORT = 0x00010000;

    /**
     * 设置应用窗口在华为刘海屏手机不使用刘海
     *
     * @param window 应用页面window对象
     */
    public static void setNotFullScreenWindowLayoutInDisplayCutout(Window window) {
        if (window == null) {
            return;
        }
        try {
            WindowManager.LayoutParams layoutParams = window.getAttributes();
            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("clearHwFlags", int.class);
            method.invoke(layoutParamsExObj, FLAG_NOTCH_SUPPORT);
        } catch (Exception e) {
            Log.e("test", "hw clear notch screen flag api error");
        }
    }

    /*********
     * 1、声明全屏显示。
     *
     * 2、适配沉浸式状态栏,避免状态栏部分显示应用具体内容。
     *
     * 3、如果应用可横排显示,避免应用两侧的重要内容被遮挡。
     */


    /********************
     * 判断该 OPPO 手机是否为刘海屏手机
     * @param context
     * @return
     */
    public static boolean hasNotchInOppo(Context context) {
        return context.getPackageManager().hasSystemFeature("com.oppo.feature.screen.heteromorphism");
    }

    /**
     * 刘海高度和状态栏的高度是一致的
     * @param context
     * @return
     */
    public static int getStatusBarHeight(Context context) {
        int resId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
        if (resId > 0){
            return context.getResources().getDimensionPixelSize(resId);
        }
        return 0;
    }


    /**
     * Vivo判断是否有刘海, Vivo的刘海高度小于等于状态栏高度
     */
    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;
        }
    }

}

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