本篇将讲解Android夜间模式,用添加蒙版的方式来实现
夜间模式
本篇已Kotlin作为主要语言
在Android中夜间模式算是一个必不可少、经常使用的一个功能。夜间模式的实现通常来说有两种方式,第一种是通过更换主题颜色来实现夜间模式、第二种是在所有界面上都添加一层灰色蒙版。下面将大概讲解蒙版方式
主题方式
主题方式简单来说就是将全局XML文件中的颜色都设置为主题颜色,然后再设计一套夜间模式下的主题颜色。通过更换主题来达到切换夜间模式的效果
蒙版方式
既然是添加蒙版了,那先得考虑一下将蒙版添加到哪里,剩下的待会再说
蒙版添加位置
这里我们将蒙版添加到Activity窗口的根视图,也就是DecorView,下面为DecorView视图结构

而PhoneWindow是Window类的具体实现,一般可以通过该类去绘制窗口,是Activity和整个View系统交互的接口,所以没必要放在这里
既然位置找好了,下一步就是获取DecorView,毕竟是需要向里面添加一个蒙版,所以现在是要获取DecorView。
DecorView是一个FrameLayout,DecorView内部又分为两部分,一部分是ActionBar,另一部分是ContentParent,即activity在setContentView对应的布局
获取DecorView
在Activity还在展示的时候,可以通过如下代码进行获取
1
2
3
4
5
6
7
8
9fun 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
9val 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
12private 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
37private 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
45inline 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
14inline 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 | fun nightAnimMode(activity: AppCompatActivity, lightOff: Boolean, animTime: Long) { |