代碼生涯的第一個開源庫,NotchAdapter 歡迎大家點評 Star
1.前言
自從2017年 iphone X 問世,劉海屏幕(Notch Screen)也開始流行。但是正如上圖官方文檔所介紹的,Android 官方是從 Android P (Android 9 API 28)開始才正式開始支持劉海屏幕的適配。也就造成了 “上面老大哥還沒定好統一的規章制度,下面各個小弟已經開始各行其道了”的形象。
所以針對 Android 手機劉海屏的適配方案,我們需要分爲Android 9及以上與Android 9以下兩種方案。
1.1 什麼時候需要適配劉海屏
Android 官方爲了確保一致性和應用兼容性,搭載 Android 9 的設備必須確保以下劉海行爲:
- 一條邊緣最多隻能包含一個劉海。
- 一臺設備不能有兩個以上的劉海。
- 設備的兩條較長邊緣上不能有劉海。
- 在未設置特殊標誌的豎屏模式下,狀態欄的高度必須至少與劉海的高度持平。
- 默認情況下,在全屏模式或橫屏模式下,整個劉海區域必須顯示黑邊。
所以,當我們需要以全屏及沉浸的模式顯示我們的頁面時,我們就需要適配劉海屏。(關於Android沉浸式的理解可以參考 郭霖老師的 Android沉浸式狀態欄完全解析)這一篇文章。
而且關於劉海屏的適配,官方提供了三種模式:
- LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT : 這是默認行爲,如上所述。在豎屏模式下,內容會呈現到劉海區域中;但在橫屏模式下,內容會顯示黑邊。
- LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES : 在豎屏模式和橫屏模式下,內容都會呈現到劉海區域中。
- LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER : 內容從不呈現到劉海區域中。
具體內容可以參考官方文檔 支持劉海屏-選擇您的應用如何處理劉海區域
2.適配方案
如上所述,我們需要分爲Android 9及以上與Android 9以下兩種方案。
2.1 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>)
}
}
2.2 Android 9以下
由於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)
}
}
3. 開源庫 NotchAdapter
正對上述的適配方案,我整理了一個開源庫,具體代碼見:傳送門 NotchAdapter
核心方法:
- 定義劉海屏接口,包含是否存在劉海與獲取劉海信息方法。
interface INotchScreen {
/**
* 當下屏幕是否存在劉海?
*/
fun isContainNotch(activity: Activity): Boolean
/**
* 獲取劉海信息參數
*/
fun getNotchInfo(activity: Activity, notchInfoCallback: NotchInfoCallback)
interface NotchInfoCallback {
fun getNotchRect(notchRectInfo: Rect)
}
}
- 通過API等級與手機生產商定義不同的適配方案。
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
}
}
3.1 使用方法
- 在 app 級別的 build.gradle 下加入依賴:
implementation 'cn.jerechen:notchAdapter:1.0.0'
- 在需要適配劉海的Activity中
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異形屏應用指南