티스토리 뷰
3가지의 중점 기술
- ConstraintLayout
- CountDownTimer
- SoundPool
ConstraintLayout
chain
seekbak
<SeekBar android:layout_width="0dp" android:layout_height="wrap_content" android:max="60" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/remainSecondTextView" />
android:max="60" → 최대 60까지 지정할 수 있도록 함.
MainActivity.kt
setOnSeekBarChangeListener
는 3개의 인스턴스 메소드인
- onProgressChanged
- onStartTrackingTouch
- onStopTrackingTouch
를 override하여 사용해야 함. 예로 다음과 같은 코드임.
// 각각의 아이디대로 이벤트를 해줄것임
private fun bindViews() {
seekBar.setOnSeekBarChangeListener(
// 하나의 태그에 여러개의 메소드를 활용해야함으로 object로 넘겨줌
// 이때 오브젝트에는 사용되는 태그와 사용할 이벤트를 지정한 후
// 객체 형태로 각각의 메소드를 모두 override하게 됨
object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(p0: SeekBar?, p1: Int, p2: Boolean) {
TODO("Not yet implemented")
}
override fun onStartTrackingTouch(p0: SeekBar?) {
TODO("Not yet implemented")
}
override fun onStopTrackingTouch(p0: SeekBar?) {
TODO("Not yet implemented")
}
}
)
}
본 프로젝트에서는 아래와 같이 사용함
private fun bindViews() {
seekBar.setOnSeekBarChangeListener(
object : SeekBar.OnSeekBarChangeListener {
@SuppressLint("SetTextI18n")
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
// Long 타입이니까 잘 파악해줘야함.
if (fromUser) {
updateRemainTime(progress * 60 * 1000L)
}
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {
// 이 곳이 사용자가 새롭게 지정한 카운트 다운이 update 되는 것임
// 여기서 이제 새롭게 카운트 다운을 진행하는 순간에 null값이 아닐텐데
// null 값이 아니면 취소시키면 됨
currentCountDownTimer?.cancel()
currentCountDownTimer = null
}
override fun onStopTrackingTouch(seekBar: SeekBar?) {
// 인자로 받은 seekBar는 nullable하기 때문에 조정해야 함.
// nullable하지 않으면 카운트하는게 의미가 없기 때문에
// (사용자가 카운트가 감소하는 것을 봐야하기 때문에)
seekBar ?: return
// 위 코드가 null인지 확인하는 코드임.
currentCountDownTimer = createCountDownTimer(seekBar.progress * 1000 * 60)
// 이때 바로 아래에도 currentCountDownTimer만 입력하면 빨간줄 생김
// 왜냐하면 그 사이에 nullable 해줄 수 있기 때문에
// ?로 null 허용해주면 사라짐
currentCountDownTimer?.start()
// 이 때 tickingSoundId의 경우 null이면 안됨.
// 그래서 다음과 같이 하여 문제를 해결함
tickingSoundId?.let { soundId ->
soundPool.play(soundId, 1F, 1F, 0, -1, 1F)
}
// null 아니면 soundId로 인자를 넘겨서 실행한다는 의미를 갖고 있음
}
}
)
}
CountDownTimer
// 공식문서 왈
object : CountDownTimer(30000, 1000) {
override fun onTick(millisUntilFinished: Long) {
mTextField.setText("seconds remaining: " + millisUntilFinished / 1000)
}
override fun onFinish() {
mTextField.setText("done!")
}
}.start()
위와 같은 방식으로 CountDownTimer는 onTick
, onFinish
를 무조건 사용해야함. 오늘 실습은 아래와 같은 코드로 나옴. onTick
은 countDownInterval마다 실행, onFinish는 다 끝나면 실행
private fun createCountDownTimer(initialMillis: Int) =
object: CountDownTimer(initialMillis.toLong(), 1000L) {
// countDownInterval 마다 실행
override fun onTick(millisUntilFinished: Long) {
updateRemainTime(millisUntilFinished)
updateSeekBar(millisUntilFinished)
}
// initialMillis마다 실행
override fun onFinish() {
updateRemainTime(0)
updateSeekBar(0)
}
}
// initialMillis을 그대로 전달 받기
// 아래 2개의 함수는 출력값이 없음
// 그렇기 때문에 따로 출력 타입을 입력하지 않아도 됨.
private fun updateRemainTime(initialMillis: Long) {
val remainSeconds = initialMillis / 1000
remainMinutesTextView.text = "%02d".format(remainSeconds / 60)
remainSecondTextView.text = "%02d".format(remainSeconds % 60)
}
private fun updateSeekBar(remainMillis: Long) {
seekBar.progress = (remainMillis / 1000 / 60).toInt()
}
}
SoundPool
SoundPool로 사운드 값을 전달함
// SoundPool의 경우 바로 초기화 시킬 수 있는게 아니기 때문에 build를 해주어야 함.
// 이거로 음성 전달 하는 것임
private val soundPool = SoundPool.Builder().build()
사전에 미리 res폴더의 raw안에 timer_ticking
과 timer_bell
을 넣어줬기 때문에 이를 사용함. 이후 사운드를 사용할 method를 만들어주는데, 이는 다음과 같음
// 물론 아래의 함수를 사용하기 전에는 tickingSoundId와 bellSoundId를 선언해야 함
// null값을 허용하면 됨
// 그리고 Int형임
private fun initSound() {
// 아래꺼 저장해야함
tickingSoundId = soundPool.load(this, R.raw.timer_ticking, 1)
bellSoundId = soundPool.load(this, R.raw.timer_bell, 1)
}
아래와 같이 tickingSoundId
와 bellSoundId
를 선언함.
private var tickingSoundId: Int? = null
private var bellSoundId: Int? = null
또한 음악을 통제하기 위해서 사용하는 method는 onResume
과 onPause
가 있음. 아래와 같이 써주면 앱이 동작하지 않으면, 소리가 작동하지 않음
// 사운드가 계속 나오는 것을 방지하기 위한 코드
override fun onResume() {
super.onResume()
soundPool.autoResume() // 앱을 다시 키면 재개! (단 앱을 끄는게 아니라 홈으로 돌아왔을 때를 말하는 것임)
}
override fun onPause() {
super.onPause()
soundPool.autoPause() // 멈춰!
}
추가적으로 앱이 완전히 작동을 멈췄을 때, onDestory
를 사용하여 음원을 아에 없애야함. 용량을 많이 차지하기 때문에!!
override fun onDestory() {
super.onDestory()
soundPool.release() // 용량 삭제!
}
Design Layout
value 폴더의 colors.xml 파일에 다음과 같은 태그 파일을 추가
<color name="pomodoro_red">#FF8585</color>
해당 컬러 값을 activity_main.xml이라는 파일에 추가
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
...
android:background="@color/pomodoro_red"
...
</androidx.constraintlayout.widget.ConstraintLayout>
이제 다시 앱을 실행시키면, 흰색 화면이 뜨고 다시 빨간색 화면으로 전환
되는 것을 알 수 있음. 이유는 안드로이드의 생명주기상 onCreate가 시작될 때는 아직 View가 완성되지 않았기 때문
임. 따라서 activity_main
에 color
값을 넣어주면 안됨. 차라리 windowbackground
를 변경하는 것이 일관성 측면에서는 좋음.
windowBackground를 건드릴려면, theme.xml
파일로 가서 다음과 같은 태그를 추가함.
<item name="android:windowBackground">@color/pomodoro_red</item>
텍스트 선을 맞추고 싶을 때
지금 위의 사진을 보면 TextView의 size를 다르게 했더니, 글자의 줄이 안 맞는 것을 확인할 수 있다. 이것을 맞추게 해주려면, 초를 나타내는 TextView의 top, bottom의 constraint를 없애고 다음과 같은 코드만 추가시키자.
app:layout_constraintBaseline_toBaselineOf="@id/remainMinutesTextView"
remainMinutesTextView
라는 id를 갖는 TextView와 baseline을 같게 한다는 의미이다.
그러면 이제 이렇게 됨
추가적으로 손가락으로 tick 하는 부분 이미지를 바꿨음
<SeekBar
android:thumb="@drawable/ic_thumb"
android:tickMark="@drawable/drawable_tick_mark"/>
thumb
의 경우 아이콘을 바꾸는 거고 tickMark
의 경우 seekBar의 모양을 바꾸는 것임.
이후
<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"
tools:ignore="ContentDescription" />
ImageView에서 tools:ignore="ContentDescription"
는 그냥 추가해줍시당 그래야 오류가 사라짐
마지막으로 00:00가 되어도 소리가 안사라지는 것을 확인할 수 있는데, 이를 없애기 위해서는 onStopTrackingTouch메소드 안에 currentCountDownTimer
를 추가해주어야 한다. 근데 이건 onStartTrackingTouch와 쓰는 것이 비슷하므로 이를 추상화하여 다음과 같이 코드를 작성한다.
// onStartTrackingTouch 와 onStopTrackingTouch에 카운트 소리가 멈추는 것에 대한 추상화
private fun stopCountDown() {
// 이 곳이 사용자가 새롭게 지정한 카운트 다운이 update 되는 것임
// 여기서 이제 새롭게 카운트 다운을 진행하는 순간에 null값이 아닐텐데
// null 값이 아니면 취소시키면 됨
currentCountDownTimer?.cancel()
currentCountDownTimer = null
soundPool.autoPause()
}
끝
MainActivity의 Full Code는 다음과 같다.
class MainActivity : AppCompatActivity() {
private val remainMinutesTextView: TextView by lazy {
findViewById<TextView>(R.id.remainMinutesTextView)
}
private val remainSecondTextView: TextView by lazy {
findViewById<TextView>(R.id.remainSecondTextView)
}
private val seekBar: SeekBar by lazy {
findViewById<SeekBar>(R.id.seekBar)
}
// SoundPool의 경우 바로 초기화 시킬 수 있는게 아니기 때문에 build를 해주어야 함.
// 이거로 음성 전달 하는 것임
private val soundPool = SoundPool.Builder().build()
// 액티비트가 실행되자마자 바로 생성되는 것이 아니라, 사용자가 시간을 다시 지정하면서 되어야 하기 때문에
// 처음에는 null값을 넣어주는 것임
private var currentCountDownTimer: CountDownTimer? = null
private var tickingSoundId: Int? = null
private var bellSoundId: Int? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
bindViews()
initSound()
}
// 사운드가 계속 나오는 것을 방지하기 위한 코드
override fun onResume() {
super.onResume()
soundPool.autoResume() // 재개!
}
override fun onPause() {
super.onPause()
soundPool.autoPause() // 멈춰!
}
override fun onDestroy() {
super.onDestroy()
soundPool.release() // 파괴!
}
// 각각의 아이디대로 이벤트를 해줄것임
private fun bindViews() {
seekBar.setOnSeekBarChangeListener(
object : SeekBar.OnSeekBarChangeListener {
@SuppressLint("SetTextI18n")
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
// Long 타입이니까 잘 파악해줘야함.
if (fromUser) {
updateRemainTime(progress * 60 * 1000L)
}
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {
stopCountDown()
}
override fun onStopTrackingTouch(seekBar: SeekBar?) {
// 인자로 받은 seekBar는 nullable하기 때문에 조정해야 함.
// nullable하지 않으면 카운트하는게 의미가 없기 때문에
// (사용자가 카운트가 감소하는 것을 봐야하기 때문에)
seekBar ?: return
if (seekBar.progress == 0) {
stopCountDown()
} else {
startCountDown()
}
}
}
)
}
private fun initSound() {
// 아래꺼 저장해야함
tickingSoundId = soundPool.load(this, R.raw.timer_ticking, 1)
bellSoundId = soundPool.load(this, R.raw.timer_bell, 1)
}
// onStopTrackingTouch에 많은 로직이 있어서 가독성을 위한 추상화
private fun startCountDown() {
// 위 코드가 null인지 확인하는 코드임.
currentCountDownTimer = createCountDownTimer(seekBar.progress * 1000 * 60)
// 이때 바로 아래에도 currentCountDownTimer만 입력하면 빨간줄 생김
// 왜냐하면 그 사이에 nullable 해줄 수 있기 때문에
// ?로 null 허용해주면 사라짐
currentCountDownTimer?.start()
// 이 때 tickingSoundId의 경우 null이면 안됨.
// 그래서 다음과 같이 하여 문제를 해결함
tickingSoundId?.let { soundId ->
soundPool.play(soundId, 1F, 1F, 0, -1, 1F)
}
// null 아니면 soundId로 인자를 넘겨서 실행한다는 의미를 갖고 있음
}
// onStartTrackingTouch 와 onStopTrackingTouch에 카운트 소리가 멈추는 것에 대한 추상화
private fun stopCountDown() {
// 이 곳이 사용자가 새롭게 지정한 카운트 다운이 update 되는 것임
// 여기서 이제 새롭게 카운트 다운을 진행하는 순간에 null값이 아닐텐데
// null 값이 아니면 취소시키면 됨
currentCountDownTimer?.cancel()
currentCountDownTimer = null
soundPool.autoPause()
}
private fun createCountDownTimer(initialMillis: Int) =
object: CountDownTimer(initialMillis.toLong(), 1000L) {
// countDownInterval 마다 실행
override fun onTick(millisUntilFinished: Long) {
updateRemainTime(millisUntilFinished)
updateSeekBar(millisUntilFinished)
}
// initialMillis마다 실행
override fun onFinish() {
completeCountDown()
}
}
// initialMillis을 그대로 전달 받기
// 아래 2개의 함수는 출력값이 없음
// 그렇기 때문에 따로 출력 타입을 입력하지 않아도 됨.
private fun updateRemainTime(initialMillis: Long) {
val remainSeconds = initialMillis / 1000
remainMinutesTextView.text = "%02d'".format(remainSeconds / 60)
remainSecondTextView.text = "%02d".format(remainSeconds % 60)
}
private fun updateSeekBar(remainMillis: Long) {
seekBar.progress = (remainMillis / 1000 / 60).toInt()
}
// onFinish의 가독성이 떨어져서 가독성을 위한 추상화
private fun completeCountDown() {
updateRemainTime(0)
updateSeekBar(0)
bellSoundId?.let { soundId ->
soundPool.play(soundId, 1F, 1F, 0, 0, 1F)
}
}
}
'Android' 카테고리의 다른 글
[Android] 간단한 권한 요청 in Kotlin (0) | 2021.11.30 |
---|---|
[Android] kapt, m1 칩 org.jetbrains.kotlin.gradle.internal.KaptWithoutKotlincTask$KaptExecutionWorkAction 오류 해결 방법 (0) | 2021.11.29 |
[Android] 기초 2 in Kotlin (0) | 2021.11.18 |
[Android] 기초 1 in Kotlin (0) | 2021.11.18 |
- Total
- Today
- Yesterday
- 알고리즘
- 일반파라미터
- 보조생성자
- Kotlin
- kotlin문법
- 카카오
- 중첩클래스와 내부클래스
- Java
- Java #객체지향 #상속 #생성자 #개념 #비전공개발자 #FullStack을 #향해
- 앱개발
- 참조연산자
- 코틀린
- 백준 #숨박꼭질3 #다익스트라 #알고리즘 #비전공개발자 #풀스택 #웹개발 #앱개발 #안드로이드 #python
- 비전공싸피합격
- 추가합격후기
- 백준
- 프로젝트구조
- 안드로이드 #안드로이드스튜디오 #Kotlin #앱개발 #안드로이드기초 #비전공개발자 #풀스택개발자 #앱개발자
- 싸피5기
- Python
- 백준알고리즘 #BFS #델타이동 #알고리즘풀이 #개발 #안전영역 #풀스택개발자가되고싶습니다. #노력할래요 # 꾸준히 # 화이팅! #비전공개발자
- Programmers #알고리즘 #Python #KAKAOINTERNSHIP #비전공개발자 #불량사용자
- 안드로이드
- DP
- 비전공개발자
- 기본생성자
- Class
- 프로그래머스
- 생성자
- 구간 합 구하기 4
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | |
7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 |