【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()
    }
}
相关推荐
居然是阿宋39 分钟前
Kotlin高阶函数 vs Lambda表达式:关键区别与协作关系
android·开发语言·kotlin
wangz7643 分钟前
kotlin的kmp编程中遇到Unresolved reference ‘java‘问题
kotlin·kmp
凉、介1 小时前
PCI 总线学习笔记(五)
android·linux·笔记·学习·pcie·pci
小贾要学习2 小时前
【C++】继承----下篇
android·java·c++
投笔丶从戎4 小时前
Kotlin Multiplatform--01:项目结构基础
android·开发语言·kotlin
Lary_Rock5 小时前
Android 编译问题 prebuilts/clang/host/linux-x86
android·linux·运维
王江奎5 小时前
Android FFmpeg 交叉编译全指南:NDK编译 + CMake 集成
android·ffmpeg
limingade5 小时前
手机打电话通话时如何向对方播放录制的IVR引导词声音
android·智能手机·蓝牙电话·手机提取通话声音
hepherd6 小时前
Flutter 环境搭建 (Android)
android·flutter·visual studio code