整体效果

主要的功能点:

  1. 监听系统时刻改变,展现对应的数字图片
  2. 监听时区改变,改变对应的时刻显示
  3. 自带中秋节元素,中秋节快乐!

演示如下:

准备图片资源

数字资源来自千库网,是一系列 0~9 的图片,因为自己没有会员,是找某宝购买的,还算实惠。所有的资源大约长这样:

【Jetpack Compose】100 行代码实现数字图片时钟

而布景就比较难搞了,很难找到和这些数字调配的布景,只能小小的妥协一下,最后运用的是这儿的资源,也是一只可心爱爱的兔子

【Jetpack Compose】100 行代码实现数字图片时钟

完成 UI 展现

如下代码信任各位 1s 就能理解:

Column {
    Row {
        Image(painter = painterResource(hourTensResId), contentDescription = "hourTensImg")
        Image(painter = ..., contentDescription = "hourUnitsImg")
    }
    Row {
        Image(painter = ..., contentDescription = "minuteTensImg")
        Image(painter = ..., contentDescription = "minuteUnitsImg")
    }
}

烘托完成之后,便是一个普普通通的四宫格布局:

【Jetpack Compose】100 行代码实现数字图片时钟

时刻更新

界说相关副作用变量

val calendar = remember { Calendar.getInstance() }
val currentTime = remember { mutableStateOf(System.currentTimeMillis()) }
val (hourTensResId, hourUnitsResId, minuteTensResId, minuteUnitsResId) =
        rememberTimeResourceIds(calendar, currentTime)
  1. 界说 calendar ,作为工具类,用来完成时刻的转化
  2. 界说 currentTime ,更新 currentTime 来触发 UI 的刷新
  3. 界说 hourTensResIdhourUnitsResIdminuteTensResIdminuteUnitsResId 这几个变量,其跟随 currentTime 改变而改变,输出图片的资源 id,用来更新数字图片时钟。

分钟级的时刻改变

分钟级别的时刻改变,界说播送接纳器监听这两个 Action: Intent.ACTION_TIME_TICKIntent.ACTION_TIME_CHANGED。这儿因为在 dispose 之后要取消播送的注册,防止内存走漏。所以咱们将播送注册和解绑的代码放到 DisposableEffect 中:

DisposableEffect(Unit) {
    val filter = IntentFilter().apply {
        addAction(Intent.ACTION_TIME_TICK)
        addAction(Intent.ACTION_TIME_CHANGED)
    }
    context.registerReceiver(receiver, filter)
    onDispose {
        context.unregisterReceiver(receiver)
    }
}

接纳到播送之后,要做的事情天然便是更新时刻了:

object : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        currentTime.value = System.currentTimeMillis()
        calendar.timeInMillis = currentTime.value
    }
}

更新 currentTime 来触发时刻的更新,进而驱动 UI 的改变

更新 calendar.timeInMillis 来改变 calendar 内部的时刻,进行时刻的转化

监听时区的改变

监听时区的改变,只需要在 IntentFilter() 里加上 Intent.ACTION_TIMEZONE_CHANGED 即可。

同时,onReceive() 方法增加相应的逻辑。把 timeZone 塞到 calendar 中,供时刻转化运用。

object : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        if (Intent.ACTION_TIMEZONE_CHANGED == intent?.action) {
            val timeZone = TimeZone.getTimeZone(intent.getStringExtra(Intent.EXTRA_TIMEZONE))
            calendar.timeZone = timeZone
        }
        // 时刻更新的逻辑不变
        currentTime.value = System.currentTimeMillis()
        calendar.timeInMillis = currentTime.value
    }
}

进行时刻的转化

如下简单列举一下时钟的转化逻辑,分钟的转化逻辑也是类似的

@Composable
fun rememberTimeResourceIds(calendar: Calendar, currentTime: MutableState<Long>): TimeResourceIds {
    // 小时十位数的资源ID
    val hourTensResId by remember(currentTime.value) {
        derivedStateOf { getResourceId(calendar.get(Calendar.HOUR_OF_DAY) / 10) }
    }
    // ...
}

咱们界说了 rememberTimeResourceIds() 方法,其接纳 currentTime ,当 currentTime.value 改变时,会进行相应的计算,然后驱动 hourTensResId 的更新。

值得注意的是咱们这儿运用 derivedStateOf ,它会缓存上一次的计算结果,当新值和上一次的值相同时,将不会触发 hourTensResId 的更新逻辑,然后减少 Compose 的重组。

具体到当前场景,时刻改变会每分钟触发一次,而 60 次分钟的改变才会触发一次小时的改变。每次时刻改变之后,假如小时数没变,hourTensResId 也不应该改变,分钟相关的 ResId 改变即可。derivedStateOf 是非常合适当前场景的。

总结

运用 Compose 写自界说 View,不免让人不和 Android 传统自界说 View 的书写做比照。总的来说,运用 Compose 写自界说 View 会愈加舒畅一点,上手的难度也不高,没有传统的自界说 View 那么多啰啰嗦嗦的代码。一切都是函数,都是组合,都是必要的逻辑。纯纯的数据驱动 UI,舒畅!

REFERENCE

core/java/android/widget/TextClock.java – platform/frameworks/base – Git at Google

源代码

真的是 100 行,不多也不少 。 Github 库房

@Composable
fun ImageClock() {
    val calendar = remember { Calendar.getInstance() }
    val currentTime = remember { mutableStateOf(System.currentTimeMillis()) }
    val (hourTensResId, hourUnitsResId, minuteTensResId, minuteUnitsResId) =
        rememberTimeResourceIds(calendar, currentTime)
    val receiver = rememberBroadcastReceiver(calendar, currentTime)
    val context = LocalContext.current
    DisposableEffect(Unit) {
        val filter = IntentFilter().apply {
            addAction(Intent.ACTION_TIME_TICK)
            addAction(Intent.ACTION_TIME_CHANGED)
            addAction(Intent.ACTION_TIMEZONE_CHANGED)
        }
        context.registerReceiver(receiver, filter)
        onDispose {
            context.unregisterReceiver(receiver)
        }
    }
    Column(modifier = Modifier.padding(20.dp)) {
        Row {
            Image(painter = painterResource(hourTensResId), contentDescription = "hourTensImg")
            Image(painter = painterResource(hourUnitsResId), contentDescription = "hourUnitsImg")
        }
        Row {
            Image(painter = painterResource(minuteTensResId), contentDescription = "minuteTensImg")
            Image(painter = painterResource(minuteUnitsResId), contentDescription = "minuteUnitsImg")
        }
    }
}
@Composable
private fun rememberBroadcastReceiver(
    calendar: Calendar,
    currentTime: MutableState<Long>
) = remember {
    object : BroadcastReceiver() {
        @RequiresApi(Build.VERSION_CODES.R)
        override fun onReceive(context: Context?, intent: Intent?) {
            if (Intent.ACTION_TIMEZONE_CHANGED == intent?.action) {
                val timeZone = TimeZone.getTimeZone(intent.getStringExtra(Intent.EXTRA_TIMEZONE))
                calendar.timeZone = timeZone
            }
            // update time
            currentTime.value = System.currentTimeMillis()
            calendar.timeInMillis = currentTime.value
        }
    }
}
fun getResourceId(num: Int) = when (num) {
    0 -> R.drawable.clock_num_0
    1 -> R.drawable.clock_num_1
    2 -> R.drawable.clock_num_2
    3 -> R.drawable.clock_num_3
    4 -> R.drawable.clock_num_4
    5 -> R.drawable.clock_num_5
    6 -> R.drawable.clock_num_6
    7 -> R.drawable.clock_num_7
    8 -> R.drawable.clock_num_8
    9 -> R.drawable.clock_num_9
    else -> R.drawable.clock_num_0
}
data class TimeResourceIds(
    val hourTensResId: Int,
    val hourUnitsResId: Int,
    val minuteTensResId: Int,
    val minuteUnitsResId: Int
)
@Composable
fun rememberTimeResourceIds(calendar: Calendar, currentTime: MutableState<Long>): TimeResourceIds {
    val hourTensResId by remember(currentTime.value) {
        derivedStateOf { getResourceId(calendar.get(Calendar.HOUR_OF_DAY) / 10) }
    }
    val hourUnitsResId by remember(currentTime.value) {
        derivedStateOf { getResourceId(calendar.get(Calendar.HOUR_OF_DAY) % 10) }
    }
    val minuteTensResId by remember(currentTime.value) {
        derivedStateOf { getResourceId(calendar.get(Calendar.MINUTE) / 10) }
    }
    val minuteUnitsResId by remember(currentTime.value) {
        derivedStateOf { getResourceId(calendar.get(Calendar.MINUTE) % 10) }
    }
    return TimeResourceIds(hourTensResId, hourUnitsResId, minuteTensResId, minuteUnitsResId)
}
@Preview
@Composable
fun ImageClockPreview() {
    ImageClockTheme {
        ImageClock()
    }
}