欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

代码生涯的第一个开源库 刘海屏适配

程序员文章站 2024-03-25 10:02:04
...

代码生涯的第一个开源库,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~ 到这文章就结束了。

其实分享文章的最大目的正是等待着有人指出我的错误,如果你发现哪里有错误,请毫无保留的指出即可,虚心请教。

另外,如果你觉得文章不错,对你有所帮助,请给我点个赞,就当鼓励,谢谢~Peace~!

参考文档:
支持刘海屏
Android刘海屏、水滴屏全面屏适配方案
小米刘海屏水滴屏 Android O 适配
OPPO凹形屏幕适配说明
Vivo异形屏应用指南