iT邦幫忙

2024 iThome 鐵人賽

DAY 28
0
Mobile Development

github裡永遠有一個還沒做的SideProject :用Kotlin來開發點沒用的酷東西系列 第 28

Day28:使用Handler實作可暫停、繼續與重置的番茄鐘計時器

  • 分享至 

  • xImage
  •  

前言

今天我們來用Handler實作一個番茄鐘計時器,主要希望透過這個實作來理解Handler的功能。要說明Handler,就不可避免地得順便一起提到Thread和Runnable,他們三個與多執行緒相關的重要概念,它們各自有不同的角色並且可以互相搭配使用。

Thread, Handler, Runnable 在kotlin之間的關係

1. Thread (執行緒)

Thread代表一個執行緒,它允許你在背景中執行任務而不會阻塞主執行緒(通常是 UI 執行緒)。在Kotlin 中,可以通過擴展Thread類或直接創建並運行Runnable來使用執行緒。

2. Runnable

Runnable是一個介面,通常作為執行緒要執行的任務。Runnable只包含一個方法run(),當你把它傳遞給 ThreadHandler時,該方法會在相應的執行緒上執行。

3. Handler

Handler主要用於執行緒之間的通訊,通常在Android應用中,Handler用來發送和處理消息或Runnable。它會將任務排隊,並在與Handler相關聯的Looper所運行的執行緒上執行這些任務。
簡單來說Thread負責在新執行緒中運行任務,Runnable是描述任務的介面,而Handler是用來在特定執行緒上排隊和調度任務的工具,特別是在Android中用於處理UI執行緒的更新。

番茄鐘實作

下面的程式碼是使用Handler來實作一個番茄鐘計時器

  • kotlin
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import java.util.concurrent.TimeUnit

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            PomodoroTimer()
        }
    }
}

@Composable
fun PomodoroTimer() {
    var timeLeftInMillis by remember { mutableStateOf(25 * 60 * 1000L) }  // 25 分鐘 (單位為毫秒)
    var isRunning by remember { mutableStateOf(false) }
    var handler = remember { Handler(Looper.getMainLooper()) }
    var lastUpdateTime by remember { mutableStateOf(0L) }  // 上次更新的時間

    // 每秒更新一次倒數時間
    val updateRunnable = object : Runnable {
        override fun run() {
            if (isRunning) {
                val currentTime = System.currentTimeMillis()
                val elapsedTime = currentTime - lastUpdateTime  // 計算已經經過的時間
                timeLeftInMillis -= elapsedTime  // 減去已經經過的時間
                lastUpdateTime = currentTime  // 更新上次 tick 的時間

                if (timeLeftInMillis > 0) {
                    handler.postDelayed(this, 1000)  // 每秒更新一次
                } else {
                    timeLeftInMillis = 0
                    isRunning = false
                }
            }
        }
    }

    // 將毫秒轉換為 mm:ss 格式
    fun formatTime(timeInMillis: Long): String {
        val minutes = TimeUnit.MILLISECONDS.toMinutes(timeInMillis) % 60
        val seconds = TimeUnit.MILLISECONDS.toSeconds(timeInMillis) % 60
        return String.format("%02d:%02d", minutes, seconds)
    }

    // 開始或繼續計時
    fun startTimer() {
        if (!isRunning) {
            lastUpdateTime = System.currentTimeMillis()  // 記錄當前時間
            handler.post(updateRunnable)  // 開始更新
            isRunning = true
        }
    }

    // 暫停計時
    fun pauseTimer() {
        handler.removeCallbacks(updateRunnable)  // 停止更新
        isRunning = false
    }

    // 重置計時
    fun resetTimer() {
        handler.removeCallbacks(updateRunnable)  // 停止更新
        timeLeftInMillis = 25 * 60 * 1000L  // 重置為 25 分鐘
        isRunning = false  // 停止運行狀態
    }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text(text = "番茄鐘", style = androidx.compose.material3.MaterialTheme.typography.headlineLarge)
        Spacer(modifier = Modifier.height(32.dp))
        Text(text = formatTime(timeLeftInMillis), style = androidx.compose.material3.MaterialTheme.typography.headlineMedium)
        Spacer(modifier = Modifier.height(32.dp))

        Row(horizontalArrangement = Arrangement.SpaceAround) {
            Button(onClick = { if (isRunning) pauseTimer() else startTimer() }) {
                Text(text = if (isRunning) "暫停" else "開始")
            }
            Spacer(modifier = Modifier.width(16.dp))
            Button(onClick = { resetTimer() }) {
                Text(text = "重置")
            }
        }
    }
}

@Preview(showBackground = true)
@Composable
fun PomodoroTimerPreview() {
    PomodoroTimer()
}

Handler是Android用來處理線程之間消息傳遞的工具,我們利用它在主線程中每秒更新一次UI。Runnable是一個可以被Handler執行的任務,在這裡我們每秒執行一次,來更新剩餘的時間。

  • kotlin
var handler = remember { Handler(Looper.getMainLooper()) }

這裡,我們初始化了一個Handler,並且將它關聯到Looper.getMainLooper()MainLooper是Android 的主線程(UI 線程),這意味著這個Handler將會在主線程上運行它處理的任務,從而保證更新倒數時間的操作與界面顯示保持一致,避免異步操作造成的問題。

  • kotlin
val updateRunnable = object : Runnable {
    override fun run() {
        if (isRunning) {
            val currentTime = System.currentTimeMillis()
            val elapsedTime = currentTime - lastUpdateTime  // 計算已經經過的時間
            timeLeftInMillis -= elapsedTime  // 減去已經經過的時間
            lastUpdateTime = currentTime  // 更新上次 tick 的時間

            if (timeLeftInMillis > 0) {
                handler.postDelayed(this, 1000)  // 每秒更新一次
            } else {
                timeLeftInMillis = 0
                isRunning = false
            }
        }
    }
}

上面這段程式碼創建了一個Runnable,它每秒會更新一次計時器的狀態。我們可以把它想像成一個不斷重複運行的「任務」,每次運行時,Runnable 都會更新剩餘時間,然後重新計劃自己在1秒後再次運行。
在這個計時器中,Handler扮演了核心的計時角色,它確保Runnable在正確的時間點執行,並且控制整個倒數過程,同時通過RunnablepostDelayed(),它讓我們可以控制倒數計時的開始、暫停和繼續,並且確保計時過程中的UI變化都能順利運行。
APP效果如下
Yes

後話

今天我們透過一個簡單的實作來了解Handler的用法和作用,我們今天的內容就先到這邊,讓我們明天再見。


上一篇
Day27:在 Android 中使用 Lottie 動畫
下一篇
Day29:在Android Studio使用Github
系列文
github裡永遠有一個還沒做的SideProject :用Kotlin來開發點沒用的酷東西30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言