【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()
    }
}
相关推荐
JMchen12333 分钟前
Android后台服务与网络保活:WorkManager的实战应用
android·java·网络·kotlin·php·android-studio
crmscs1 小时前
剪映永久解锁版/电脑版永久会员VIP/安卓SVIP手机永久版下载
android·智能手机·电脑
localbob1 小时前
杀戮尖塔 v6 MOD整合版(Slay the Spire)安卓+PC端免安装中文版分享 卡牌肉鸽神作!杀戮尖塔中文版,电脑和手机都能玩!杀戮尖塔.exe 杀戮尖塔.apk
android·杀戮尖塔apk·杀戮尖塔exe·游戏分享
机建狂魔1 小时前
手机秒变电影机:Blackmagic Camera + LUT滤镜包的专业级视频解决方案
android·拍照·摄影·lut滤镜·拍摄·摄像·录像
hudawei9961 小时前
flutter和Android动画的对比
android·flutter·动画
lxysbly3 小时前
md模拟器安卓版带金手指2026
android
儿歌八万首4 小时前
硬核春节:用 Compose 打造“赛博鞭炮”
android·kotlin·compose·春节
消失的旧时光-19437 小时前
从 Kotlin 到 Dart:为什么 sealed 是处理「多种返回结果」的最佳方式?
android·开发语言·flutter·架构·kotlin·sealed
有位神秘人7 小时前
kotlin与Java中的单例模式总结
java·单例模式·kotlin
Jinkxs7 小时前
Gradle - 与Groovy/Kotlin DSL对比 构建脚本语言选择指南
android·开发语言·kotlin