# 결과물 미리보기
seekBar를 드래그하여 원하는 시간을 설정하는 타이머이다. 타이머는 해당 시간부터 00분00초가 될때까지 진행된다. 타이머가 끝나면 벨소리가 울린다. 시간이 가는 중에는 째깍소리가 난다. 홈키를 눌렀을때 즉 앱이 백그라운드로 전환 됐을때는 소리가 멈춘다.
# 알게 된 것
- Layout - SeekBar
- soundPool을 사용하여 소리 제어하기
- SeekBar 사용하여 조작하기
- 문자열 포맷팅 - "%02d".format()
- createCountTimer
## SeekBar - Layout
SeekBar는 슬라이더 형태를 띄는 게이지바이다. 위에 그림에서 안드로이드이미지를 슬라이드하여 원하느 곳에 위치 시킬 수 있으며, 해당 위치를 progress에 값을 가지고 있다.
<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/tick_mark" // 한 틱의 이미지를 설정 함( 커스텀 가능 )
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/minuteTextView" />
위 SeekBar를 만들기 위해서 사용한 코드이다. thumb, tickMark등을 나에게 맞게 설정할 수 있다.
틱을 저렇게 drawble의 shape로 정의하여 사용할 수 있다.
## soundPool을 사용하여 소리 제어
동시에 여러 소리(효과음)을 제어 할 수 있다. 먼저 res -> raw(리소스 폴더 추가) -> 사용할 파일 추가를 해준다.
그리고 soundPool을 빌드해준다. 전역변수에 담아준다.
private val soundPool = SoundPool.Builder().build()
soundPool을 초기화 해주는 작업을 추상화 시키는 함수를 생성한다. 해당 함수가 onCreate에서 호출되면, raw에 위치한 .mp4파일들이 불러와진다라고 생각하면 될 듯 싶다.
private fun initSoundPool() {
soundTick = soundPool.load(this, R.raw.timer_ticking, 1)
soundEnd = soundPool.load(this, R.raw.timer_bell, 1)
}
// soundPool.load(context = "this", R.raw.timer_ticking, priority = 1)
### SoundPool 조작
// 재생하기
// loop = -1 반복재생, loop = 0 한번 재생
soundTick?.let { it ->
soundPool.play(it, 1F, 1F, 0, -1, 1F)
//soundPool.play(it, leftVolume = 1F, rightVolume = 1F, priority = 0, loop = -1, rate = 1F)
}
// 모든 효과음 중지
soundPool.autoPause()
// 중지된 모든 효과음 재생
soundPool.autoResume()
// 부착된 효과음 없애기
soundPool.release()
## 문자열 포맷팅 - "%02d".format()
이렇게 포맷팅을 하지 않으면 분이나 초가 십의자리가 아닐때는 ex) 03:03이 아닌 3:3으로 나오게 된다. 0을 각각 추가해주기 위해서 위와 같이 포맷팅을 해줘야 했다.
minuteTextVIew.text = "%02d'".format(millisUntilFinished / 60 / 1000L)
secondTextVIew.text = "%02d".format(millisUntilFinished / 1000 % 60L)
% -> 명령어 사용을 의미
0 -> 빈곳을 채울 문자
2 -> 총 자리수 ( 두자리를 표현하 돼 비게되면 0으로 채운다 )
d -> 십진수로 된 정수
## SeekBar 활용하기
사실상 이번 챕터에서 핵심이 되는 부분이었다. 은근히 배우게 된 것이 많았다. 먼저 추상클래스와, 인터페이스를 어떤 개념으로 응용하고 활용되는지 알 수 있었다.
간단하게 아래와 같이 활용하여 값을 가져오거나 조절해 줄 수 있다.
// seekBar의 값을 가져옴
seekBar.progress
그러나 응용이 필요하다. 종합적으로 보면, 카운트다운이 됨에따라 seekBar의 progress를 변경해야하며, seekBar의 슬라이더를 조절함에 따라서, 시간이 설정 되고 타이머가 기능에 맞게 동작해야한다.
아래 코드는 종합적인 코드이다. 하나씩 살펴보도록 하겠다.
private fun bindViews() {
seekBar.setOnSeekBarChangeListener(
object : SeekBar.OnSeekBarChangeListener {
//유저가 UI를 건드린건지 or 시간이 지남에 따라 Progress바가 변해서인지를 구분해야함
override fun onProgressChanged(
seekBar: SeekBar?, progress: Int, fromUser: Boolean,
) {
// Progress의 변경이 감지되면 작동됨
// 프로그램적으로 일어나는지 유저의 의해서 발생했는지를 판단할 수 있다.
if (fromUser.not()) return
updateRemainTime(progress * 60L * 1000L)
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {
// 지금 실행되던 카운트다운을 종료시킴
// 실행되던 소리를 멈춤
myCounter?.cancel()
soundPool.autoPause()
}
override fun onStopTrackingTouch(seekBar: SeekBar?) {
seekBar ?: return
// 슬라이더를 설정했다는 뜻이기 때문에
// 새로운 카운터다운을 만들어주고
// 소리를 재생시킨다
if( seekBar.progress == 0 ) {
soundPool.autoPause()
} else {
myCounter = createCountTimer(seekBar?.progress * 60L * 1000L)
myCounter?.start()
soundTick?.let {
soundPool.play(it, 1F, 1F, 0, -1, 1F
)
}
}
}
}
)
}
### seekBar.setOnSeekBarChangeListener( )
seekBar.setOnSeekBarChangeListener()
해당 함수는 위 설명과 함께 OnSeekBarChangeListener을 인자로 가지고 있다.
OnSeekBarChangeListener는 인터페이스로서 onProgressChanged, onStartTrackingTouch, onStopTrackingTouch를 구현줘야 한다. 각각 순서대로 Progress가 바뀔때, 슬라이더가 터치 되어 움직일때, 슬라이더의 움직임이 멈칠때의 기능을 구현해줘야한다.
여기서 인터페이스가 이렇게 쓰이는구나를 알 수 있었다. 그래서 위 코드처럼 구현했다.
결국 인자로는
object : SeekBar.OnSeekBarChangeListener {
//유저가 UI를 건드린건지 or 시간이 지남에 따라 Progress바가 변해서인지를 구분해야함
override fun onProgressChanged(
seekBar: SeekBar?, progress: Int, fromUser: Boolean,
) {
if (fromUser.not()) return
updateRemainTime(progress * 60L * 1000L)
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {
myCounter?.cancel()
soundPool.autoPause()
}
override fun onStopTrackingTouch(seekBar: SeekBar?) {
seekBar ?: return
if( seekBar.progress == 0 ) {
soundPool.autoPause()
} else {
myCounter = createCountTimer(seekBar?.progress * 60L * 1000L)
myCounter?.start()
soundTick?.let {
soundPool.play(it, 1F, 1F, 0, -1, 1F
)
}
}
}
}
이렇게 object, 익명 객체를 만들어서 넣어주었는데 사실 이부분이 뭔지 이해가 안됐었는데 구글링을 하면서, 일회성으로 사용할 객체같은 경우 편하게 만들어 줄 수 있는 요소라고 했다. 즉 OnSeekBarChangeListener인터페이스를 상속받는 익명객체를 만들어 준 것이다. 아래에 참고링크를 달아주겠다. 이해하는데 상당한 도움이 됐다.
## createCountTimer
1초마다 카운트 다운이 되는 것을 구현해줘야 했다. 아마 쓰레드를 공부해보면서, Post.delayed를 통해서 구현했었던 기억이 있다. 그때는 1초마다 메시지를 전해줘 핸들러에서 처리하게 해줬었는데 CountDownTimer이라는 객체가 존재했다.
private fun createCountTimer(millis: Long): CountDownTimer {
return object : CountDownTimer(millis, 1000L) {
override fun onTick(millisUntilFinished: Long) {
Log.d(tag, "$millisUntilFinished")
updateRemainTime(millisUntilFinished)
updateProgressBar(millisUntilFinished)
}
override fun onFinish() {
updateRemainTime(0L)
updateProgressBar(0L)
myCounter?.cancel()
soundPool.autoPause()
soundEnd?.let {
soundPool.play(it, 1F, 1F, 0, 0, 1F)
}
}
}
}
여기서도 위에서 사용했던 익명객체의 개념이 등장했다. 위 함수는 타이머를 만들어주는 역할을 한다. 인자값으로 정해진 시간을 받아 1초마다 카운트다운을 하게 된다. 그렇기 때문에 CountDownTimer라는 객체를 만들어 반환을 해줘야했고, CountDownTimer을 상속받는 익명객체를 만들었다. 그리고 onTick()과 onFinsh()를 override하여 정의해줬다.
onTick의 매개변수로는 종료시까지 남은시간, onFinish는 종료시 수행할 동작을 정의해주면 된다. millisecond(밀리초)이기 때문에 1초는 1000L이다.
# 느낀점
기능에 따른 함수를 만들어줬다. 굳이 한번만 사용하게 되는 기능들을 함수로 구현하여 호출시키는지 잘 이해가 가지 않았는데 코드의 가독성과 캡슐화와 같은 기능때문에, 함수로 만들어 호출하는 방식의 코딩 스타일을 사용한다는 사실을 알게 됐다. 확실히 함수명에 따라서 보게 되니까 코드를 다시 보게 될때 가독성이 올라가는거 같아서 좋았다.
인터페이스, 추상화, 추상클래스 등등의 개념들을 조금씩 레퍼런스를 보면서 나올때마다 이해하고있는데 이렇게 쓰임에 맞게 이해하게 되니까, 체계적으로 딱딱 이해하기보다는 뭔가 어떤 흐름으로 동작하는지 알 수 있어서 좋다. 현장직에서 현장을 체험해보면서 몸소 체화하는 기분이다. 체화후 문서화 해야겠다.
시간이 된다면 위 코틀린 문법이나 개념들을 한번 쫙 정리하고 싶다.
#참고
object개념 설명