夜间模式

本篇将讲解Android夜间模式,用添加蒙版的方式来实现

夜间模式

本篇已Kotlin作为主要语言

在Android中夜间模式算是一个必不可少、经常使用的一个功能。夜间模式的实现通常来说有两种方式,第一种是通过更换主题颜色来实现夜间模式、第二种是在所有界面上都添加一层灰色蒙版。下面将大概讲解蒙版方式

主题方式

主题方式简单来说就是将全局XML文件中的颜色都设置为主题颜色,然后再设计一套夜间模式下的主题颜色。通过更换主题来达到切换夜间模式的效果

蒙版方式

既然是添加蒙版了,那先得考虑一下将蒙版添加到哪里,剩下的待会再说

  • 蒙版添加位置

    这里我们将蒙版添加到Activity窗口的根视图,也就是DecorView,下面为DecorView视图结构

    img

    而PhoneWindow是Window类的具体实现,一般可以通过该类去绘制窗口,是Activity和整个View系统交互的接口,所以没必要放在这里

既然位置找好了,下一步就是获取DecorView,毕竟是需要向里面添加一个蒙版,所以现在是要获取DecorView。

DecorView是一个FrameLayout,DecorView内部又分为两部分,一部分是ActionBar,另一部分是ContentParent,即activity在setContentView对应的布局

  • 获取DecorView

    在Activity还在展示的时候,可以通过如下代码进行获取

    1
    2
    3
    4
    5
    6
    7
    8
    9
    fun getMDecorView(activity: Activity): FrameLayout? {
    // 为了处理在api小于17的时候isDestroyed无法使用的情况
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
    if (!activity.isFinishing && !activity.isDestroyed) return activity.window?.decorView as? FrameLayout
    } else {
    if (!activity.isFinishing) return activity.window?.decorView as? FrameLayout
    }
    return null
    }

    也可以这样写,写成扩展函数,方便后面的使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    val Activity.decorView: FrameLayout?
    get() = (takeIf {
    // 为了处理在api小于17的时候isDestroyed无法使用的情况
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
    !isFinishing && !isDestroyed
    } else {
    !isFinishing
    }
    }?.window?.decorView) as? FrameLayout

    这里需要强转一下,getDecorView返回的类型是View (强转要用 as? 以免强转失败而崩溃)

接下来就是向DecorView中添加蒙版View了,但是添加的时机也是非常重要的。要是添加时机晚了,在进入新页面时,会有一个”闪一下“的过程

  • 添加蒙版时机

    为了避免出现”闪一下“的问题,这里通过Handler.postAtFrontOfQueue()将添加蒙版View任务放入消息队列的头部,进行优先处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    private val handler = Handler(Looper.getMainLooper())

    /**
    * 设置是否为夜间模式
    */
    fun nightMode(activity: AppCompatActivity, lightOff: Boolean) {
    ...
    // 为了避免界面展示出来后黑一下,所以将“添加蒙版”任务添加到主线程消息队列的头部,优先处理
    handler.postAtFrontOfQueue {
    ...
    }
    ...
  • 添加蒙版View

    添加的时机也解决了,剩下的自然就是添加View了。这里添加View是使用的动态构建布局,动态的进行添加和移除

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    private val handler = Handler(Looper.getMainLooper())
    /**
    * 夜间模板统一Layout_id
    */
    private const val mId = "darkMask"
    const val match_parent = ViewGroup.LayoutParams.MATCH_PARENT
    const val wrap_content = ViewGroup.LayoutParams.WRAP_CONTENT
    const val parent_id = "0"

    /**
    * 设置是否为夜间模式
    */
    fun nightMode(activity: AppCompatActivity, lightOff: Boolean) {
    if (lightOff) {
    // 为了避免界面展示出来后黑一下,所以将“添加蒙版”任务添加到主线程消息队列的头部,优先处理
    handler.postAtFrontOfQueue {
    val maskView = activity.View {
    layout_id = mId // 将LayoutId都指定为统一的id
    layout_width = match_parent
    layout_height = match_parent
    background_color = getColors(R.color.ui_status_dark_color)
    }
    activity.decorView?.apply {
    // 判断View是否已经添加
    val view = findViewById<View>(mId.toLayoutId())
    if (view == null) {
    addView(maskView)
    }
    }
    }
    } else {
    // 若不是夜间模式,则移除View
    activity.decorView?.apply {
    find<View>(mId)?.let { removeView(it) }
    }
    }
    ...

    蒙版的背景这里是用的 60% 透明度的黑色背景(颜色值为:#99000000)

    这里的添加和移除都是对相同LayoutId的View进行的操作,避免出错

  • 内联+扩展函数

    上方的layout_id、layout_width、layout_height等都是使用了扩展函数,方便使用和提升可读性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    inline fun Context.View(init: View.() -> Unit) =
    View(this).apply(init)

    inline var View.layout_id: String
    get() {
    return ""
    }
    set(value) {
    id = value.toLayoutId()
    }
    inline var View.layout_width: Int
    get() {
    return 0
    }
    set(value) {}

    inline var View.layout_height: Int
    get() {
    return 0
    }
    set(value) {}

    inline var View.background_color: Int
    get() {
    return 0
    }
    set(value) {
    setBackgroundColor(value)
    }

    inline var View.layout_visibility: Int
    get() {
    return -1
    }
    set(value) {
    visibility = value
    }

    fun String.toLayoutId(): Int {
    var id = java.lang.String(this).bytes.sum()
    if (id == 48) id = 0
    return id
    }

    fun <T : View> View.find(id: String): T? = findViewById(id.toLayoutId())

    基本上就完成了,剩下的可以创建一个Activity的基类,将添加蒙版方法放入其中,来进行统一管理

    至于上方的String.toLayoutId():

    View.setId(int id)接收 int 类型的值,但 int 值没有语义,起不到标记控件的作用,所以扩展属性layout_id是 String 类型的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    inline var View.layout_id: String
    get() {
    return ""
    }
    set(value) {
    id = value.toLayoutId()
    }

    //'将String转化成对应的Int值'
    fun String.toLayoutId():Int{
    var id = java.lang.String(this).bytes.sum()
    if (id == 48) id = 0
    return id
    }

    String 必须转化成 Int 才能调用View.setId(),采用的方法是:先将 String 转化成 byte 数组,然后对数组累加。但 Kotlin 中的 String 没有getBytes(),所以只能显示地构造java.lang.String

动画效果

至于动画效果嘛,可以在添加蒙版View的时候先将透明度设置为0f,在使用属性动画使渐显示。移除的时候也可以先使用动画进行隐藏再移除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
fun nightAnimMode(activity: AppCompatActivity, lightOff: Boolean, animTime: Long) {
if (lightOff) {
handler.postAtFrontOfQueue {
val maskView = activity.View {
layout_id = mId
layout_width = match_parent
layout_height = match_parent
background_color = getColors(R.color.ui_status_dark_color)
// 先将透明度设置为0f,也就是先隐藏,后通过动画来显示
this.alpha = 0f
}
activity.decorView?.apply {
val view = findViewById<View>(mId.toLayoutId())
if (view == null) {
this.addView(maskView).let {
// 添加完View之后,再获取View来进行属性动画
find<View>(mId)?.let {
val animation = AlphaAnimation(0f, 1.0f)
animation.duration = animTime
it.startAnimation(animation)
it.alpha = 1.0f
}
}
}
}
}
} else {
activity.decorView?.apply {
find<View>(mId)?.let {
// 先隐藏,后移除
val animation = AlphaAnimation(1.0f, 0f)
animation.duration = animTime
it.startAnimation(animation)
removeView(it)
}
}
}
}