10
02

ํ”„๋กœ๊ทธ๋ž˜์Šค ๋ฐ”๋ฅผ ์ด์šฉํ•ด์„œ ์ œํ•œ์‹œ๊ฐ„ ๋ง‰๋Œ€๋ฅผ ๋งŒ๋“ค์–ด์•ผํ–ˆ๋‹ค. ๊ฒฐ๊ณผ๋ฌผ์„ ๋จผ์ € ๋ณด๊ณ , ๊ฑฐ๊ธฐ๊นŒ์ง€ ๊ฐ€๋Š” ์—ฌ์ •์„ ์ ์–ด๋ณด๊ฒ ๋‹ค

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

"๋Œ“๊ธ€, ๊ณต๊ฐ ๋ฒ„ํŠผ ํ•œ ๋ฒˆ์”ฉ ๋ˆ„๋ฅด๊ณ  ๊ฐ€์ฃผ์‹œ๋ฉด ํฐ ํž˜์ด ๋ฉ๋‹ˆ๋‹ค"
๋ฐ˜์‘ํ˜•
COMMENT