ํ๋ก๊ทธ๋์ค ๋ฐ๋ฅผ ์ด์ฉํด์ ์ ํ์๊ฐ ๋ง๋๋ฅผ ๋ง๋ค์ด์ผํ๋ค. ๊ฒฐ๊ณผ๋ฌผ์ ๋จผ์ ๋ณด๊ณ , ๊ฑฐ๊ธฐ๊น์ง ๊ฐ๋ ์ฌ์ ์ ์ ์ด๋ณด๊ฒ ๋ค
ProgressBar ๋ง๋ค๊ธฐ
์ฐ์ ํ๋ก๊ทธ๋์ค๋ฐ์ xml์์ ์ธํ ์ ๋จผ์ ๋ณด์.
<ProgressBar
android:id="@+id/quiz_daily_progress_bar"
style="@style/CustomProgressBar"
android:layout_width="0dp"
android:layout_height="24dp"
android:layout_marginHorizontal="20dp"
android:layout_marginBottom="4dp"
android:max="1500"
android:progress="1500"
app:layout_constraintBottom_toTopOf="@id/quiz_daily_tv_time"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
max ๊ฐ์ 1500์ผ๋ก ํ๊ณ , ์ฐจ๊ฐ๋๋ ๋ชจ์ต์ ๋ณด์ฌ์ฃผ๊ธฐ ์ํด ์ด๊ธฐ progress ๊ฐ์ 1500์ผ๋ก ํ๋ค.(1500์ผ๋ก ํ ์ด์ ๋ ๋ค์ ์์ ํ๊ฒ ๋ค.)
style์ ์ปค์คํ ํด์ ๋ง๋ ๊ฑธ ๋ฃ์๋ค.
<!-- ProgressBar Style -->
<style name="CustomProgressBar" parent="android:Widget.ProgressBar.Horizontal">
<item name="android:progressDrawable">@drawable/custom_progress_bar</item>
</style>
๊ฐ๋ก ๋ง๋์ธ ProgressBar.Horizontal์ ์์๋ฐ์ ๋ง๋ค์๊ณ ํ๋ก๊ทธ๋์ค ๋ฐ์ ์ฌ์ฉ๋ drawable ํ์ผ์ ์ง์ ํ๋ค.
custom_progress_bar.xml ํ์ผ์ ๋ณด์.
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape>
<solid android:color="@color/White_01" />
<corners android:radius="5dp" />
</shape>
</item>
<item android:id="@android:id/progress">
<scale android:scaleWidth="100%">
<shape>
<corners android:radius="5dp"/>
<solid android:color="@color/Blue_01"/>
</shape>
</scale>
</item>
</layer-list>
๋ง๋ ์์ ํ๋์ ๋ฐฐ๊ฒฝ์์ ํฐ์์ผ๋ก ์ง์ ํ๊ธฐ ์ํด layer-list๋ฅผ ์ฌ์ฉํ๋ค. ์ ์ผ ์ค์ํ ๋ถ๋ถ์ธ๋ฐ, ๋ชจ์์ ์ง์ ํ๊ธฐ ์ ์ scale ํ๊ทธ๋ก ๋ฌถ๋๋ค. ๊ฒ์ ๊ฒฐ๊ณผ์์ ๋์จ ๋ธ๋ก๊ทธ๋ค์ clip์ ์ฌ์ฉํ๊ฑฐ๋, ๊ทธ๋ฅ shape๋ฅผ ์ฌ์ฉํ๋๋ฐ, ๊ทธ๋ ๊ฒ ํ๋ฉด progressbar๊ฐ max์ผ๋๋ ์ง์ ํ corners๊ฐ ์ ์๋ํ์ง๋ง max๊ฐ ์๋๋ฉด ์ค์ด๋ค๊ฑฐ๋ ๋์ด๋๋ ๋ถ๋ถ์ด ์ผ์๋ก ์๋ ค์ ๋์จ๋ค. clip์ด ํ๋ ธ๋ค๋ ๋ง์ด ์๋๋ผ ๋ด ๊ฒฝ์ฐ์ ๋ง์ง ์๋ ๋ฐฉ๋ฒ์ด์๋ค.
Clip vs Scale
ํด๋ฆฝ์ ์๋ณธ ์ด๋ฏธ์ง๊ฐ ์์ผ๋ฉด ๊ทธ๊ฑธ clip์์ ๋ฐ๋ผ ์๋ผ์ ๋ณด์ฌ์ฃผ๋ ๋ฐฉ๋ฒ์ด๊ณ , scale์ ๋น์จ์ ์ง์ผ์ ๋ณด์ฌ์ฃผ๋ ๋ฐฉ๋ฒ์ด๋ค. ๊ฒ์๊ฒฐ๊ณผ์๋ ๋ฐฐ๊ฒฝ์์ ๋ฐ๋ก ์ง์ ํด๋๊ธฐ์ clip์ผ๋ก ํด๋ ์๊ด์ด ์๋ ๊ฒฐ๊ณผ๋ง ๋์์ผ๋, ๋ด ๊ฒฝ์ฐ์๋ ์ฃผ์ด์ง ํผ๊ทธ๋ง ๋์์ธ์์ ํ๋ก๊ทธ๋์ค๋ฐ ๋ฐฐ๊ฒฝ์์ด ์์๊ธฐ ๋๋ฌธ์ clip์ ์ฌ์ฉํ๋ฉด ์๋๋ค.
๋ค์์ clip์ ์ฌ์ฉํ ์์๋ค.
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape>
<solid android:color="@color/White_01" />
<corners android:radius="5dp" />
</shape>
</item>
<item android:id="@android:id/progress">
<clip>
<shape>
<corners android:radius="5dp" />
<solid android:color="@color/Blue_01" />
</shape>
</clip>
</item>
</layer-list>
clip์ผ๋ก ๊ตฌํํ๋ฉด ์ฆ๊ฐ๋ถ๋ถ์ ๋ง๋๊ฐ ์ผ์๋ก ์๋ ค์ ๋์จ๋ค. scale๋ก ๊ตฌํํ๋ฉด ๋น์จ์ ์งํค๊ธฐ ๋๋ฌธ์ ์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์๋ค.
scale์ ํ์ฌ ๋ ๋ฒจ์ ๊ธฐ์ค์ผ๋ก ๋ค๋ฅธ ๋๋ก์ด๋ธ์ ํฌ๊ธฐ๋ฅผ ๋ณ๊ฒฝํ๋, XML์ ์ ์๋ ๋๋ก์ด๋ธ์ธ๋ฐ, ์กฐ์ ํ ์์๋ก๋ gravity, height, width ์ธ๊ฐ๋ฅผ ๋ณด๋ฉด๋๋ค.
android:scaleHeight๋ ์ธ๋ก ๋น์จ์ ์กฐ์ ํ๋ ๊ฒ์ผ๋ก, 100%๋ฉด ์ ์ฒด๋ฅผ ์๋ฏธํ๊ณ ๋น์จ์ด ์ค์ด๋ค ๋ 1:1๋ก ์ค์ด๋ค๊ฒ ๋๋ค. progress 1๋งํผ ์ค์ด๋ค๋ฉด scale๋ 1๋น์จ ๋งํผ ์ค์ด๋ค๊ฒ ๋๋ ๊ฒ์ด๋ค. android:scaleWidth ๊ฐ๋ก๋ ๋ง์ฐฌ๊ฐ์ง๋ค. 100%๋ณด๋ค ์๊ฒ ์ค์ ํ๋ฉด, progress๊ฐ 1๋งํผ ์ค์ด๋ค ๋ scale์ 1๋ณด๋ค ์๊ฒ ์ค์ด๋ค๊ฒ ๋๋ค. ๋ฐ๋๋ก ํฌ๋ฉด ๋ ๋ง์ด ์งํ๋๋ค.
์ด์ ๊ณจ์น์ํ UI๋ฅผ ํด๊ฒฐํ์ผ๋ ์ฝ๋๋ก ๋์ด๊ฐ๋ฉด ๋๋ค.
Timer vs Coroutine, withContext
๊ตฌํ ๋ชฉํ๋ 15์ด๋์ progressbar๋ฅผ ๋ชจ๋ ์ค์ด๋ค๊ฒ ํ๋ ๊ฒ์ด์๋๋ฐ, coroutine์ผ๋ก ๊ตฌํํด๋ณด๋ delay์ progress๋น์จ์ ์ ํด๋ 15์ด๊ฐ ์๋๋ผ 16.3์ด ์ ๋๋ก ๋์๋ค. ๋จผ์ ํด๋น ์ฝ๋๋ฅผ ๋ณด์.
fun ProgressBar.initProgressBar(detailText: TextView, submitIncorrect: () -> Unit): Job {
var totalProgress = 1500
detailText.text = context.getString(R.string.quiz_daily_time, totalProgress / 100)
return CoroutineScope(Dispatchers.IO).launch {
while (totalProgress > 0) {
delay(100)
totalProgress -= 10
withContext(Dispatchers.Main) {
progress = totalProgress
detailText.text = context.getString(
R.string.quiz_daily_time,
ceil(totalProgress.toDouble() / 100).toInt()
)
}
}
submitIncorrect()
}
}
delay๊ฑฐ๋ ๋ถ๋ถ์ IO์ฐ๋ ๋์์ ํ๊ณ , withContext๋ฅผ ์ด์ฉํด Main์ฐ๋ ๋๋ก UI๋ก ๋ณด๋๋ค. ์ด๋ฐ ์ค์ฐจ๊ฐ ๋ฐ์ํ๋ ์ด์ ๋ delay์ดํ์ ์ฐ์ฐํ ๋ ์๊ธฐ๋ ์๊ฐ ๋๋ฌธ์ด๋ผ๊ณ ์ถ๋ก ๋๋ค. delay๊ฐ์ 95ms ์ ๋๋ก ์ค์ด๋ 15์ด์ ๊ทผ์ฌํ๊ธดํ์ผ๋ ๋ญ๊ฐ ์ฐ์ฐํด์ ๋ค๋ฅธ ๋ฐฉ๋ฒ์ ์ฐพ๋ค ๋ฐ๊ฒฌํ ๊ฒ Timer๋ค.
Timer
์ฌ์ฉํ๊ฒ ๋ Timer๋ java.util์ ์ํ๋ค. kotlin์ ํนํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์๋ค๋ฉด ๊ต์ฒดํ ์๊ฐ์ด ์๋ค.(kotlin.concurrent.timer๊ฐ ์๋ค๋ ๊ฑด ์ธ์งํ๊ณ ์๋ค.)
import java.util.Timer
import java.util.TimerTask
Timer๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด์, TimerTask๋ก ์ ์ด๋ฅผ ํ๊ฒ ๋๋ค. Timer-TimerTask ์กฐํฉ๋ณด๋ค ์ถ์ฒํ๊ณ ์๋ ๊ฒ์ด ScheduledThreadPoolExecutor์ธ๋ฐ, ๋ฉํฐ์ค๋ ๋๋ฅผ ํ์ฉํ๊ณ TimerTask๋ฅผ ์ฌ์ฉํ์ง ์์๋ ๋๋ค๊ณ ํ๋ค. ๊ฐ๋จํ ๊ตฌํ๋ง ํ๋ฉด ๋๊ธฐ์ Timer-TimerTask๋ฅผ ์ ํํ๋ค. Timerํด๋์ค๋ TimerThread๋ฅผ ์ฌ์ฉํด ํ๋ฅผ ๋ฃ๊ณ , ์ ๊ฑฐํ๋ ๋ฐฉ์์ผ๋ก ์๋ํ๋ค.
์ด์ ์ฝ๋ฃจํด์ผ๋ก ์์ฑํ ์ฝ๋๋ฅผ ๋ค์ ๋ณด๋ฉด, 15์ด ๋์ ์ผ์ ๊ฐ๊ฒฉ(delay๋ฅผ ์ฌ์ฉํด)์ผ๋ก ๊ฐ์ด ์ค์ด๋ค๊ณ , ๊ทธ๊ฑธ textview.text๋ก ์ํ๋ฅผ ๋ฃ์ด์ค๋ค. ์ด์ ์ด ์์ ์ Timer๋ก ๋ฐ๊พธ๋ฉด ์๋์ ๊ฐ๋ค.
fun ProgressBar.initProgressBar(detailText: TextView, submitIncorrect: () -> Unit): Timer {
var totalProgress = 15000
val timer = Timer()
detailText.text = context.getString(R.string.quiz_daily_time, totalProgress / 100)
timer.scheduleAtFixedRate(object : TimerTask() {
override fun run() {
if (totalProgress > 0) {
totalProgress -= 1
progress = totalProgress/10
detailText.post {
detailText.text = context.getString(
R.string.quiz_daily_time,
ceil(totalProgress.toDouble() / 1000).toInt()
)
}
} else {
timer.cancel()
submitIncorrect()
}
}
}, 0, 1L)
return timer
}
withContext์ ํด๋นํ๋ ๋ถ๋ถ์ด detail.post{}์ด๊ณ , run์ด ์ฝ๋ฃจํด์ ๋์ํ๋ค. timer ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด์, ์ธ๋ถ ์ํธ์์ฉ์ ๋ฐ๋ผ timer๋ฅผ ํ์์ ์ง์ฐ๊ธฐ ์ํด ๋ฐํํ์ ์ผ๋ก๋ ๋๋ค. ์ง๊ธ ์ฝ๋์์ ์ฌ์ฉ๋ scheduleAtFixedRate ๋ฉ์๋๋ฅผ ๋ณด์.
public void scheduleAtFixedRate(TimerTask task, Date firstTime,
long period) {
if (period <= 0)
throw new IllegalArgumentException("Non-positive period.");
sched(task, firstTime.getTime(), period);
}
Timer๊ฐ์ฒด, ์์ ์๊ฐ, ์์์ธ Longํ์ ์ผ๋ก ๋ ๊ฐ๊ฒฉ์ ํ๋ผ๋ฏธํฐ๋ก ๋ฐ๋๋ค(๋จ์๋ ms๋ค). sched๋ period๊ฐ 0์ด๋ฉด ์ผํ์ฑ ์์ ์ ์คํํ๊ณ , ์๋๋ฉด period ์ฃผ๊ธฐ๋ก ๋ฐ๋ณต ์คํํ๋ ๋ฉ์๋๋ก, ์ฌ๊ธฐ์ sched๋ ์ฃผ๊ธฐ์ ์ํ๋ฅผ ํ์ธํ์ง์๊ธฐ ๋๋ฌธ์ ๊ฐ๋ ์ฑ์ ์ํด ๋ฐ๋ก ๋นผ๋ ๋๋์ด์๋ค.
timer.cancel()์ ๋จผ์ ๋ณด์. TimerTask์๋ cancel์ด ์๊ณ Timer์๋ cancel์ด ์๋๋ฐ, Timer๊ฐ์ฒด๋ฅผ ๋งค๋ฒ ์์ฑํด์ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ์ ์ฝ๋์์๋ task๋ฅผ cancelํ๋๊ฒ ์๋ ํ์ฌ timer๋ฅผ cancelํด์ผํ๋ค.
public void cancel() {
synchronized(queue) {
thread.newTasksMayBeScheduled = false;
queue.clear();
queue.notify(); // In case queue was already empty.
}
}
Timer์ cancel์ ํ๋ฅผ ๋น์์ ์์ ์ ์ค์งํ๋ค. ์ด์ withContext์ ๋์ํ๋ ๊ฒ์ผ๋ก ๋ณด์ด๋ post๋ฅผ ์ดํด๋ณด๊ฒ ๋ค.
detailText.post {
detailText.text = context.getString(
R.string.quiz_daily_time,
ceil(totalProgress.toDouble() / 100).toInt()
)
}
post๋ runnable์ ํ๋ผ๋ฏธํฐ๋ก ๋ฐ๋๋ค. ์ด ๋ง์, ๋๋ค๋ก ์ฒ๋ฆฌ๊ฐ ๊ฐ๋ฅํ๋ค๋ ์๋ฏธ๋ก ์ ์ฝ๋์์ ํํํ ๊ฒ๊ณผ ๊ฐ๋ค.
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
getRunQueue().post(action);
return true;
}
message queue๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ์์ผ๋ก, ์ด runnable action์ UI Thread์์ ๋์๊ฐ๋ค.(์ฒ๋ฆฌ๋์ด ๋ง๋ค๋ฉด ANR์ ๊ฑธ๋ฆด ์ ์๋ค) ์ด๋ mAttachInfo์ ์ ๋ณด๋ฅผ ์ดํด๋ณด๋ API28 ์ดํ๋ฅผ ํ๊ฒํ ํ๋ ์ฑ์์๋ง ์ก์ธ์ค ๊ฐ๋ฅํ๋ค๊ณ ์ ํ์๋ค. UI์ฐ๋ ๋์์ ๋์๊ฐ๋ textview์ text๋ฅผ ๋ฐ๊พธ๋ ๊ฒ๊ณผ ๊ฐ์ ui์์ ์ด ๊ฐ๋ฅํ ๊ฒ์ด๋ค.
์ฐธ์กฐ:
https://developer.android.com/guide/topics/resources/drawable-resource?hl=ko#Clip
https://developer.android.com/guide/topics/resources/drawable-resource?hl=ko#Scale
"๋๊ธ, ๊ณต๊ฐ ๋ฒํผ ํ ๋ฒ์ฉ ๋๋ฅด๊ณ ๊ฐ์ฃผ์๋ฉด ํฐ ํ์ด ๋ฉ๋๋ค"