Android刘海屏适配全方案

目前市面上的刘海屏和水滴屏手机越来越多了,颜值方面是因人而异,有的人觉得很好看,也有人觉得丑爆了,我个人觉得是还可以。但是作为移动开发者来说,这并不是一件好事,越来越多异形屏手机的出现意味着我们需要投入大量精力在适配上(就不提之后会出的折叠屏手机了)。本文总结了当下主流手机的刘海屏适配方案,鉴于目前Android碎片化的情况,想要覆盖所有的机型是不可能的,但是能适配一些是一些,总比什么都不做要好。

所谓刘海屏,指的是手机屏幕正上方由于追求极致边框而采用的一种手机解决方案。因形似刘海儿而得名——来自百度百科,水滴屏也是类似,为了简单起见,下文就统称这两种为刘海屏了。

什么时候需要适配

在这里插入图片描述

从图中可以看出,刘海区域是镶嵌在状态栏内部的,刘海区域的高度一般是不超过状态栏高度的。因此,当我们的应用布局需要占据状态栏来显示时,就需要考虑到刘海区域是否会遮挡住页面上的控件或者背景,这就是为什么将状态栏区域称为危险区域。如果应用不需要占据状态栏显示,全部显示在安全区域内,那么恭喜你,不需要做任何适配处理。总结来说,只有当应用需要全屏显示时才需要进行适配。

全屏显示无非就是两种情况:第一种是我们常说的沉浸式状态栏,也就是状态栏透明,页面的布局延伸到状态栏显示,这种情况下状态栏依然可见;第二种是类似应用的闪屏页风格,页面全屏显示,状态栏不可见。这两种情况下如果不进行适配处理都会产生一些问题。
第一种情况,沉浸式风格。需要将状态栏设置为透明,需要注意只有在Android 4.4(API Level 19)以上才支持设置透明状态栏。有两种设置方法:
方法一:为Activity设置style,添加一个属性:

<!--状态栏半透明状态:位置透明即状态栏不占用位置,但还是显示内容-->
<item name="android:windowTranslucentStatus">true</item>

方法二:在Activity的onCreate()中为Window添加Flag

public class ImmersiveActivity extends AppCompatActivity {
    
    
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_immersive);
        // 透明状态栏
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    
    
            getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
        }
    }
}

页面的布局很简单,只包含一个按钮,为了明显,我为根布局设置了一个背景。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/ll_root"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@mipmap/bg"
    android:orientation="vertical">
    <Button
        android:layout_width="150dp" 
       android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal" />
</LinearLayout>

运行之后发现按钮会被刘海区域所遮挡,如图所示:
在这里插入图片描述
第二种情况:全屏风格,状态栏不可见。同样有两种设置方法:
方法一:为Activity设置style,添加属性:

<!--全屏即无状态栏-->
<item name="android:windowFullscreen">true</item>
<!-- 这里为了简单,直接从style中指定一个背景 -->
<item name="android:windowBackground">@mipmap/bg</item>

方法二:在Activity的OnCreate()中添加代码:

public class FullScreenActivity extends AppCompatActivity {
    
    
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        // 全屏显示
        getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN |View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
    }
}

补充说明一点,现在的手机屏幕高宽比例越来越大,我们还需要额外做一下适配才能使应用在所有手机上都能全屏显示,具体方式有两种:
方式一:在AndroidManifest.xml中配置支持最大高宽比

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

或者

android:maxAspectRatio="ratio_float" (API LEVEL 26)

说明:以上两种接口可以二选一,ratio_float = 屏幕高 / 屏幕宽 (如oppo新机型屏幕分辨率为2280 x 1080, ratio_float = 2280 / 1080 = 2.11,建议设置 ratio_float为2.2或者更大)

方式二:在AndroidManifest.xml中配置支持分屏,注意验证分屏下界面兼容性

android:resizeableActivity="true"  

也可以通过设置targetSdkVersion>=24(即Android 7.0),该属性的值会默认为true,就不需要在AndroidManifest.xml中配置了。
运行之后,我们发现状态栏的部分留出了一条黑边,看上起很奇怪,这显然不是我们想要的效果。
在这里插入图片描述

如何适配

1.沉浸式状态栏的适配

其实沉浸式状态栏带来的遮挡问题与刘海屏无关,本质上是由于设置了透明状态栏导致布局延伸到了状态栏中,就算是不具有刘海屏,一定程度上也会造成布局的遮挡。不过既然刘海屏是处在状态栏当中的,那么我们就把这种情况也包含在刘海屏的适配中。清楚了原因之后,解决起来就很简单了,我们只需要让控件或布局避开状态栏显示就可以了,具体的解决方法有三种:
方法一.利用fitsSystemWindows属性
当我们给最外层View设置了android:fitsSystemWindows="true"属性后,当设置了透明状态栏或者透明导航栏后,就会自动给View添加paddingTop或paddingBottom属性,这样就在屏幕上预留出了状态栏的高度,我们的布局就不会占用状态栏来显示了。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/ll_root"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@mipmap/bg"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <Button
        android:layout_width="150dp" 
       android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal" />
</LinearLayout>

方法二.根据状态栏高度手动设置paddingTop
这种方法的实现本质上和设置fitsSystemWindows是一样的,首先获取状态栏高度,然后设置根布局的paddingTop等于状态栏高度就可以了,代码如下:

public class ImmersiveActivity extends AppCompatActivity {
    
    
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_immersive);
        // 透明状态栏        
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    
    
            getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
        }
        LinearLayout llRoot = findViewById(R.id.ll_root);
        // 设置根布局的paddingTop        
		llRoot.setPadding(0, getStatusBarHeight(this), 0, 0);
    }
    /**
     * 获取状态栏高度     
*
     * @param context
     * @return
     */    
public int getStatusBarHeight(Context context) {
    
    
        int statusBarHeight = 0;
        int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
        if (resourceId > 0) {
    
    
            statusBarHeight = context.getResources().getDimensionPixelSize(resourceId);
        }
        return statusBarHeight;
    }
}

方法三.在布局中添加一个和状态栏高度相同的View

和前两种方法原理类似,同样是让屏幕预留出状态栏的高度,这里在根布局中添加了一个透明的View,高度和状态栏高度相同。这种方法的好处是可以自定义填充状态栏View的背景,更灵活地实现我们想要的效果。

public class ImmersiveActivity extends AppCompatActivity {
    
    
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_immersive);
        // 透明状态栏        
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    
    
            getWindow().addFlags(
                    WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
        } 
       LinearLayout llRoot = findViewById(R.id.ll_root);
        View statusBarView = new View(this);
        statusBarView.setBackgroundColor(Color.TRANSPARENT);
        ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                getStatusBarHeight(this));
        // 在根布局中添加一个状态栏高度的View
        llRoot.addView(statusBarView, 0, lp);
    }
    /**
     * 获取状态栏高度     
*     
* @param context
     * @return
     */
    public int getStatusBarHeight(Context context) {
    
    
        int statusBarHeight = 0;
        int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
        if (resourceId > 0) {
    
    
            statusBarHeight = context.getResources().getDimensionPixelSize(resourceId);
        } 
       return statusBarHeight;
    }
}

适配之后成功地将控件避开了状态栏(危险区域),如下图所示:
在这里插入图片描述

2.全屏显示的适配

2.1.Android P及以上

谷歌官方从Android P开始给开发者提供了刘海屏相关的API,可以通过直接调用API来进行刘海屏的适配处理。
通过DisplayCutout类可以获得安全区域的范围以及刘海区域(官方的叫法是缺口)的信息,需要注意只有API Level在28及以上才可以调用。

/** * 获得刘海区域信息 
*/@TargetApi(28)public void getNotchParams() {
    
    
    	View decorView = getWindow().getDecorView();
        if (decorView != null) {
    
    
            decorView.post(new Runnable() {
    
    
                @Override
                public void run() {
    
    
                    WindowInsets windowInsets = decorView.getRootWindowInsets();
                    if (windowInsets != null) {
    
    
                        // 当全屏顶部显示黑边时,getDisplayCutout()返回为null,显示黑边的情况:窗口设置windowFullscreen会造成黑边
                        DisplayCutout displayCutout = windowInsets.getDisplayCutout();
                        if (displayCutout != null) {
    
    
                            Log.e(TAG, "安全区域距离屏幕左边的距离 SafeInsetLeft:" + displayCutout.getSafeInsetLeft());
                            Log.e(TAG, "安全区域距离屏幕右部的距离 SafeInsetRight:" + displayCutout.getSafeInsetRight());
                            Log.e(TAG, "安全区域距离屏幕顶部的距离 SafeInsetTop:" + displayCutout.getSafeInsetTop());
                            Log.e(TAG, "安全区域距离屏幕底部的距离 SafeInsetBottom:" + displayCutout.getSafeInsetBottom());

                            // 获得刘海区域
                            List<Rect> rects = displayCutout.getBoundingRects();
                            if (rects == null && rects.size() == 0) {
    
    
                                Log.e("TAG", "不是刘海屏");
                            } else {
    
    
                                Log.e(TAG, "刘海屏数量:" + rects.size());
                                for (Rect rect : rects) {
    
    
                                    Log.e(TAG, "刘海屏区域:" + rect);
                                }

                            }
                        }
                    }
                }
            });
       }
}

这里我在测试时也发现了一个问题,就是如果是在style中设置了全屏模式,在适配之前,顶部状态栏区域显示一条黑边,这时候调用getDisplayCutout()获取DisplayCutout对象返回的结果是null,其实这也不难理解,因为这时候是看不出刘海区域的,但是这样会导致在适配之前无法通过DisplayCutout判断是否存在刘海屏,只能在适配后才能获取到刘海区域信息,因此只能对于所有设备都添加适配代码。
那么接下来如何进行适配呢,Android P中增加了一个窗口布局参数属性layoutInDisplayCutoutMode,该属性有三个值可以取:

LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT:默认的布局模式,仅当刘海区域完全包含在状态栏之中时,才允许窗口延伸到刘海区域显示,也就是说,如果没有设置为全屏显示模式,就允许窗口延伸到刘海区域,否则不允许。
LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER:永远不允许窗口延伸到刘海区域。
LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES:始终允许窗口延伸到屏幕短边上的刘海区域,窗口永远不会延伸到屏幕长边上的刘海区域。

还有一个LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS模式,目前已经被LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES所取代,不允许使用了,这里就不提了。
这么看可能还是有些不理解,接下来我们在一个全屏显示的页面分别设置三种布局模式,看看有什么区别。

public class FullScreenActivity extends AppCompatActivity {
    
    
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
    
    
            WindowManager.LayoutParams lp = getWindow().getAttributes();
            // 仅当缺口区域完全包含在状态栏之中时,才允许窗口延伸到刘海区域显示//            
lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
            // 永远不允许窗口延伸到刘海区域//
            lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
            // 始终允许窗口延伸到屏幕短边上的刘海区域
            lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
            getWindow().setAttributes(lp);
        }
    }
}

也可在style.xml或者theme.xml中设置

<!--全屏无状态栏窗口延伸情况:
        never:永远不允许窗口延伸到刘海区域
        shortEdges:始终允许窗口延伸到屏幕短边上的刘海区域,窗口永远不会延伸到屏幕长边上的刘海区域
        default:没有设置为全屏显示模式,就允许窗口延伸到刘海区域,否则不允许-->
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>

三种模式下的显示效果如下图所示:
LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT:default
在这里插入图片描述
LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER:never
在这里插入图片描述
LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES:shortEdges
在这里插入图片描述
可以看出,当在全屏显示情况下,LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT和LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER的效果是一样的,都是在状态栏显示一条黑边,也就是不允许窗口布局延伸到刘海区域,而LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES则允许窗口布局延伸到了刘海区域,这里需要注意是短边刘海区域,不过一般市面上的手机刘海区域都是在短边上的,我是没见过刘海长在“腰”上的,因此利用这个模式就实现适配了。
通过之前沉浸式状态栏的显示效果可以看出,LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT在此时是允许窗口布局延伸到刘海区域的,因此更证实了只有在全屏显示的情况下该模式才不允许窗口布局延伸到刘海区域。
适配后效果如下,现在看起来就很舒服了:

在这里插入图片描述
不复制了 好麻烦 转:https://blog.csdn.net/yangjunjin/article/details/113509754

最后贴上自己全面屏的解决方式,
第一种是不占用状态栏的位置:

		<!--windowFullscreen、windowLayoutInDisplayCutoutMode、windowTranslucentStatus三个属性一起用可以隐藏状态栏的情况下将挖孔屏设备窗口延伸之任务栏区域-->
        <!--全屏即无状态栏-->
        <item name="android:windowFullscreen">true</item>
        <!--全屏无状态栏窗口延伸情况:
        never:永远不允许窗口延伸到刘海区域
        shortEdges:始终允许窗口延伸到屏幕短边上的刘海区域,窗口永远不会延伸到屏幕长边上的刘海区域
        default:没有设置为全屏显示模式,就允许窗口延伸到刘海区域,否则不允许-->
        <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
        <!--状态栏半透明状态:位置透明-->
        <item name="android:windowTranslucentStatus">true</item>
        <!--用于根据状态栏等系统窗口调整视图布局。如果为 true,则调整此视图的填充以为系统窗口留出空间。仅当此视图在非嵌入式活动中时才会生效-->
        <item name="android:fitsSystemWindows">true</item>

请添加图片描述
第二种是占用状态栏的位置:
上面的xml代码去掉android:fitsSystemWindows即可

		<!--windowFullscreen、windowLayoutInDisplayCutoutMode、windowTranslucentStatus三个属性一起用可以隐藏状态栏的情况下将挖孔屏设备窗口延伸之任务栏区域-->
        <!--全屏即无状态栏-->
        <item name="android:windowFullscreen">true</item>
        <!--全屏无状态栏窗口延伸情况:
        never:永远不允许窗口延伸到刘海区域
        shortEdges:始终允许窗口延伸到屏幕短边上的刘海区域,窗口永远不会延伸到屏幕长边上的刘海区域
        default:没有设置为全屏显示模式,就允许窗口延伸到刘海区域,否则不允许-->
        <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
        <!--状态栏半透明状态:位置透明-->
        <item name="android:windowTranslucentStatus">true</item>

请添加图片描述

猜你喜欢

转载自blog.csdn.net/weixin_46139477/article/details/126914152