屏幕适配3种方案及9.0刘海屏适配方案

时间:2024-03-15 14:47:02

今天说一下android开发中老生常谈的一个问题---屏幕适配。屏幕适配目前来说常用的有4种方案,

  • 像素适配(px适配),
  • 百分比适配,
  • 通过修改系统的density适配,
  • 对于谷歌9.0推出的刘海屏,也需要做到刘海屏适配。

我们在开发界面的时候会参考设计师给出的设计稿进行开发,如果设计稿是以px为单位,比如是以512x1920(单位px)的设备为基础,我们可以进行px适配

1、px适配

原理:所谓的px适配就是根据手机系统提供的分辨率,比如1024x1920(单位px)计算得出 宽比为1024/512=2 ,高比为1920/1920=1,根据宽高的缩放比例,实现对子view的等比缩放,即可达到适配的效果,所以大致可分为两步

第一步:计算宽高缩放比例

​
/**
 * <pre>
 *     author  : QB
 *     time    : 2019/3/25
 *     version : v1.0.0
 *     qq      : 952722763
 *     email   : [email protected]
 *     desc    : 屏幕像素适配工具类
 * </pre>
 */
public class ScreenFitUtils {
    private static ScreenFitUtils instance = null;

    //设计稿给出的像素基准值
    private static final float STANDARD_WIDTH = 512;
    private static final float STANDARD_HEIGHT = 1920;


    //当前手机的真实宽高
    private int mCurrentDeviceWidth;
    private int mCurrentDeviceHeight;

    private ScreenFitUtils(Context mContext) {
        //获取屏幕的宽高
        if(mCurrentDeviceWidth == 0 || mCurrentDeviceHeight == 0){
            WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
            if(wm != null){
                DisplayMetrics dm = new DisplayMetrics();
                wm.getDefaultDisplay().getMetrics(dm);

                //判断当前是横屏还是竖屏
                if(dm.widthPixels > dm.heightPixels){
                    mCurrentDeviceWidth = dm.heightPixels - getDeviceStatusHeight(mContext);
                    mCurrentDeviceHeight = dm.widthPixels;
                }else{
                    mCurrentDeviceWidth = dm.widthPixels;
                    mCurrentDeviceHeight = dm.heightPixels - getDeviceStatusHeight(mContext);
                }

            }
        }
    }

    /**
     * 获取状态栏高度
     * @param mContext
     */
    private int getDeviceStatusHeight(Context mContext){
        int statusID = mContext.getResources().getIdentifier("status_bar_height","dimen","android");
        //>0表示获取成功
        if(statusID > 0){
           return mContext.getResources().getDimensionPixelSize(statusID);
        }

        return 0;
    }


    /**
     * 单例
     * @return
     */

    public static ScreenFitUtils getInstance(Context mContext){
        if(instance == null){
            synchronized (ScreenFitUtils.class){
                if(instance == null){
                    //防止内存泄漏 使用 mContext.getApplicationContext()
                    instance = new ScreenFitUtils(mContext.getApplicationContext());
                }
            }
        }

        return instance;
    }

    /**
     * 获取宽度缩放比例
     */
    public float getScaleWidth(){
        return mCurrentDeviceWidth/STANDARD_WIDTH;
    }

    /**
     * 获取高度缩放比例
     */
    public float getScaleHeight(){
        return mCurrentDeviceHeight/STANDARD_HEIGHT;
    }



}

​

首先定义一个工具类,计算出宽度缩放比例和高度缩放比例,如果要实现精确的比例,可以考虑去除状态栏的高度在进行计算。

第二步:自定义RelativeLayout,当然也可以自定义其他类型的ViewGroup容器,实现子view宽高根据缩放比例进行缩放

/**
 * <pre>
 *     author  : QB
 *     time    : 2019/3/25
 *     version : v1.0.0
 *     qq      : 952722763
 *     email   : [email protected]
 *     desc    : 自定义像素适配,实现子view等比缩放
 * </pre>
 */
public class RelativePixelsFit extends RelativeLayout {



    private boolean isDraw = false;

    public RelativePixelsFit(Context context) {
        super(context);
    }

    public RelativePixelsFit(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public RelativePixelsFit(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {


        //防止二次测量,scale被放大
        if(!isDraw){
            isDraw = true;

            //获取当前缩放比例
            float scaleX = ScreenFitUtils.getInstance(getContext()).getScaleWidth();
            float scaleY = ScreenFitUtils.getInstance(getContext()).getScaleHeight();

            //对子view进行适配
            int childCount = getChildCount();
            for (int i=0;i<childCount;i++){
                //获取子view
                View childView = getChildAt(i);
                //获取当前子view布局属性实体
                LayoutParams layoutParams = (LayoutParams) childView.getLayoutParams();
                layoutParams.width = (int) (layoutParams.width*scaleX);
                layoutParams.height = (int) (layoutParams.height*scaleY);
                layoutParams.leftMargin = (int) (layoutParams.leftMargin*scaleX);
                layoutParams.rightMargin = (int) (layoutParams.rightMargin*scaleX);
                layoutParams.topMargin = (int) (layoutParams.topMargin*scaleY);
                layoutParams.bottomMargin = (int) (layoutParams.bottomMargin*scaleY);
                childView.setLayoutParams(layoutParams);
            }
        }

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    }
}

注意代码中,有个防止二次测量的标志位isDraw,不设置这个标志位会使得我们的缩放比例放大。到不到预期的效果。

实现了以上两步基本上就可以尝试一把了,看效果图:可以看到子view的宽度设置成设计稿的一半,就占据了屏幕的一半,达到了适配的效果

屏幕适配3种方案及9.0刘海屏适配方案屏幕适配3种方案及9.0刘海屏适配方案

2、百分比适配

原理:如果UI想做一个效果,没有给出一定的尺寸参考,只知道宽占据屏幕的一半,高占据屏幕的一半,居中显示,那么就可以使用这种布局方式。当然,android5.0 google 官方也推出了百分比布局支持库,可以参考https://blog.csdn.net/copy_yuan/article/details/52012141文章中做了详细的使用介绍,这里不做说明。这里主要详解自己如何写一个自定义的百分比布局库

第一步:自定义布局属性 谷歌使用的是百分比的格式,这里我们使用float形式

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

    <declare-styleable name="PercentLayout">
        <attr name="widthPercent" format="float"/>
        <attr name="heightPercent" format="float"/>
        <attr name="marginLeftPercent" format="float"/>
        <attr name="marginRightPercent" format="float"/>
        <attr name="marginTopPercent" format="float"/>
        <attr name="marginBottomPercent" format="float"/>

    </declare-styleable>

</resources>

第二步:自定义RelativeLayout,当然也可以自定义其他类型的ViewGroup容器,实现子view根据百分比进行缩放

/**
 * <pre>
 *     author  : QB
 *     time    : 2019/3/26
 *     version : v1.0.0
 *     qq      : 952722763
 *     email   : [email protected]
 *     desc    : 自定义百分比布局
 * </pre>
 */
public class RelativePercentFit extends RelativeLayout {
    public RelativePercentFit(Context context) {
        super(context);
    }

    public RelativePercentFit(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public RelativePercentFit(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //获取当前布局的尺寸
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        //获取子控件个数
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            //获取子控件的布局属性
            ViewGroup.LayoutParams vl = child.getLayoutParams();

            //判断是不是实现了该布局属性
            if(checkLayoutParams(vl)){
                PercentParams lp = (PercentParams) vl;
                float widthPercent = lp.widthPercent;
                float heightPercent = lp.heightPercent;
                float marginLeftPercent = lp.marginLeftPercent;
                float marginRightPercent = lp.marginRightPercent;
                float marginTopPercent = lp.marginTopPercent;
                float marginBottomPercent = lp.marginBottomPercent;

                //做判断
                if(widthPercent > 0){
                    vl.width = (int) (widthPercent * widthSize);
                }

                if(heightPercent > 0){
                    vl.height = (int) (heightPercent * heightSize);
                }

                if(marginLeftPercent > 0){
                    ((PercentParams) vl).leftMargin = (int) (marginLeftPercent * widthSize);
                }

                if(marginRightPercent > 0){
                    ((PercentParams) vl).rightMargin = (int) (marginRightPercent * widthSize);
                }

                if(marginTopPercent > 0){
                    ((PercentParams) vl).topMargin = (int) (marginTopPercent * heightSize);
                }

                if(marginBottomPercent > 0){
                    ((PercentParams) vl).bottomMargin = (int) (marginBottomPercent * heightSize);
                }


            }
        }

    }
    /**
     * 重写对子控件布局属性进行获取解析
     */
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new PercentParams(getContext(),attrs);
    }

    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof PercentParams;
    }

    /**
     * 自定义布局属性对象 继承自父亲 保留父亲的布局属性
     */
    public static class PercentParams extends RelativeLayout.LayoutParams{
        private float widthPercent;
        private float heightPercent;
        private float marginLeftPercent;
        private float marginRightPercent;
        private float marginTopPercent;
        private float marginBottomPercent;

        public PercentParams(Context c, AttributeSet attrs) {
            super(c, attrs);
            //解析属性值
            TypedArray ta = c.obtainStyledAttributes(attrs, R.styleable.PercentLayout);
            widthPercent = ta.getFloat(R.styleable.PercentLayout_widthPercent,0);
            heightPercent = ta.getFloat(R.styleable.PercentLayout_heightPercent,0);
            marginLeftPercent = ta.getFloat(R.styleable.PercentLayout_marginLeftPercent,0);
            marginRightPercent = ta.getFloat(R.styleable.PercentLayout_marginRightPercent,0);
            marginTopPercent = ta.getFloat(R.styleable.PercentLayout_marginTopPercent,0);
            marginBottomPercent = ta.getFloat(R.styleable.PercentLayout_marginBottomPercent,0);
            ta.recycle();
        }
    }
}

通过使用谷歌的依赖库发现,我们使用它提供的自定义布局,系统自带的属性同样可以继续起作用,我们同样要实现这一点,所以这里使用内部类的方式继承了RelativeLayout的布局属性,这样就可以使用原生的属性了,注意generateLayoutParams()方法必须重写,返回我们的内部类对象。可以看出,构造方法中主要是解析自定义属性,onMeasure()方法我们的子view根据属性值重新测量绘制。实现了以上两步基本上就可以尝试一把了,看效果图:

屏幕适配3种方案及9.0刘海屏适配方案

3、density适配

想要实现density适配,我们需要了解到几个概念

  • density : 当前设备屏幕总宽度(单位为像素)/ 设计图总宽度(单位为 dp) = density
  • scaleDensity:字体缩放比例
  • densityDpi:每英寸像素点160个

通过源码发现,无论你在布局文件中是以何种尺寸单位,最终系统都会转成px处理,density的计算方法就是根据源码而来的

public static float applyDimension(int unit, float value,
                                       DisplayMetrics metrics)
    {
        switch (unit) {
        case COMPLEX_UNIT_PX:
            return value;
        case COMPLEX_UNIT_DIP:
            return value * metrics.density;
        case COMPLEX_UNIT_SP:
            return value * metrics.scaledDensity;
        case COMPLEX_UNIT_PT:
            return value * metrics.xdpi * (1.0f/72);
        case COMPLEX_UNIT_IN:
            return value * metrics.xdpi;
        case COMPLEX_UNIT_MM:
            return value * metrics.xdpi * (1.0f/25.4f);
        }
        return 0;
    }

density在每个设备上都是固定的,DPI / 160 = density ,屏幕的总 px 宽度 / density = 屏幕的总 dp 宽度,以下参考一篇博客,他的例子是比较容易理解的,博客地址https://blog.csdn.net/wy391920778/article/details/81939233

了解到原理,下面我直接上代码

/**
 * <pre>
 *     author  : QB
 *     time    : 2019/3/26
 *     version : v1.0.0
 *     qq      : 952722763
 *     email   : [email protected]
 *     desc    : 修改系统density达到适配
 * </pre>
 */
public class DensityUtils {

    public static final float WIDTH = 480;//相当于设计稿给出的宽度 dp值
    public static  float appDensity;//表示屏幕密度
    public static float appScaleDensity;//字体缩放比例,默认appDensity


    public static void setDensity(final Application application, Activity activity){
        //获取app的DisplayMetrics
        DisplayMetrics displayMetrics = application.getResources().getDisplayMetrics();
        if(appDensity == 0 || appScaleDensity == 0){
            appDensity = displayMetrics.density;
            appScaleDensity = displayMetrics.scaledDensity;

            //添加系统字体变化监听回调
            application.registerComponentCallbacks(new ComponentCallbacks() {
                @Override
                public void onConfigurationChanged(Configuration newConfig) {
                    //字体大小发生变化条件
                    if(newConfig != null && newConfig.fontScale > 0){
                        appScaleDensity = application.getResources().getDisplayMetrics().scaledDensity;
                    }
                }

                @Override
                public void onLowMemory() {

                }
            });
        }

        float targetDensity = displayMetrics.widthPixels/WIDTH;
        float targetScaleDensity = targetDensity * (appScaleDensity/appDensity);
        int targetDensityDpi = (int) (160*targetDensity);

        //替换activity的density scaleDensity densityDpi
        DisplayMetrics dm = activity.getResources().getDisplayMetrics();
        dm.density = targetDensity;
        dm.scaledDensity = targetScaleDensity;
        dm.densityDpi = targetDensityDpi;




    }
}

注意:代码中为了防止修改系统的字体规格,导致app中显示的字体规格不一致,需要监听系统字体规格的变化。比如,在设置中设置字体为超大,不做监听处理的话,app字体不会跟随变化。效果图如下

屏幕适配3种方案及9.0刘海屏适配方案

4、刘海屏适配

刘海屏适配的几个步骤:

  • 判断手机厂商
  • 设置成全屏模式
  • 判断手机是不是刘海屏
  • 让内容区域延伸至刘海屏
  • 设置成沉浸式
  • 设置控件是否需要避开刘海 ,代码动态修改topMargin,或者父容器设置paddingTop

(1)判断手机厂商

目前很多国内厂商由于定制化的原因,会针对刘海屏适配做不同的处理,参考如下

其他手机厂商(华为,小米,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

具体不同厂商实现适配代码如下:

public class Utils {

    /**
     * 是否刘海
     * @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;
        }
    }

}

(2)设置成全屏模式

//1.设置成全屏
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        Window widow = getWindow();
        widow.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,WindowManager.LayoutParams.FLAG_FULLSCREEN);

(3)判断是不是刘海屏,里面做了版本的判断

​
/**
     * 判断是不是刘海屏
     * @return
     */
    private boolean isHasCutOut(Window widow){


            DisplayCutout displayCutout = getDisplayCutout(widow);


                if(displayCutout != null){
                    if(displayCutout.getBoundingRects() != null && displayCutout.getBoundingRects().size()>0 && displayCutout.getSafeInsetTop()>0){
                        return true;
                    }
                }


        return false;

    }

private DisplayCutout getDisplayCutout(Window widow){
        DisplayCutout displayCutout = null;

        //9.0以后才有刘海屏
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P){
            View rootView = widow.getDecorView();
            WindowInsets rootWindowInsets = rootView.getRootWindowInsets();
            if(rootWindowInsets != null){
                displayCutout = rootWindowInsets.getDisplayCutout();
            }
        }
        return displayCutout;

    }

​

(4)如果是刘海屏,让内容延伸至刘海区

//2.让内容区域延伸至刘海区
            WindowManager.LayoutParams layoutParams = widow.getAttributes();
            /**
             * * @see #LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT 全屏模式,内容下移,非全屏模式不受影响
             * * @see #LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES 允许内容延伸至刘海
             * * @see #LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER 不允许内容延伸至刘海
             */
            layoutParams.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
            widow.setAttributes(layoutParams);

(5)设置成沉浸式

//3.设置成沉浸式
            int flag = View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
            int visibility = widow.getDecorView().getSystemUiVisibility();
            visibility |= flag; // 追加沉浸式设置
            widow.getDecorView().setSystemUiVisibility(visibility);

(6)如果想要自己的控件不被刘海区覆盖,需要动态修改控件的布局属性

RelativeLayout rl = findViewById(R.id.rel);
        int height = getCutOutHeight();
        rl.setPadding(rl.getPaddingLeft(),getCutOutHeight(),rl.getPaddingRight(),rl.getPaddingBottom());

/**
     * 通常情况下状态栏的高度可以看成是刘海的高度,刘海的高度<=状态栏高度
     * @return
     */
    private int getCutOutHeight(){
        int resourceId = getResources().getIdentifier("status_bar_height","dimen","android");
        if(resourceId > 0){
            return getResources().getDimensionPixelSize(resourceId);
        }

        return 0;

    }

以上所有的设置都必须在setContentView()调用之前进行设置

附上完整代码:

​
/**
 * <pre>
 *     author  : QB
 *     time    : 2019/3/25
 *     version : v1.0.0
 *     qq      : 952722763
 *     email   : [email protected]
 *     desc    :
 * </pre>
 */
public class MyViewActivity extends Activity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        DensityUtils.setDensity(getApplication(),this);
        //1.设置成全屏
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        Window widow = getWindow();
        widow.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,WindowManager.LayoutParams.FLAG_FULLSCREEN);


        //判断手机是不是刘海屏
        boolean isHasCutOut = isHasCutOut(widow);

        if(isHasCutOut){
            //2.让内容区域延伸至刘海区
            WindowManager.LayoutParams layoutParams = widow.getAttributes();
            /**
             * * @see #LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT 全屏模式,内容下移,非全屏模式不受影响
             * * @see #LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES 允许内容延伸至刘海
             * * @see #LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER 不允许内容延伸至刘海
             */
            layoutParams.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
            widow.setAttributes(layoutParams);




            //3.设置成沉浸式
            int flag = View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
            int visibility = widow.getDecorView().getSystemUiVisibility();
            visibility |= flag; // 追加沉浸式设置
            widow.getDecorView().setSystemUiVisibility(visibility);
        }





        setContentView(R.layout.activity_screenfit_density_view);

       //是否需要避开刘海
        RelativeLayout rl = findViewById(R.id.rel);
        int height = getCutOutHeight();
        rl.setPadding(rl.getPaddingLeft(),getCutOutHeight(),rl.getPaddingRight(),rl.getPaddingBottom());
        /*View view = findViewById(R.id.view);
        RelativeLayout.LayoutParams rl = (RelativeLayout.LayoutParams) view.getLayoutParams();
        rl.topMargin = getCutOutHeight();
        view.setLayoutParams(rl);*/


    }

    /**
     * 判断是不是刘海屏
     * @return
     */
    private boolean isHasCutOut(Window widow){


            DisplayCutout displayCutout = getDisplayCutout(widow);


                if(displayCutout != null){
                    if(displayCutout.getBoundingRects() != null && displayCutout.getBoundingRects().size()>0 && displayCutout.getSafeInsetTop()>0){
                        return true;
                    }
                }


        return false;

    }


    private DisplayCutout getDisplayCutout(Window widow){
        DisplayCutout displayCutout = null;

        //9.0以后才有刘海屏
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P){
            View rootView = widow.getDecorView();
            WindowInsets rootWindowInsets = rootView.getRootWindowInsets();
            if(rootWindowInsets != null){
                displayCutout = rootWindowInsets.getDisplayCutout();
            }
        }
        return displayCutout;

    }

    /**
     * 通常情况下状态栏的高度可以看成是刘海的高度,刘海的高度<=状态栏高度
     * @return
     */
    private int getCutOutHeight(){
        int resourceId = getResources().getIdentifier("status_bar_height","dimen","android");
        if(resourceId > 0){
            return getResources().getDimensionPixelSize(resourceId);
        }

        return 0;

    }
}

​

运行效果如下:

屏幕适配3种方案及9.0刘海屏适配方案