刘海屏是指某些设备显示屏上的一个区域延伸到显示面,这样既能为用户提供全面屏体验,又能为设备正面的重要传感器留出空间。Android 在搭载 Android 9(API 级别 28)及更高版本的设备上正式支持刘海屏。请注意,设备制造商也可以选择在搭载 Android 8.1 或更低版本的设备上支持刘海屏。
选择您的应用如何处理刘海区域
如果不希望您的内容与刘海区域重叠,请确保您的内容不与状态栏和导航栏重叠,这样做一般就足够了。
如果您要将内容呈现到刘海区域中,则可以使用 WindowInsets.getDisplayCutout() 来检索 DisplayCutout 对象,该对象包含每个刘海区域的安全边衬区和边界框。您可以使用这些 API 来检查您的内容是否与刘海区域重叠,以便根据需要重新放置。
Android 还允许您控制是否在刘海区域内显示内容。窗口布局属性 layoutInDisplayCutoutMode 控制您的内容如何呈现在刘海区域中。您可以将 layoutInDisplayCutoutMode 设为以下某个值:
- LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT - 这是默认行为,如上所述。在竖屏模式下,内容会呈现到刘海区域中;但在横屏模式下,内容会显示黑边。
- LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
- LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
您可以通过编程或在 Activity 中设置样式来设置刘海模式。以下示例定义了一种样式,您可以使用它将 LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES 属性应用到 Activity。
<style name="ActivityTheme">
<item name="android:windowLayoutInDisplayCutoutMode">
shortEdges <!-- default, shortEdges, never -->
</item>
</style>
下面几部分更详细地介绍了不同的刘海模式。
#1. 默认行为
默认情况下,在未设置特殊标志的竖屏模式下,在带刘海屏的设备上,状态栏的大小会调整为至少与刘海一样高,而您的内容会显示在下方区域。在横屏模式或全屏模式下,您的应用窗口会显示黑边,因此您的任何内容都不会显示在刘海区域中。
#2. 将内容呈现在短边刘海区域中
对于某些内容(如视频、照片、地图和游戏),呈现在刘海区域中是一种很好的方法,这样能够为用户提供沉浸感更强的全面屏体验。如果设置了 LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES,则在竖屏模式和横屏模式下,内容都会延伸到显示屏的短边上的刘海区域,而不管系统栏处于隐藏还是可见状态。请注意,窗口无法延伸到屏幕的长边上的刘海区域。使用此模式时,请确保没有重要内容与刘海区域重叠。
请注意,Android 可能不允许内容视图与系统栏重叠。要替换此行为并强制内容延伸到刘海区域,请通过 View.setSystemUiVisibility(int) 方法将以下任一标志应用于视图可见性:
- SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
- SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
- SYSTEM_UI_FLAG_LAYOUT_STABLE
下面是一些 LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
示例:
#3. 从不将内容呈现在刘海区域中
如果设置了 LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER,则不允许窗口与刘海区域重叠。
此模式应该用于暂时设置 View.SYSTEM_UI_FLAG_FULLSCREEN 或 View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 的窗口,以避免在设置或清除了该标志时执行另一种窗口布局。
请查看下面的 LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
示例:
支持刘海屏的最佳做法
使用刘海屏时,请务必考虑以下几点:
- 不要让刘海区域遮盖任何重要的文本、控件或其他信息。
- 不要将任何需要精细轻触识别的交互式元素放置或延伸到刘海区域。刘海区域中的轻触灵敏度可能要比其他区域低一些。
- 避免对状态栏高度进行硬编码,因为这样做可能会导致内容重叠或被切断。如有可能,请使用 WindowInsetsCompat 检索状态栏高度,并确定要对您的内容应用的适当内边距。
- 不要假定应用会占据整个窗口,而应使用 View.getLocationInWindow() 来确认应用的位置。不要使用 View.getLocationOnScreen()。
- 务必妥善处理进入或退出全屏模式。
- 对于竖屏模式下的默认刘海行为,如果刘海区域位于顶部边缘,并且窗口未设置 FLAG_FULLSCREEN 或 View.SYSTEM_UI_FLAG_FULLSCREEN,则窗口可以延伸到刘海区域。同样,如果刘海区域位于底部边缘,并且窗口未设置 View.SYSTEM_UI_FLAG_HIDE_NAVIGATION,则窗口可以延伸到刘海区域。在全屏模式或横屏模式下,窗口的布局方式应确保其不与刘海区域重叠。
- 如果您的应用需要进入和退出全屏模式,请使用 shortEdges 或 never 刘海模式。默认刘海行为可导致应用中的内容在全屏模式转换过程中上下移动,如下图所示:
- 在全屏模式下,在使用窗口坐标与屏幕坐标时应保持谨慎,因为在显示黑边的情况下,您的应用不会占据整个屏幕。由于显示黑边,因此根据屏幕原点得到的坐标与根据窗口原点得到的坐标不再相同。您可以根据需要使用 getLocationOnScreen() 将屏幕坐标转换为视图坐标
对于避免对状态栏高度进行硬编码,因为这样做可能会导致内容重叠或被切断。如有可能,请使用 WindowInsetsCompat 检索状态栏高度,并确定要对您的内容应用的适当内边距。
### 以下介绍如何检索状态栏高度,并设置适当内边距:
1. 获取状态栏高度:
/**
* get statusBar height
*
* @param context
* @return statusBar height
*/
private fun getStatusBarHeight(context: Context): Int {
try {
var result = 0
val resourceId =
context.resources.getIdentifier("status_bar_height", "dimen", "android")
if (resourceId > 0) {
result = context.resources.getDimensionPixelSize(resourceId)
}
return result
} catch (e: Exception) {
e.printStackTrace()
}
return 0
}
2. 贴上 layout_header.xml 文件,并设置 view layoutParams 的高度:
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<View android:id="@+id/view_status_bar"
android:layout_width="wrap_content"
android:layout_height="25dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/layout_header"
android:layout_width="match_parent"
android:layout_height="50dp"
app:layout_constraintTop_toBottomOf="@+id/view_status_bar"
tools:ignore="MissingConstraints">
<ImageView
android:id="@+id/header_image_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/header_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#D9D9D9"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="测速历史" />
<ImageView
android:id="@+id/header_image_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
/**
* get statusBar height
*
* @param context
* @return statusBar height
*/
private fun getStatusBarHeight(context: Context): Int {
try {
var result = 0
val resourceId =
context.resources.getIdentifier("status_bar_height", "dimen", "android")
if (resourceId > 0) {
result = context.resources.getDimensionPixelSize(resourceId)
}
return result
} catch (e: Exception) {
e.printStackTrace()
}
return 0
}
fun View.adaptStatusBarHeight() {
val pH = getStatusBarHeight(context)
Logger.d("StatusBarHeight", "status_bar_height = $pH")
if (pH > 0){
val params = this.layoutParams
params.height = pH
layoutParams = params
}
}
view_status_bar?.adaptStatusBarHeight()
贴上简单的效果:
---------------------- 补充说明 ------------------------
* 1. 上面贴的代码是做了状态栏浸透及状态栏半透明效果(StatusBarUtil.setTranslucentForImageView(this, 50, null)),适配了状态栏高度不一致问题(即设置适当内边距)
* 2. 额外添加了 view_status_bar, 是为了兼容,如果我在隐藏header的时候,app内容也需要这个适当内边距,以免内容错位至状态栏。
* 3. 这里的代码可以放到 BaseActivity/BaseFragment 中,做全局处理。(注意添加适当的判断)