代碼生涯的第一個開源庫 劉海屏適配

代碼生涯的第一個開源庫,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

項目結構

核心方法:

  1. 定義劉海屏接口,包含是否存在劉海與獲取劉海信息方法。
interface INotchScreen {

    /**
     * 當下屏幕是否存在劉海?
     */
    fun isContainNotch(activity: Activity): Boolean

    /**
     * 獲取劉海信息參數
     */
    fun getNotchInfo(activity: Activity, notchInfoCallback: NotchInfoCallback)

    interface NotchInfoCallback {
        fun getNotchRect(notchRectInfo: Rect)
    }
}
  1. 通過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 使用方法

  1. 在 app 級別的 build.gradle 下加入依賴:
implementation 'cn.jerechen:notchAdapter:1.0.0'
  1. 在需要適配劉海的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異形屏應用指南

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章