CodeXiaoMai

CodeXiaoMai的博客


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

  • 搜索

刘海屏适配

发表于 2019-09-10 更新于 2019-09-15 分类于 Android 阅读次数:
本文字数: 15k

关于刘海屏的适配方案有两种:

  1. 可以在刘海屏区域展示,即使部分内容被『吃掉』也没关系;
  2. 不可以在刘海屏区域展示,避免重要的信息不能被用户看到,影响使用。

本文针对这两种情况进行分析:

知识储备

window.decorView.systemUiVisibility 的可选值(部分)

  • View.SYSTEM_UI_FLAG_VISIBLE

    默认标记。显示状态栏和导航栏,Activity 正常显示。

  • View.INVISIBLE

    隐藏状态栏,同时Activity会伸展全屏显示。

  • View.SYSTEM_UI_FLAG_LOW_PROFILE

    低调模式, 会隐藏不重要的状态栏图标

  • View.SYSTEM_UI_FLAG_LAYOUT_STABLE

    保持整个View稳定, 常和控制System UI悬浮, 隐藏的Flags共用, 使View不会因为System UI的变化而重新layout

  • View.SYSTEM_UI_FLAG_FULLSCREEN

    状态栏隐藏,Activity全屏显示。效果同设置WindowManager.LayoutParams.FLAG_FULLSCREEN

  • View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN

    视图延伸至状态栏区域,状态栏上浮于视图之上

  • View.SYSTEM_UI_FLAG_HIDE_NAVIGATION

    隐藏导航栏

  • View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION

    视图延伸至导航栏区域,导航栏上浮于视图之上

  • View.SYSTEM_UI_FLAG_IMMERSIVE

    沉浸模式, 隐藏状态栏和导航栏, 并且在第一次会弹泡提醒, 并且在状态栏区域滑动可以呼出状态栏(这样会清除之前设置的View.SYSTEM_UI_FLAG_FULLSCREEN 或 View.SYSTEM_UI_FLAG_HIDE_NAVIGATION标志)。使之生效,需要和 View.SYSTEM_UI_FLAG_FULLSCREEN,View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 中的一个或两个同时设置。

  • View.SYSTEM_UI_FLAG_IMMERSIVE_STIKY

    与上面唯一的区别是, 呼出隐藏的状态栏后不会清除之前设置的View.SYSTEM_UI_FLAG_FULLSCREEN或View.SYSTEM_UI_FLAG_HIDE_NAVIGATION标志,在一段时间后将再次隐藏系统栏)。

layoutInDisplayCutoutMode(Android P 提供)

模式 模式说明
LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT 只有当DisplayCutout完全包含在系统栏中时,才允许窗口延伸到DisplayCutout区域。 否则,窗口布局不与DisplayCutout区域重叠。
LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER 该窗口决不允许与DisplayCutout区域重叠。
LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES 该窗口始终允许延伸到屏幕短边上的DisplayCutout区域。

在刘海屏区域内展示内容,部分内容被『吃掉』不影响

效果如下:

在刘海屏内显示

这种方案比较好适配,如果是 Android 9.0 及以上系统,使用官方提供的方案,否则设置布局延伸到状态栏即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package com.xiaomai.demo

import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.view.View
import android.view.WindowManager
import kotlinx.android.synthetic.main.full_screen.*

class FullActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setContentView(R.layout.full_screen)

showTitleBar()

// 如果系统版本大于等于 Android 9.0,系统支持刘海屏, 否则部分国产手机可能在 Android 8.0 就支持了刘海屏,需要在 manifest 中配置。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
window.attributes = window.attributes.apply {
layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
}
}

var isShow = true

touchView.setOnClickListener {
isShow = !isShow
if (isShow) {
showTitleBar()
} else {
hideTitleBar()
}
}
}

private fun showTitleBar() {
// 设置状态栏透明
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
window.statusBarColor = Color.TRANSPARENT
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
window.decorView.systemUiVisibility =
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or // 视图延伸至导航栏区域,导航栏上浮于视图之上
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN // 视图延伸至状态栏区域,状态栏上浮于视图之上
}
}

/**
* 已知问题,在 Vivo X21A 全屏适配有问题
*/
private fun hideTitleBar() {
var options = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // 隐藏导航栏

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
options = options or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or // 视图延伸至导航栏区域,导航栏上浮于视图之上
View.SYSTEM_UI_FLAG_FULLSCREEN or // 隐藏状态栏
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN // 视图延伸至状态栏区域,状态栏上浮于视图之上
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
options = options or
View.SYSTEM_UI_FLAG_IMMERSIVE // 沉浸模式, 隐藏状态栏和导航栏, 并且在第一次会弹泡提醒, 并且在状态栏区域滑动可以呼出状态栏
}

window.decorView.systemUiVisibility = options
}
}

内容重要,不能在刘海屏区域展示

no_display_in_cutout

思路:首先检查设备是否是刘海屏,如果是的话则设置不在状态栏显示内容,如果不是话则可以在状态栏显示。

检测设备是否是刘海屏的思路:

check

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
class FullActivity : AppCompatActivity() {

private val TAG = "FullActivity"

private var hasDisplayCutout = false
set(value) {
field = value

if (value) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
window.attributes = window.attributes.apply {
layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
}
}
// 设置状态栏为黑色
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
window.statusBarColor = Color.BLACK
}
} else {
// 设置状态栏透明
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
window.statusBarColor = Color.TRANSPARENT
}
}

showTitleBar()
}

/**
* 监查是否是刘海屏
*/
private fun checkDisplayCutout() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val decorView = window.decorView

decorView.post {
val displayCutout = decorView.rootWindowInsets.displayCutout

displayCutout?.apply {
Log.e(TAG, "安全区域距离屏幕左边的距离 SafeInsetLeft:" + displayCutout.safeInsetLeft)
Log.e(TAG, "安全区域距离屏幕右部的距离 SafeInsetRight:" + displayCutout.safeInsetRight)
Log.e(TAG, "安全区域距离屏幕顶部的距离 SafeInsetTop:" + displayCutout.safeInsetTop)
Log.e(TAG, "安全区域距离屏幕底部的距离 SafeInsetBottom:" + displayCutout.safeInsetBottom)
}

hasDisplayCutout = displayCutout?.safeInsetTop ?: 0 > 0
}
} else {
hasDisplayCutout = DisplayCutoutUtils.isSupportDisplayCutout(this@FullActivity)
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setContentView(R.layout.full_screen)

checkDisplayCutout()

var isShow = true

touchView.setOnClickListener {
isShow = !isShow
if (isShow) {
showTitleBar()
} else {
hideTitleBar()
}
}
}

private fun showTitleBar() {
var newOptions = window.decorView.systemUiVisibility
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
newOptions = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
}

if (hasDisplayCutout) {
// do nothing
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
newOptions = newOptions or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
}
}
window.decorView.systemUiVisibility = newOptions
}

private fun hideTitleBar() {
var newOptions = window.decorView.systemUiVisibility

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
newOptions =
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
}

if (hasDisplayCutout) {
// do nothing
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
newOptions = (newOptions
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_FULLSCREEN)
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
newOptions = newOptions or View.SYSTEM_UI_FLAG_IMMERSIVE
}
}

window.decorView.systemUiVisibility = newOptions
}
}

以下是通过反射获取 Android P 版本以下的设备是否是刘海屏的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
object DisplayCutoutUtils {

private const val TAG = "DisplayCutoutUtils"

private fun Number.dp2Pixels() =
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
this.toFloat(),
Resources.getSystem().displayMetrics
)

fun isSupportDisplayCutout(context: Context): Boolean {
return getDisplayCutoutHeight(context) > 0
}

//调用该方法,可以获取刘海屏的px值,没刘海屏则返回0
private fun getDisplayCutoutHeight(context: Context): Float {
var displayCutoutHeight = 0.0f

//判断手机厂商,目前8.0只有华为、小米、oppo、vivo适配了刘海屏
val phoneManufacturer = Build.BRAND.toLowerCase(Locale.CHINESE)
if ("huawei" == phoneManufacturer) {
//huawei,长度为length,单位px
val haveDisplayCutoutEMUI = hasDisplayCutoutInEMUI(context)
if (haveDisplayCutoutEMUI) {
val displayCutout: IntArray = getDisplayCutoutSizeInEMUI(context)
displayCutoutHeight = displayCutout[1].toFloat()

Log.e(
TAG,
"haveDisplayCutoutInEMUI: $haveDisplayCutoutEMUI, displayCutout: $displayCutout"
)
}
} else if ("xiaomi" == phoneManufacturer) {
//xiaomi,单位px
val haveDisplayCutoutInMIUI = getDisplayCutoutInMIUI("ro.miui.notch", 0) == 1
if (haveDisplayCutoutInMIUI) {
val resourceId = context.resources.getIdentifier("notch_height", "dimen", "android")
var result = 0
if (resourceId > 0) {
result = context.resources.getDimensionPixelSize(resourceId)
}
displayCutoutHeight = result.toFloat()

Log.e(
TAG,
"haveDisplayCutoutInMIUI: $haveDisplayCutoutInMIUI, displayCutoutHeight: $displayCutoutHeight"
)
}
} else if ("vivo" == phoneManufacturer) {
//vivo,单位dp,高度27dp
val haveDisplayCutoutInVIVO = hasDisplayCutoutInVivo(context)
if (haveDisplayCutoutInVIVO) {
displayCutoutHeight = 27.dp2Pixels()
Log.e(
TAG,
"haveDisplayCutoutInVIVO: $haveDisplayCutoutInVIVO, displayCutoutHeight: $displayCutoutHeight"
)
}
} else if ("oppo" == phoneManufacturer) {
//oppo
val haveDisplayCutoutInOPPO =
context.packageManager.hasSystemFeature("com.oppo.feature.screen.heteromorphism")
if (haveDisplayCutoutInOPPO) {
displayCutoutHeight = 80f
Log.e(
TAG,
"haveDisplayCutoutInOPPO: $haveDisplayCutoutInOPPO, displayCutoutHeight: $displayCutoutHeight"
)
}
}

return displayCutoutHeight
}

//huawei
private fun hasDisplayCutoutInEMUI(context: Context): Boolean {
var ret = false
try {
val cl = context.classLoader
val hwNotchSizeUtilClass = cl.loadClass("com.huawei.android.util.HwNotchSizeUtil")
val hasNotchInScreenMethod = hwNotchSizeUtilClass.getMethod("hasNotchInScreen")
ret = hasNotchInScreenMethod.invoke(hwNotchSizeUtilClass) as Boolean
} catch (e: ClassNotFoundException) {
Log.e(TAG, "hasDisplayCutoutInEMUI ClassNotFoundException")
} catch (e: NoSuchMethodException) {
Log.e(TAG, "hasDisplayCutoutInEMUI NoSuchMethodException")
} catch (e: Exception) {
Log.e(TAG, "hasDisplayCutoutInEMUI Exception")
} finally {
return ret
}
}

private fun getDisplayCutoutSizeInEMUI(context: Context): IntArray {
var ret = intArrayOf(0, 0)
try {
val cl = context.classLoader
val hwNotchSizeUtilClass = cl.loadClass("com.huawei.android.util.HwNotchSizeUtil")
val getNotchSizeMethod = hwNotchSizeUtilClass.getMethod("getNotchSize")
ret = getNotchSizeMethod.invoke(hwNotchSizeUtilClass) as IntArray
} catch (e: ClassNotFoundException) {
Log.e(TAG, "getDisplayCutoutSizeInEMUI ClassNotFoundException")
} catch (e: NoSuchMethodException) {
Log.e(TAG, "getDisplayCutoutSizeInEMUI NoSuchMethodException")
} catch (e: Exception) {
Log.e(TAG, "getDisplayCutoutSizeInEMUI Exception")
} finally {
return ret
}
}

private fun getDisplayCutoutInMIUI(key: String, def: Int): Int {
var getIntMethod: Method? = null
try {
if (getIntMethod == null) {
getIntMethod = Class.forName("android.os.SystemProperties")
.getMethod("getInt", String::class.java, Int::class.javaPrimitiveType)
}
return (getIntMethod?.invoke(null, key, def) as? Int)?.toInt() ?: 0
} catch (e: Exception) {
Log.e(TAG, "Platform error: $e")
return def
}

}

private fun hasDisplayCutoutInVivo(context: Context): Boolean {
var ret = false
try {
val classLoader = context.classLoader
val ftFeatureClass = classLoader.loadClass("android.util.FtFeature")
val isFeatureSupportMethod =
ftFeatureClass.getMethod("isFeatureSupport", Int::class.javaPrimitiveType!!)
ret = isFeatureSupportMethod.invoke(ftFeatureClass, 0x00000020) as Boolean
} catch (e: ClassNotFoundException) {
Log.e(TAG, "hasDisplayCutoutInVivo ClassNotFoundException")
} catch (e: NoSuchMethodException) {
Log.e(TAG, "hasDisplayCutoutInVivo NoSuchMethodException")
} catch (e: Exception) {
Log.e(TAG, "hasDisplayCutoutInVivo Exception")
} finally {
return ret
}
}
}

带虚拟导航的 Pad 设备

有的 Pad 在横屏时,虚拟导航在屏幕右侧,而有的在屏幕下方。有两种计算屏幕宽高的方法,一种包含虚拟导航栏,一种不包含。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
fun getScreenHeight() = Resources.getSystem().displayMetrics.heightPixels

fun getScreenWidth() = Resources.getSystem().displayMetrics.widthPixels

/**
* 包含虚拟导航栏
*/
fun getRealScreenHeight(context: Context): Int {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
val display = windowManager.defaultDisplay
val metrics = DisplayMetrics()
display.getRealMetrics(metrics)
metrics.heightPixels
} else {
getScreenHeight()
}
}

/**
* 包含虚拟导航栏
*/
fun getRealScreenWidth(context: Context): Int {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
val display = windowManager.defaultDisplay
val metrics = DisplayMetrics()
display.getRealMetrics(metrics)
metrics.widthPixels
} else {
getScreenWidth()
}
}

height

参考链接

  • https://developer.android.com/guide/topics/display-cutout
  • https://juejin.im/post/5b1930835188257d7541ba33
  • MIUI 开发文档
  • Vivo 开发文档
# Android # 屏幕适配 # 刘海屏
Dialog 中监听键盘弹出与收起事件
FragmentManager is already executing transactions 异常分析
  • 文章目录
  • 站点概览
CodeXiaoMai

CodeXiaoMai

CodeXiaoMai的博客
12 日志
6 分类
19 标签
GitHub E-Mail
  1. 1. 知识储备
    1. 1.1. window.decorView.systemUiVisibility 的可选值(部分)
    2. 1.2. layoutInDisplayCutoutMode(Android P 提供)
  2. 2. 在刘海屏区域内展示内容,部分内容被『吃掉』不影响
  3. 3. 内容重要,不能在刘海屏区域展示
  4. 4. 带虚拟导航的 Pad 设备
  5. 5. 参考链接
© 2019 CodeXiaoMai
|