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

整体效果

主要的功能点:

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

演示如下:

准备图片资源

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

而背景就比较难搞了,很难找到和这些数字搭配的背景,只能小小的妥协一下,最后使用的是这里的资源,也是一只可可爱爱的兔子:

实现 UI 展示

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

kotlin 复制代码
Column {
    Row {
        Image(painter = painterResource(hourTensResId), contentDescription = "hourTensImg")
        Image(painter = ..., contentDescription = "hourUnitsImg")
    }
    Row {
        Image(painter = ..., contentDescription = "minuteTensImg")
        Image(painter = ..., contentDescription = "minuteUnitsImg")
    }
}

渲染完成之后,就是一个普普通通的四宫格布局:

时间更新

定义相关副作用变量

kotlin 复制代码
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 中:

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

接收到广播之后,要做的事情自然就是更新时间了:

kotlin 复制代码
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 中,供时间转换使用。

kotlin 复制代码
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
    }
}

进行时间的转换

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

kotlin 复制代码
@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 仓库

kotlin 复制代码
@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()
    }
}
相关推荐
每次的天空7 小时前
Android学习总结之算法篇四(字符串)
android·学习·算法
x-cmd8 小时前
[250331] Paozhu 发布 1.9.0:C++ Web 框架,比肩脚本语言 | DeaDBeeF 播放器发布 1.10.0
android·linux·开发语言·c++·web·音乐播放器·脚本语言
高林雨露9 小时前
Java 与 Kotlin 对比示例学习(三)
java·kotlin
lc9991029 小时前
基于kotlin native的C与kotlin互相调用
开发语言·kotlin
tangweiguo0305198711 小时前
Android BottomNavigationView 完全自定义指南:图标、文字颜色与选中状态
android
遥不可及zzz12 小时前
Android 应用程序包的 adb 命令
android·adb
无知的前端13 小时前
Flutter 一文精通Isolate,使用场景以及示例
android·flutter·性能优化
_一条咸鱼_13 小时前
Android Compose 入门之字符串与本地化深入剖析(五十三)
android
ModestCoder_14 小时前
将一个新的机器人模型导入最新版isaacLab进行训练(以unitree H1_2为例)
android·java·机器人
robin_suli14 小时前
Spring事务的传播机制
android·java·spring