代码生涯的第一个开源库,NotchAdapter 欢迎大家点评 Star
自从2017年 iphone X 问世,刘海屏幕(Notch Screen)也开始流行。但是正如上图官方文档所介绍的,Android 官方是从 Android P (Android 9 API 28)开始才正式开始支持刘海屏幕的适配。也就造成了 “上面老大哥还没定好统一的规章制度,下面各个小弟已经开始各行其道了”的形象。
所以针对 Android 手机刘海屏的适配方案,我们需要分为Android 9及以上与Android 9以下两种方案。
Android 官方为了确保一致性和应用兼容性,搭载 Android 9 的设备必须确保以下刘海行为:
所以,当我们需要以全屏及沉浸的模式显示我们的页面时,我们就需要适配刘海屏。(关于Android沉浸式的理解可以参考 郭霖老师的 Android沉浸式状态栏完全解析)这一篇文章。
而且关于刘海屏的适配,官方提供了三种模式:
具体内容可以参考官方文档 支持刘海屏-选择您的应用如何处理刘海区域
如上所述,我们需要分为Android 9及以上与Android 9以下两种方案。
我们可以分为两步,1.设置刘海模式。2.获取刘海坐标
/**
* @author jere
*/
@RequiresApi(Build.VERSION_CODES.P)
class AndroidPNotchScreen : INotchScreen {
override fun isContainNotch(activity: Activity): Boolean {
var isContainNotch = false
getNotchRectList(activity, object : GetNotchRectListener {
override fun onResult(rectList: List<Rect>) {
isContainNotch = rectList.isNotEmpty()
}
})
return isContainNotch
}
override fun getNotchInfo(activity: Activity, notchInfoCallback: INotchScreen.NotchInfoCallback) {
getNotchRectList(activity, object : GetNotchRectListener {
override fun onResult(rectList: List<Rect>) {
if (rectList.isNotEmpty()) {
//只支持只有一块刘海屏幕
notchInfoCallback.getNotchRect(rectList[0])
}
}
})
}
private fun getNotchRectList(activity: Activity, notchRectListener: GetNotchRectListener) {
//设置刘海区域展示的模式, 会允许应用程序的内容延伸到刘海区域。
val window = activity.window
// 延伸显示区域到耳朵区
val lp = window.attributes
//在竖屏模式和横屏模式下,内容都会呈现到刘海区域中
lp.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
window.attributes = lp
// 允许内容绘制到耳朵区
val decorView = window.decorView
//设置真正的全屏显示
decorView.systemUiVisibility =
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
decorView.post {
kotlin.run {
val windowInsets = decorView.rootWindowInsets
if (windowInsets != null) {
//获取刘海屏的坐标位置
val cutout = windowInsets.displayCutout
if (cutout != null) {
val rectList = cutout.boundingRects
notchRectListener.onResult(rectList)
}
}
}
}
}
interface GetNotchRectListener {
fun onResult(rectList: List<Rect>)
}
}
由于Android 9以下官方是没有出关于刘海屏的API的,所以我们需要针对各大手机生产商给出的刘海屏相关的API进行适配。
fun getNotchScreen(): INotchScreen? {
var notchScreen: INotchScreen? = null
//Android 9及以上,官方才出刘海屏API
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
notchScreen = AndroidPNotchScreen()
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//判断手机生产厂商
when(Build.MANUFACTURER.toLowerCase()) {
HUAWEI -> {
notchScreen = HuaWeiNotchScreen()
}
VIVO -> {
notchScreen = VivoNotchScreen()
}
XIAOMI -> {
notchScreen = XiaoMiNotchScreen()
}
OPPO -> {
notchScreen = OppoNotchScreen()
}
}
}
return notchScreen
}
各大手机生产商之间也是大同小异,都会给出API来判断当前设备是否存在刘海,以及获取刘海信息的API,如:Oppo 会直接给出刘海屏的坐标,华为与小米则会给出刘海屏的长度与高度,Vivo则不给。
如:小米的适配方案
/**
* @author jere
*/
class XiaoMiNotchScreen : INotchScreen {
//参考文档: https://dev.mi.com/console/doc/detail?pId=1293
override fun isContainNotch(activity: Activity): Boolean {
val getInt = Class.forName("android.os.SystemProperties").getMethod(
"getInt",
String::class.java,
Int::class.javaPrimitiveType
)
//值为1时则是 Notch 屏手机
val notchStatusId = getInt.invoke(null, "ro.miui.notch", 0) as Int
Log.e("jereTest", "isContainNotch = $notchStatusId")
return notchStatusId == 1
}
override fun getNotchInfo(activity: Activity, notchInfoCallback: INotchScreen.NotchInfoCallback) {
val notchRect = ScreenUtil.calculateNotchRect(activity, getNotchWidth(activity), getNotchHeight(activity))
notchInfoCallback.getNotchRect(notchRect)
}
/**
* 获取刘海区域的高度
*/
private fun getNotchHeight(context:Context): Int {
var notchHeight = 0
val resourceId: Int = context.resources.getIdentifier("notch_height", "dimen", "android")
if (resourceId > 0) {
notchHeight = context.resources.getDimensionPixelSize(resourceId)
}
Log.e("jereTest", "notch_height = $notchHeight")
return notchHeight
}
/**
* 获取刘海区域的长度
*/
private fun getNotchWidth(context: Context): Int {
var notchWidth = 0
val resourceId: Int = context.resources.getIdentifier("notch_width", "dimen", "android")
if (resourceId > 0) {
notchWidth = context.resources.getDimensionPixelSize(resourceId)
}
Log.e("jereTest", "notch_width = $notchWidth")
return notchWidth
}
/**
* 对特定 Window 作处理
*
* 0x00000100 | 0x00000200 | 0x00000400 横竖屏都绘制到耳朵区
*/
fun addExtraFlags(activity: Activity) {
val flag = 0x00000100 or 0x00000200 or 0x00000400
val method: Method = Window::class.java.getMethod(
"addExtraFlags",
Int::class.javaPrimitiveType
)
method.invoke(activity.window, flag)
}
}
正对上述的适配方案,我整理了一个开源库,具体代码见:传送门 NotchAdapter
核心方法:
interface INotchScreen {
/**
* 当下屏幕是否存在刘海?
*/
fun isContainNotch(activity: Activity): Boolean
/**
* 获取刘海信息参数
*/
fun getNotchInfo(activity: Activity, notchInfoCallback: NotchInfoCallback)
interface NotchInfoCallback {
fun getNotchRect(notchRectInfo: Rect)
}
}
object NotchManager {
private val HUAWEI = "huawei"
private val VIVO = "vivo"
private val XIAOMI = "xiaomi"
private val OPPO = "oppo"
fun getNotchScreen(): INotchScreen? {
var notchScreen: INotchScreen? = null
//Android 9及以上,官方才出刘海屏API
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
notchScreen = AndroidPNotchScreen()
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//判断手机生产厂商
when(Build.MANUFACTURER.toLowerCase()) {
HUAWEI -> {
notchScreen = HuaWeiNotchScreen()
}
VIVO -> {
notchScreen = VivoNotchScreen()
}
XIAOMI -> {
notchScreen = XiaoMiNotchScreen()
}
OPPO -> {
notchScreen = OppoNotchScreen()
}
}
}
return notchScreen
}
/**
* 获取状态栏的高度
*/
fun getStatusBarHeight(context: Context): Int {
var result = 0
val resourceId = context.resources.getIdentifier("status_bar_height", "dimen", "android")
if (resourceId > 0) {
result = context.resources.getDimensionPixelSize(resourceId)
}
return result
}
}
implementation 'cn.jerechen:notchAdapter:1.0.0'
class PortraitTestActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 设置Activity全屏
window.setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
)
setContentView(R.layout.activity_portrait_test)
val notchScreen = NotchManager.getNotchScreen()
val isContainNotch = notchScreen?.isContainNotch(this)
Log.e("jereTest", "portrait activity isContainNotch : $isContainNotch")
notchScreen?.getNotchInfo(this, object : INotchScreen.NotchInfoCallback {
override fun getNotchRect(notchRectInfo: Rect) {
Log.e("jereTest", "Rect Bottom : ${notchRectInfo.bottom}")
//将被刘海挡住的 portraitTitleTv 向下移动一个 刘海高度 距离
val lp: ConstraintLayout.LayoutParams =
portraitTitleTv.layoutParams as ConstraintLayout.LayoutParams
//在原有的 topMargin 基础上再加上 刘海屏的高度
lp.topMargin += notchRectInfo.bottom
portraitTitleTv.layoutParams = lp
}
})
}
}
END~ 到这文章就结束了。
代码生涯的第一个开源库,NotchAdapter 欢迎大家点评 Star
其实分享文章的最大目的正是等待着有人指出我的错误,如果你发现哪里有错误,请毫无保留的指出即可,虚心请教。
另外,如果你觉得文章不错,对你有所帮助,请给我点个赞,就当鼓励,谢谢~Peace~!
参考文档:
支持刘海屏
Android刘海屏、水滴屏全面屏适配方案
小米刘海屏水滴屏 Android O 适配
OPPO凹形屏幕适配说明
Vivo异形屏应用指南