2024. 5. 14. 17:28, ๐ฑAndroid Study
๋ฐ์ํ
Thread๋ฅผ ๋ณต์ตํ๊ธฐ ์ํ ๊ฐ๋จํ ์คํฑ์์น ํ๋ก์ ํธ๋ฅผ ์ ์ํ๋ค. (Joyce ์์ ์์)
- ๋ฉ์ธ ์ค๋ ๋
- ์ฑ์ด ์ฒ์ ์์๋ ๋ ์์ฑ๋๋ ์ค๋ ๋
- ์กํฐ๋นํฐ์ ๋ชจ๋ ์๋ช ์ฃผ๊ธฐ ๊ด๋ จ ์ฝ๋ฐฑ ์คํ์ ๋ด๋น
- ๋ฒํผ, ์๋งํ ์คํธ์ ๊ฐ์ UI ์์ ฏ์ ์ฌ์ฉํ ์ฌ์ฉ์ ์ด๋ฒคํธ์ UI ๋๋ก์ ์ด๋ฒคํธ ๋ด๋น
- Handler ํด๋์ค, AsyncTask ํด๋์ค, runOnUiThread() ๋ฉ์๋ ๋ฑ ํ์ฉ - ๋ฐฑ๊ทธ๋ผ์ด๋ ์ค๋ ๋
- ์์ ๋์ด ํฐ ์ฐ์ฐ, ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฟผ๋ฆฌ, ๋คํธ์ํฌ ํต์ ๋ฑ
- ์ ๋๋ก UI๊ด๋ จ ์ฐ์ฐ์ ๋ฐฑ๊ทธ๋ผ์ด๋ ์ค๋ ๋์์ ํ๋ฉด ์ ๋จ
(๊ฐ ๋ฐฑ๊ทธ๋ผ์ด๋ ์ค๋ ๋๊ฐ ์ธ์ ์ฒ๋ฆฌ๋ฅผ ๋๋ด๊ณ UI์ ์ ๊ทผํ ์ง ์ ์ ์์) - ๊ตฌ๊ธ์์๋ ์ฑ ๊ฐ๋ฐ ๋น์์ ์ต์ API๋ฅผ ํ๊ฒ API๋ก ์์ฑํ ๊ฒ์ ๊ถ๊ณ ํ๊ณ ์์ (๋ณด์, ์ฑ๋ฅ ๋ฑ)
- ์๋ถ์ด ์ ๋ ฌ ์ Baseline์ ์ด์ฉํ Constraint Layout ์ ๋ ฌ
๐ก timer(period = ){}
private fun start() { // ์คํฑ์์น ์ธก์ ์์
binding.btnStart.text = "์ผ์์ ์ง"
binding.btnStart.setBackgroundColor(getColor(R.color.red))
isRunning = true // ์คํ ์ํ ๋ณ๊ฒฝ
timer = timer(period = 10){ // ์คํฑ์์น ์์ ๋ก์ง
time++ // 10๋ฐ๋ฆฌ์ด ๋จ์ ํ์ด๋จธ
val millisecond = time % 100
val second = (time % 6000) / 100
val minute = time / 6000
binding.tvMillisecond.text =
if (millisecond < 10) ".0${millisecond}" else ".${millisecond}"
binding.tvSecond.text =
if (second < 10) ":0${second}" else ".${second}"
binding.tvMinute.text = "${minute}"
}
}
- ์ฝํ๋ฆฐ์์ ์ ๊ณตํ๋ timer(period = [์ฃผ๊ธฐ]) {} ํจ์๋ ์ผ์ ํ ์ฃผ๊ธฐ๋ก ๋ฐ๋ณตํ๋ ๋์์ ์ํํ ๋ ์ ์ฉํ๊ฒ ์ฐ์
- {} ์์ ์ฐ์ธ ์ฝ๋๋ค์ ๋ชจ๋ ๋ฐฑ๊ทธ๋ผ์ด๋ ์ค๋ ๋์์ ์คํ
- ์ฃผ๊ธฐ๋ฅผ ๋ํ๋ด๋ period ๋ณ์๋ฅผ 10์ผ๋ก ์ง์ ํ์ผ๋ฏ๋ก 10๋ฐ๋ฆฌ์ด๋ง๋ค ์คํ
- 0.01์ด๋ง๋ค time์ 1์ ๋ํ๋ ๊ฒ์ ์ฃผ๊ธฐ๊ฐ 10๋ฐ๋ฆฌ์ด์ด๊ธฐ ๋๋ฌธ
- ๋ทฐ์ ๊ณ์ธต ๊ตฌ์กฐ๋ฅผ ์์ฑํ ๋ฉ์ธ ์ค๋ ๋์์๋ง ๊ทธ ๋ทฐ๋ค์ ์ ๊ทผํ ์ ์์
-> ๋ฐฑ๊ทธ๋ผ์ด๋ ์ค๋ ๋์์ UI์์ ์ ํ๋ ์ด ์ฝ๋์ ๊ฒฝ์ฐ ์ค๋ฅ๊ฐ ๋ธ - ๋ฐ๋ผ์ ์๋์ ๊ฐ์ด ์์
๐ก runOnUiThread
private fun start() { // ์คํฑ์์น ์ธก์ ์์
binding.btnStart.text = "์ผ์์ ์ง"
binding.btnStart.setBackgroundColor(getColor(R.color.red))
isRunning = true // ์คํ ์ํ ๋ณ๊ฒฝ
timer = timer(period = 10) { // ์คํฑ์์น ์์ ๋ก์ง
time++ // 10๋ฐ๋ฆฌ์ด ๋จ์ ํ์ด๋จธ
val millisecond = time % 100
val second = (time % 6000) / 100
val minute = time / 6000
runOnUiThread { // UI ์ค๋ ๋ ์์ฑ
if (isRunning) // UI ์
๋ฐ์ดํธ ์กฐ๊ฑด ์ค์
binding.tvMillisecond.text =
if (millisecond < 10) ".0${millisecond}" else ".${millisecond}"
binding.tvSecond.text =
if (second < 10) ":0${second}" else ".${second}"
binding.tvMinute.text = "${minute}"
}
}
}
- runonUiThread๋ก UI ์ค๋ ๋๋ฅผ ์์ฑํด์ฃผ๊ณ , ๊ทธ ์์ ํ ์คํธ๋ทฐ๋ฅผ ์ค์ ํ๋ ์ฝ๋๋ฅผ ๋ค ๋ฃ์ด์ฃผ๋ฉด ์ ์ ์๋
- isRunning์ด true์ผ ๋๋ง UI๊ฐ ์
๋ฐ์ดํธ ๋์ด์ผ ํจ
-> ์ฌ์ฉ์๊ฐ ํ์ด๋จธ๋ฅผ ์ ์งํ๋ ์์ ๊ณผ UI์ค๋ ๋์์ ์ฝ๋๊ฐ ์คํ๋๋ ์์ ์ด ๋ค๋ฅผ ์ ์๊ธฐ ๋๋ฌธ
๐ก ์ต์ข ๊ฒฐ๊ณผ ๋ฐ ์ฝ๋
๋๋ณด๊ธฐ
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/tv_minute"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textSize="45sp"
app:layout_constraintBaseline_toBaselineOf="@+id/tv_second"
app:layout_constraintEnd_toStartOf="@+id/tv_second"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/tv_second"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=":00"
android:textSize="45sp"
app:layout_constraintBottom_toTopOf="@+id/btn_refresh"
app:layout_constraintEnd_toStartOf="@+id/tv_millisecond"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/tv_minute"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_millisecond"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=".00"
android:textSize="30sp"
app:layout_constraintBaseline_toBaselineOf="@+id/tv_second"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/tv_second" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_refresh"
android:layout_width="100dp"
android:layout_height="70dp"
android:layout_marginBottom="50dp"
android:background="@color/yellow"
android:padding="20dp"
android:text="@string/refresh"
android:textColor="@color/white"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/btn_start"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_start"
android:layout_width="100dp"
android:layout_height="70dp"
android:layout_marginBottom="80dp"
android:background="@color/blue"
android:padding="20dp"
android:text="@string/start"
android:textColor="@color/white"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity.kt
package com.limheejin.stopwatch
import android.os.Bundle
import android.view.View
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.limheejin.stopwatch.databinding.ActivityMainBinding
import java.util.Timer
import kotlin.concurrent.timer
class MainActivity : AppCompatActivity(), View.OnClickListener { // ํด๋ฆญ ์ด๋ฒคํธ ์ฒ๋ฆฌ ์ธํฐํ์ด์ค
private var isRunning = false // ์คํ ์ฌ๋ถ ํ์ธ์ฉ ๋ณ์
private var timer: Timer? = null // timer ๋ณ์ ์ถ๊ฐ
private var time = 0 // time ๋ณ์ ์ถ๊ฐ
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
enableEdgeToEdge()
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
initViews()
setupListeners()
}
private fun initViews() {
binding.btnStart.text = getString(R.string.start)
binding.btnStart.setBackgroundColor(ContextCompat.getColor(this, R.color.blue))
binding.tvMillisecond.text = ".00"
binding.tvSecond.text = ":00"
binding.tvMinute.text = "0"
}
private fun setupListeners() {
binding.btnStart.setOnClickListener(this)
binding.btnRefresh.setOnClickListener(this)
}
override fun onClick(v: View?) {
when (v?.id) {
R.id.btn_start -> {
if (isRunning) {
pause()
} else {
start()
}
}
R.id.btn_refresh -> {
refresh()
}
}
}
private fun start() { // ์คํฑ์์น ์ธก์ ์์
binding.btnStart.text = getString(R.string.pause)
binding.btnStart.setBackgroundColor(getColor(R.color.red))
isRunning = true // ์คํ ์ํ ๋ณ๊ฒฝ
timer = timer(period = 10) { // ์คํฑ์์น ์์ ๋ก์ง
time++ // 10๋ฐ๋ฆฌ์ด ๋จ์ ํ์ด๋จธ
val millisecond = time % 100
val second = (time % 6000) / 100
val minute = time / 6000
runOnUiThread { // UI ์ค๋ ๋ ์์ฑ
if (isRunning) // UI ์
๋ฐ์ดํธ ์กฐ๊ฑด ์ค์
binding.tvMillisecond.text = ".${millisecond.toString().padStart(2, '0')}"
binding.tvSecond.text = ":${second.toString().padStart(2, '0')}"
binding.tvMinute.text = minute.toString()
}
}
}
private fun pause() { // ์คํฑ์์น ์ผ์์ ์ง
isRunning = false // ๋ฉ์ถค์ผ๋ก ์ ํ
timer?.cancel() // ํ์ด๋จธ ๋ฉ์ถ๊ธฐ - cancel()ํจ์๋ ๋ฐฑ๊ทธ๋ผ์ด๋ ์ค๋ ๋์ ์๋ ํ๋ฅผ ๊น๋ํ๊ฒ ๋น์
binding.btnStart.text = getString(R.string.start)
binding.btnStart.setBackgroundColor(getColor(R.color.blue))
}
private fun refresh() { // ์คํฑ์์น ์ด๊ธฐํ
timer?.cancel() // ๋ฐฑ๊ทธ๋ผ์ด๋ ํ์ด๋จธ ๋ฉ์ถ๊ธฐ
isRunning = false
initViews()
time = 0 // ํ์ด๋จธ ์ด๊ธฐํ
}
// override fun onDestroy() {
// super.onDestroy()
// timer?.cancel()
// }
}
MainAcitivty.kt -> timer(period)๋ฅผ ์ด์ฉํ ๊ฒ์ด ์๋ Thread ํด๋์ค๋ฅผ ์ด์ฉํ์ฌ ๋ช ์์ ๊ตฌํ
class MainActivity : AppCompatActivity(), View.OnClickListener {
private var isRunning = false
private var time = 0
private lateinit var binding: ActivityMainBinding
private var timerThread: Thread? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
enableEdgeToEdge()
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
initViews()
setupListeners()
}
private fun initViews() {
// ์ด๊ธฐํ ์ฝ๋๋ ๋์ผํ๊ฒ ์ ์ง
}
private fun setupListeners() {
binding.btnStart.setOnClickListener(this)
binding.btnRefresh.setOnClickListener(this)
}
override fun onClick(v: View?) {
when (v?.id) {
R.id.btn_start -> {
if (isRunning) {
pause()
} else {
start()
}
}
R.id.btn_refresh -> {
refresh()
}
}
}
private fun start() {
binding.btnStart.text = getString(R.string.pause)
binding.btnStart.setBackgroundColor(getColor(R.color.red))
isRunning = true
timerThread = Thread {
while (isRunning) {
try {
Thread.sleep(10) // 10๋ฐ๋ฆฌ์ด๋ง๋ค ์์
์ํ
time++
val millisecond = time % 100
val second = (time % 6000) / 100
val minute = time / 6000
runOnUiThread {
binding.tvMillisecond.text = ".${millisecond.toString().padStart(2, '0')}"
binding.tvSecond.text = ":${second.toString().padStart(2, '0')}"
binding.tvMinute.text = minute.toString()
}
} catch (e: InterruptedException) {
e.printStackTrace()
}
}
}
timerThread?.start()
}
private fun pause() {
isRunning = false
binding.btnStart.text = getString(R.string.start)
binding.btnStart.setBackgroundColor(getColor(R.color.blue))
}
private fun refresh() {
isRunning = false
timerThread?.interrupt() // ์ค๋ ๋๋ฅผ ์ค์งํ๊ธฐ ์ํด interrupt ํธ์ถ
initViews()
time = 0
}
override fun onDestroy() {
super.onDestroy()
timerThread?.interrupt() // ์กํฐ๋นํฐ๊ฐ ์ข
๋ฃ๋ ๋ ์ค๋ ๋๋ฅผ ์ค์ง
}
}
๋ฐ์ํ
๐ฌ C O M M E N T