Android刘海屏适配

代码生涯的第一个开源库,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异形屏应用指南

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值