- Constraint View
- SeekBar
- CountDownTimer
- ํ์ด๋จธ ์๊ฐํ
- ํ์ด๋จธ ์๋, ์ข ๋ฃ์ ์๋ฆฌ ์๋ฆผ
- ์ํ ๊ด๋ฆฌ
๊ฐ๋ฐ ๊ณผ์ (๋ ธ์ ์์ ํ์ธ)
์ฐ์ Constraint View๋ฅผ ์ฌ์ฉํด์ ๋ถ๊ณผ ์ด๋ฅผ ํ์ํ๋ค. ์ด๋ฒ ํ๋ก์ ํธ์์ ์ฒ์ ์ฌ์ฉํ ๊ฒ์ Constraint Chain Style
๊ณผ BaseLine
์ด๋ค. ๋ ๊ฐ์ง๋ฅผ ์ ์ฉํ ์ด์ ๋ ๋ถ๊ณผ ์ด๋ฅผ ํ์ํ๋ TextView๊ฐ ๋ถ์ด์๊ฒ ๊ตฌํํ๊ธฐ ์ํด์์๋ค.
์ฒซ ๋ฒ์งธ ์ฌ์ง์ Constraint Chain๊ณผ BaseLine์ ์ ์ฉํ๊ธฐ ์ ์ด๋ค. ๋ถ๊ณผ ์ด๋ฅผ ํ์ํ๋ View ์ฌ์ด์ ์ผ์ ํ ๊ฐ๊ฒฉ์ด ์๊ณ Constraint๋ฅผ ์, ํ, ์ข, ์ฐ ๋ชจ๋ ์ฃผ์๊ธฐ ๋๋ฌธ์ ์ค์์ ์์นํ๋ ๊ฒ์ ๋ณผ ์ ์๋ค. ๋ ๋ฒ์งธ ์ฌ์ง์ Constraint Chain๋ง ์ ์ฉํ์ ๋๋ค. ๋ถ์ ํ์ํ๋ TextView๋ฅผ top, bottom์ผ๋ก constraint๋ฅผ ๊ฑธ์ด๋์ ๊ทธ๋๋ก ์ค์์ ์์นํ๋ค. ์ด๋ฅผ ์ข ๋ ๋ณด๊ธฐ ์ข๊ฒ ํํํ๊ณ ์ top, bottom์ constraint๋ฅผ ์์ ๊ณ baseline์ผ๋ก ์งํํ๋ค. ์ธ ๋ฒ์งธ ์ฌ์ง์ด Constraint Chain๊ณผ BaseLine์ ์ ์ฉ ์ฌ์ง์ด๋ค.
์๋จ์ ํ ๋งํ ๊ผญ์ง ์ด๋ฏธ์ง๋ .xml์ ์ด์ฉํด์ ๋ฃ์ด์คฌ๋ค. ๊ฐ๋ก ์ธ๋ก 40dp ์ดํ์ ํฌ๊ธฐ์ด๋ฉด Vector Asset
์ ์ฌ์ฉํด๋ ๊ด์ฐฎ๋ค๊ณ ํ๋ค. ๋ ํฐ ํฌ๊ธฐ์ ๊ฒฝ์ฐ ์ฌ๋ฌ ์ฌ์ด์ฆ์ ์ด๋ฏธ์ง ์ ๋ณด๋ฅผ ๋ด๊ณ ์๋ Image Asset
์ ์ฌ์ฉํ๋๋ก ํ์.
์ดํ android:src
๋ฅผ ์ฌ์ฉํด์ ๋ถ๋ฌ์จ๋ค.
๋ค์์ ํ๋จ์ SeekBar์ด๋ค. max ์์ฑ์ ์ด์ฉํด์ ํํํ ๋ฒ์๋ฅผ ์ ํ ์ ์๋ค. ๊ตฌํํ ํ์ด๋จธ๋ 60๋ถ์ด ์ต๋์ด๋ฏ๋ก 60์ ์ง์ ํด์คฌ๋ค.
thumb ์์ฑ์ ์ง์์? ์ ๊ฐ์ ์ญํ ์ ํ๋ค.
res์ drawable ํด๋์ .xml์ ์๋กญ๊ฒ ๋ง๋ค์ด์คฌ๋ค.
tickMark๋ SeekBar์์ ๊ฐ๊ฒฉ์ ํ์ํ๋ ๋ฐฉ๋ฒ? ์ ์ ์ํ๋ ์์ฑ์ด๋ค.
์ญ์ .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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_img_tomato_stem"
app:layout_constraintBottom_toTopOf="@id/remainMinutesTextView"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/remainMinutesTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="00"
android:textColor="@color/white"
android:textSize="120sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@id/remainSecondsTextView"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/remainSecondsTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="00"
android:textColor="@color/white"
android:textSize="70sp"
android:textStyle="bold"
app:layout_constraintBaseline_toBaselineOf="@id/remainMinutesTextView"
app:layout_constraintLeft_toRightOf="@id/remainMinutesTextView"
app:layout_constraintRight_toRightOf="parent" />
<SeekBar
android:id="@+id/seekBar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:max="60"
android:thumb="@drawable/ic_thumb"
android:tickMark="@drawable/drawable_tick_mark"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/remainSecondsTextView" />
</androidx.constraintlayout.widget.ConstraintLayout>
์ฐ์ .xml์์ ์ ์ธํ seekBar์ ์ฐ๊ฒฐํด์ฃผ๊ธฐ ์ํ ๋ณ์๋ฅผ ์ ์ธํ๋ค.
private val seekBar: SeekBar by lazy{
findViewById(R.id.seekBar)
}
seekBar๋ setOnSeekBarChangeListener
๋ฅผ ์ฌ์ฉํ ์ ์๋๋ฐ ์ด๋ OnSeekBarChangeListener
๋ฅผ ์ธ์๋ก ๋ฐ๋๋ค.
private fun bindViews() {
seekBar.setOnSeekBarChangeListener(
object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
if (fromUser) {
updateRemainTimes(progress * 60 * 1000L)
}
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {
stopCountDown()
}
override fun onStopTrackingTouch(seekBar: SeekBar?) {
seekBar ?: return
if (seekBar.progress== 0) {
stopCountDown()
} else {
startCountDown()
}
}
}
)
}
OnSeekBarChangeListener์๋ 3๊ฐ์ง์ ์ํ๊ฐ ์๋๋ฐ onProgressChanged
, onStartTrackingTouch
, onStopTrackingTouch
๊ฐ ์๋ค.
- onProgressChanged : ๋๋๊ทธ๋ฅผ ํ๊ณ ์์ ๋ ์ด๋ฒคํธ ๋ฐ์
- onStartTrackingTouch : ๋๋๊ทธ ์์ํ ๋ ์ด๋ฒคํธ ๋ฐ์
- onStopTrackingTouch : ๋๋๊ทธ ๋ฉ์ท์ ๋ ์ด๋ฒคํธ ๋ฐ์
๋ฐ๋ผ์ 3๊ฐ์ง ์ํ์ ๋ฐ๋ผ ๊ฐ๊ฐ ์ด๋ฒคํธ๋ฅผ ๊ตฌํ ์ค๋ฒ๋ผ์ด๋ ํด์ค์ผํ๋ค.
์ฌ์ฉ์๊ฐ SeekBar๋ฅผ ์กฐ์ํ๊ณ ์์ ๋ ๋ฐ์ํ๋ ์ด๋ฒคํธ์ด๋ค. ์ฆ ํ์ด๋จธ๋ฅผ ์ค์ ํ๊ธฐ ์ํด ์กฐ์ํ๊ณ ์์ผ๋ฏ๋ก ๋๋๊ทธ์ ๋ฐ๋ผ ํ์ํ๋ ์ซ์๊ฐ ๋ฐ๋์ด์ผํ๋ค. onProgressChanged
๋ฉ์๋๋ฅผ ๋ณด๋ฉด Progress
๋ฅผ ๋งค๊ฐ๋ณ์๋ก ๋ฐ๋ ๊ฒ์ ๋ณผ ์ ์๋ค. ์ด๋ max๋ก ์ ํด์ค 60์ ๋ฒ์ ์ด๋ด์์ 0 ~ 60์ ๊ฐ์ ๊ฐ๋๋ค. ์ดํ milliseconds
๋จ์๋ก ๋ฐ๊ฟ์คฌ๋ค.
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
if (fromUser) {
updateRemainTimes(progress * 60 * 1000L)
}
}
updateRemainTimes
์์ ๋ถ๊ณผ ์ด๋ฅผ ์ฒ๋ฆฌํ๊ฒ ๋๋๋ฐ ์ธ์๋ก milliseconds ๋จ์๋ก ๋ฐ์์ผ๋ฏ๋ก ๊ณ์ฐ์ ์ํด ๋ค์ ์ด๋ก ๋ณ๊ฒฝํด์คฌ๋ค. ์ด๋ฅผ 60์ผ๋ก ๋๋ ๊ฐ์ ๋ถ์ด ๋๊ณ , 60์ผ๋ก ๋๋ ๋๋จธ์ง๋ ์ด๊ฐ ๋๋ค.
private fun updateRemainTimes(remainMillis: Long) {
val remainSeconds = remainMillis / 1000
remainMinutesTextView.text = "%02d'".format(remainSeconds / 60)
remainSecondsTextView.text = "%02d".format(remainSeconds % 60)
}
์ด์ ์ฌ์ฉ์๊ฐ ์๊ฐ์ ์ค์ ํ ์ ์๋๋ก ๋ง๋ค์๋ค. ๋ค์์ ์ค์ ๋ ์๊ฐ์ด 1์ด๋ง๋ค ์ค์ด๋๋ ํ์ด๋จธ๋ฅผ ๋ง๋ค์ด์ผํ๋ค.
์ฐ์ CountDownTimer
ํด๋์ค๋ฅผ ์ดํด๋ณด์.
public abstract class CountDownTimer {
public CountDownTimer(long millisInFuture, long countDownInterval) {
throw new RuntimeException("Stub!");
}
public final synchronized void cancel() {
throw new RuntimeException("Stub!");
}
public final synchronized CountDownTimer start() {
throw new RuntimeException("Stub!");
}
public abstract void onTick(long var1);
public abstract void onFinish();
}
์ด์ฒ๋ผ CountDownTimer
ํด๋์ค๋ ์ถ์ ํด๋์ค์ฌ์ abstract
๋ก ์ ์ธํ onTick
๊ณผ onFinish
๋ฅผ ๊ตฌํํด์ค์ผ ํ๋ค.
private fun createCountDownTimer(initialMillis: Long): CountDownTimer {
return object : CountDownTimer(initialMillis, 1000L) {
override fun onTick(millisUntilFinished: Long) {
updateRemainTimes(millisUntilFinished)
updateSeekBar(millisUntilFinished)
}
override fun onFinish() {
completeCountDown()
}
}
}
CountDownTimer
๋ ์๊ฐ
๊ณผ ์๊ฐ ๊ฐ๊ฒฉ
์ ์ธ์๋ก ๋ฐ๋๋ค. onTick
์ ์ง์ ํ ์๊ฐ ๊ฐ๊ฒฉ๋ง๋ค ํธ์ถ๋๊ณ ๋จ์ ์๊ฐ์ด ์ธ์๋ก ๋์ด์จ๋ค. ๊ฒฐ๊ณผ์ ์ผ๋ก millisUntilFinished
์๋ ๋จ์ ์๊ฐ์ด ์ ์ฅ๋์ด ์๊ณ ์ด๋ฅผ ํ๋ฉด์ ํ์ํด์ฃผ๋ฉด ๋๋ค. updateRemainTimes
๋ ๋จ์ ์๊ฐ์ ํ๋ฉด์ ๋ณด์ฌ์ฃผ๋ ๋ฉ์๋์ด๊ณ ,
private fun updateSeekBar(remainMillis: Long) {
seekBar.progress = (remainMillis / 1000 / 60).toInt()
}
updateSeekBar
๋ ๋จ์ ์๊ฐ์ ๋ฐ๋ผ ํ๋จ seekBar์์์ ํฌ์ธํฐ์ ์์น๋ฅผ ์ง์ ํด์ฃผ๋ ๋ฉ์๋์ด๋ค.
์ด๋ ๊ฒ onProgressChanged
์ ๊ด๋ จ๋ ๋ฉ์๋๋ ๋๋ฌ๋ค. ํ์ง๋ง ์์ง ์ฒ๋ฆฌํ์ง ์์ ๋ ๊ฐ์ ๋ฉ์๋๊ฐ ์๋ค. ๋ฐ๋ก onStartTrackingTouch
์ onEndTrackingTouch
์ด๋ค. ์ ๋ฉ์๋๋ฅผ ์ธ์ ์ฌ์ฉํ ๊น? ๊ฐ์ ์ ํด๋ณด์. ์ฐ์ onEndTrackingTouch์ ๊ฒฝ์ฐ ๋๋๊ทธ๋ฅผ ์ข
๋ฃํ ๋ ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ๋๊น ๊ฒฐ๊ตญ ์ฌ์ฉ์๊ฐ ์๊ฐ์ ์ค์ ํ๊ณ ์์ ๋ ๋ ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ๋ค. ๊ฒฐ๊ณผ์ ์ผ๋ก onEndTrackingTouch์๋ ํ์ด๋จธ๋ฅผ ์๋ํ๋ ๋ฉ์๋๊ฐ ๋ค์ด๊ฐ์ผ ํ๋ค.
๊ทธ๋ ๋ค๋ฉด onStartTrackingTouch๋? ์ฒ์์ ์๊ฐ์ ์ง์ ํ ๋ ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ๋ค. ๋ง์ฝ ํ์ด๋จธ๊ฐ ์๋ ์ค์ด์ง ์๋ค๋ฉด ๋ณ ๋ฌธ์ ๊ฐ ๋์ง ์๋๋ค. ํ์ง๋ง ํ์ด๋จธ๊ฐ ์๋ ์ค์ผ ๋ ํ๋จ์ seekBar๋ฅผ ๋ง์ง๋ค๋ฉด? ๋ด๋ถ์ ์ผ๋ก ํ์ด๋จธ๋ ์๋ํ๊ณ ์๊ณ ์ด๋ฅผ ๋ณด์ฌ์ฃผ๋ TextView ์ ๋ญ๊ฐ ์ค๋ฅ๊ฐ ์๊ธธ ๊ฒ ๊ฐ์ง ์์๊ฐ? ์ค์ ๋ก ๋๊ธฐํ ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ค. ์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด seekBar๋ฅผ ๋ง์ง๋ ๊ฒฝ์ฐ ํ์ด๋จธ๋ฅผ ์ ์ ์ ์งํ๋ ๋ฉ์๋๋ฅผ ๋ง๋ค์ด์ผํ๋ค.
์ฐ์ ํ์ด๋จธ๋ฅผ ์์ํ๋ ๋ฉ์๋๋ถํฐ ์ดํด๋ณด์. ์์์ ๋งํ๋ฏ onStartTrackingTouch
๋ ์ฌ์ฉ์๊ฐ seekBar๋ฅผ ๋๋ ์ ๋ ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ๋ ๋ฉ์๋์ด๋ค. ๋ ๊ฐ์ง ๊ฒฝ์ฐ๊ฐ ์๋ค. ์ดํ๋ฆฌ์ผ์ด์
์ ์ฒ์ ์คํํ๊ณ ์๊ฐ์ ์ค์ ํ ๋์ ์๋ ์ค์ธ ํ์ด๋จธ์ ์๊ฐ์ ๋ณ๊ฒฝํ๋ ๊ฒฝ์ฐ. ์ฒซ ๋ฒ์งธ ๊ฒฝ์ฐ์ผ ๋๋ ํฐ ๋ฌธ์ ๊ฐ ์์ง๋ง ๋ ๋ฒ์งธ ๊ฒฝ์ฐ์ผ ๋๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ์ ์๋ค. ํ์ด๋จธ๊ฐ ์๋ ์ค์ด๋ฏ๋ก ์ดํ๋ฆฌ์ผ์ด์
๋ด๋ถ์ ์ผ๋ก ์ฒ์ ์ค์ ํ ์๊ฐ์์ 1์ด์ฉ ์ค์ด๋ค๊ณ ์๋๋ฐ ์ด ๋ ์๋ก์ด ์๊ฐ์ ์ค์ ํ๋ฉด ์๊ฐ์ด ๊ฒน์ณ๋ฒ๋ฆฌ๋? ์ค๋ฅ๊ฐ ๋ฐ์ํ ๊ฒ์ด๋ค. ์ค์ ๋ก ์๋ ์ค์ ํ์ด๋จธ ์๊ฐ์ ์ฌ์ค์ ํ๋ฉด 1์ด ์ ๋ delay๊ฐ ๋ฐ์ํ๋ค. ์ด๋ฅผ ํด๊ฒฐํด์ฃผ๊ธฐ ์ํด ์๋ ์ค์ธ ํ์ด๋จธ์ ์๊ฐ์ ์ฌ์ค์ ํ๋ ๊ฒฝ์ฐ ์ ์ ํ์ด๋จธ๋ฅผ ๋ฉ์ถ๋ ๋ฉ์๋๋ฅผ ์ถ๊ฐํด์คฌ๋ค.
override fun onStartTrackingTouch(seekBar: SeekBar?) {
stopCountDown()
}
// ... ์ค๋ต
private fun stopCountDown() {
currentCountDownTimer?.cancel()
currentCountDownTimer = null
soundPool.autoPause()
}
CountDownTimer
ํด๋์ค๋ start
์ cancel
์ ๋ฉ์๋๊ฐ ๊ตฌํ๋์ด ์๋ค. ์ด๋ฅผ ์ฌ์ฉํด์ ์๋์ค์ธ ํ์ด๋จธ๋ฅผ ์ค์งํ๊ณ null์ ํตํด ๊ธฐ์กด ํ์ด๋จธ๋ฅผ ๋น์์คฌ๋ค.
์ด ๋ฉ์๋๋ ์ฌ์ฉ์๊ฐ seekBar์์ ์์ ๋ ์ ๋ ๋ฐ์ํ๋ ์ด๋ฒคํธ์ด๋ค. ๋ฐ๋ผ์ ํ์ด๋จธ๋ฅผ ์์ํด์ผํ๋ค. ๋ฐ๋ผ์ ํ์ด๋จธ๋ฅผ ์์ํ๋ ๋ฉ์๋๋ฅผ ์ถ๊ฐํด์คฌ๋ค. ํ์ง๋ง ์ฌ๊ธฐ์ ์ฃผ์ํ ์ ์ ๋ง์ฝ ์ฌ์ฉ์๊ฐ ํ์ด๋จธ๋ฅผ 00๋ถ์ผ๋ก ์ค์ ํ ๊ฒฝ์ฐ ํ์ด๋จธ๊ฐ ์์ํ๋ฉด ์๋๋ค. ๋ฐ๋ผ์ seekBar.progress == 0
์ธ ๊ฒฝ์ฐ start๊ฐ ์๋ stop์ ์คํํ๋๋ก ๊ตฌํํ๋ค.
override fun onStopTrackingTouch(seekBar: SeekBar?) {
seekBar ?: return
if (seekBar.progress == 0) {
stopCountDown()
} else {
startCountDown()
}
}
// ... ์ค๋ต
private fun startCountDown() {
currentCountDownTimer = createCountDownTimer(seekBar.progress * 60 * 1000L)
currentCountDownTimer?.start()
tickingSoundId?.let { soundPool.play(it, 1F, 1F, 0, -1, 1F) }
}
์ด๋ ๊ฒ ์์ฑํ๊ณ ๋น๋๋ฅผ ํ๋ฉด ์ข ํน์ดํ ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ค. ์ดํ์ ์คํํ๊ณ ํ๋ฒํผ์ ๋๋ฌ์ ๋๊ฐ๊ฒ ๋๋ฉด ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์คํ๋๋๋ฐ ์ด ๋๋ ๊ณ์ ํ์ด๋จธ ์๋ฆฌ๊ฐ ๋ฐ์ํ๋ ๊ฒ์ ๋ณผ ์ ์๋ค. ์ํ ๊ด๋ฆฌ๋ฅผ ํ์ง ์์์ ๋ฐ์ํ๋ ๋ฌธ์ ์๋ค. ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์คํ๋์ง ์๊ฒ ํ๋ ค๋ฉด onPause
๋ฉ์๋๋ฅผ ์ค๋ฒ๋ผ์ด๋ฉ
ํด์ค์ผ ํ๋ค.
override fun onResume() {
super.onResume()
soundPool.autoResume()
}
override fun onPause() {
super.onPause()
soundPool.autoPause()
}
override fun onDestroy() {
super.onDestroy()
soundPool.release()
}