안드로이드(Android)

스톱워치(StopWatch)_3 / [Android-Kotlin]

이망국 2023. 2. 13. 20:24
728x90
반응형

참고 교재 : Joyce의 안드로이드 앱 프로그래밍 - 홍정아

 

전체 코드가 궁금하다면 아래 링크를 참고하시길 바랍니다.

 

원본 코드 : Joyce의 안드로이드 앱 프로그래밍 - 홍정아

현재 블로그 작성 코드 :Gnow


이번에는 코틀린을 이용한 스톱워치 만들기 3편입니다.

 

4. 버튼에 이벤트 연결하기

[app] → [java]  → [com.example.stopwatch] → MainActivity.kt  의 코드 중 일부

왼쪽의 코드를 보면 import문이 있는데 이는 class 코드를 작성하다보면 자동적으로 작성됩니다.

오른쪽의 코드를 보면 12번 줄의 OnClickListener가 있는데 이는 클릭 이벤트를 처리하는 인터페이스입니다.

자세한 OnClickListener에 대한 설명은 여기를 참고하세요.

 

14줄의 isRunning을 통해 스톱워치가 현재 실행되고 있는지를 확인합니다.

 

[app] → [java]  → [com.example.stopwatch] → MainActivity.kt  의 코드 중 일부

41번 줄의 onClick 함수는 클릭 이벤트가 발생했을 때 어떤 기능을 수행할지 지정합니다.

 

[코드]_ 버튼에 이벤트 연결하기 [app] → [java]  → [com.example.stopwatch] → MainActivity.kt

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.TextView

// 클릭 이벤트 처리 인터페이스
class MainActivity : AppCompatActivity(), View.OnClickListener {

    val isRunning = false   // 실행 여부 확인용 변수 false로 초기화

     private lateinit var btn_start: Button
     private lateinit var btn_refresh: Button
     private lateinit var tv_millisecond: TextView
     private lateinit var tv_second: TextView
     private lateinit var tv_minute: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 레이아웃에서 정의한 뷰 가져오기
        btn_start = findViewById(R.id.btn_start)
        btn_refresh = findViewById(R.id.btn_refresh)
        tv_millisecond = findViewById(R.id.tv_millisecond)
        tv_second = findViewById(R.id.tv_second)
        tv_minute   = findViewById(R.id.tv_minute)

        // 버튼별 리스너 등록, 등록을해야 클릭이 가능
        btn_start.setOnClickListener(this)
        btn_refresh.setOnClickListener(this)
    }

    // 클릭 이벤트시 수행할 기능 구현
    // setOnClickListener 인터페이스는 반드시 onClick() 함수를 오버라이드해야 한다.
    override fun onClick(v: View?) {
        when(v?.id) {
            R.id.btn_start -> { // 클릭 이벤트 시 뷰id가 R.id.btn_start 이며
                if(isRunning) { // 스톱워치가 동작 중이라면
                    pause()     // 일시정지 메서드를 실행하고
                } else {        // 동작 중이 아니라면
                    start()     // 시작 메서드를 실행한다.
                }
            }
            R.id.btn_refresh -> {   // 뷰id가 R.id.btn_refresh이면
                refresh()       // 초기화 메서드를 실행한다.
            }
        }
    }

    private fun start() {

    }

    private fun pause() {

    }

    private fun refresh() {

    }
}

5. 스톱워치 시작 기능 구현하기

[app] → [java]  → [com.example.stopwatch] → MainActivity.kt  의 코드 중 일부

MainActivity.kt 에 timer, time 변수를 추가합니다.

[코드]

var isRunning = false       // 실행 여부 확인용 변수 false로 초기화
    var timer : Timer? = null   // timer 변수 추가
    var time = 0                // time 변수 추가

[app] → [java]  → [com.example.stopwatch] → MainActivity.kt  의 코드 중 일부

start() 함수를 통해 시작 버튼을 누르면 작동하게 합니다.

 

59 ~ 61번 줄

시작 버튼을 누를 경우 텍스트를 일시정지로 변경합니다.
버튼 색상은 빨강으로 변경합니다.
isRunning을 true로 바꾸면서 실행으로 변경합니다.

 

63 ~ 64번 줄

timer(period = [주기]){ } 함수 : 일정한 주기로 동작을 반복할 때 사용합니다.

66번 줄

period = 10 → 10밀리초 → 0.01초마다 time에 1을 더합니다.

 

69 ~ 71번 줄

milli_second(밀리초), second(초), minute(분) 를 계산하는 식을 지정해줍니다.

 

74 ~ 78번 줄

각 시간이 한 자리일 경우 텍스트 길이의 통일을 위해 0을 추가합니다.
ex) 0:1.8 → 0:01.08

 

[코드]

private fun start() {

        btn_start.text = "일시정지"
        btn_start.setBackgroundColor(getColor(R.color.red))
        isRunning = true

        // 스톱워치를 시작하는 로직
        timer = timer(period = 10) {

            time++  // 10밀리초 단위 타이머

            // 시간 계산
            val milli_second = time % 100
            val second = (time % 6000) / 100
            val minute = time / 6000
            
             // 밀리초
                    tv_millisecond.text = if (milli_second < 10)
                                          ".0${milli_second}" else ".${milli_second}"
                    // 초
                    tv_second.text = if (second < 10) ":0${second}" else ":${second}"
                    // 분
                    tv_minute.text = "${minute}"
         }
}

지금까지 작성한 코드를 실행해봅시다.

 

에러1

지금까지 만든 앱을 실행하게 되면 다음과 같은 에러메시지가 뜹니다.

:app:checkDebugAarMetadata

6 issues were found when checking AAR metadata:

6가지 이슈 중 1번 항목을 보면 compile 버전이 33 이상이여야 한다고 알려주고 있습니다.

 

해결 : [app] → [build.gradle] 의 compileSdk 버전을 33으로 변경, targetSdk 버전도 33으로 변경
상단의 Sync Now로 적용 → 정상적으로 빌드가 되는 모습입니다.


에러2

다시 실행 버튼을 눌러 실행하면 앱은 잘 켜집니다. 시작 버튼을 누를 시 앱이 강제 종료 됩니다.

하단의 Logcat을 통해 원인 파악이 가능할 수 있습니다. 다양한 로그가 나오는데 Error를 선택해 Error만 확인이 가능합니다.

확인 결과 Only the original thread that created a view hierarchy can touch its view라는 문구를 확인할 수 있습니다.

백그라운드 스레드에서 UI 작업을 해줬기 때문에 에러가 발생하였습니다.

[app] &rarr; [java]&nbsp; &rarr;&nbsp;[com.example.stopwatch]&nbsp;&rarr;&nbsp;MainActivity.kt &nbsp;의 코드 중 일부

해결 : 73번 줄의 runOnUiThread를 통해 UI 스레드에서 실행되도록 수정합니다.
          또한 isRunning이 ture일 경우만 UI 업데이트되게 설정합니다.
이러한 설정을 하는 이유는 사용자가 정지하는 시점과 UI 스레드에서 코드가 실행되는 시점이 다를 수 있기 때문입니다.

 이후 실행 시 오른쪽과 같이 시간이 잘 흐르는 모습을 볼 수 있습니다.

 

[코드]

package com.example.stopwatch
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.TextView
import java.util.*
import kotlin.concurrent.timer

// 클릭 이벤트 처리 인터페이스
class MainActivity : AppCompatActivity(), View.OnClickListener {

    var isRunning = false       // 실행 여부 확인용 변수 false로 초기화
    var timer : Timer? = null   // timer 변수 추가
    var time = 0                // time 변수 추가

     private lateinit var btn_start: Button
     private lateinit var btn_refresh: Button
     private lateinit var tv_millisecond: TextView
     private lateinit var tv_second: TextView
     private lateinit var tv_minute: TextView
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // 레이아웃에서 정의한 뷰 가져오기
        btn_start = findViewById(R.id.btn_start)
        btn_refresh = findViewById(R.id.btn_refresh)
        tv_millisecond = findViewById(R.id.tv_millisecond)
        tv_second = findViewById(R.id.tv_second)
        tv_minute   = findViewById(R.id.tv_minute)
        // 버튼별 리스너 등록, 등록을해야 클릭이 가능
        btn_start.setOnClickListener(this)
        btn_refresh.setOnClickListener(this)
    }
    // 클릭 이벤트시 수행할 기능 구현
    // setOnClickListener 인터페이스는 반드시 onClick() 함수를 오버라이드해야 한다.
    override fun onClick(v: View?) {
        when(v?.id) {
            R.id.btn_start -> { // 클릭 이벤트 시 뷰id가 R.id.btn_start 이며
                if(isRunning) { // 스톱워치가 동작 중이라면
                    pause()     // 일시정지 메서드를 실행하고
                } else {        // 동작 중이 아니라면
                    start()     // 시작 메서드를 실행한다.
                }
            }
            R.id.btn_refresh -> {   // 뷰id가 R.id.btn_refresh이면
                refresh()       // 초기화 메서드를 실행한다.
            }
        }
    }

    private fun start() {

        btn_start.text = "일시정지"
        btn_start.setBackgroundColor(getColor(R.color.red))
        isRunning = true

        // 스톱워치를 시작하는 로직
        timer = timer(period = 10) {

            time++  // 10밀리초 단위 타이머

            // 시간 계산
            val milli_second = time % 100
            val second = (time % 6000) / 100
            val minute = time / 6000

            runOnUiThread {     // UI 스레드 생성
                if (isRunning){ // UI 업데이트 조건 설정
                    // 밀리초
                    tv_millisecond.text = if (milli_second < 10)
                                          ".0${milli_second}" else ".${milli_second}"
                    // 초
                    tv_second.text = if (second < 10) ":0${second}" else ":${second}"
                    // 분
                    tv_minute.text = "${minute}"
                }
            };
        }
    }

    private fun pause() {
    }
    private fun refresh() {
    }
}

6. 일시정지 기능 구현하기

[app] &rarr; [java]&nbsp; &rarr;&nbsp;[com.example.stopwatch]&nbsp;&rarr;&nbsp;MainActivity.kt &nbsp;의 코드 중 일부

pause() 함수에 코드를 추가합니다.

 

89번 줄 : 일시정지 버튼을 누를 경우 시작이라는 텍스트로 바꿉니다.

 

90번 줄 : 버튼의 색상을 파란색으로 바꿉니다.

 

92번 줄 : 멈춤 상태로 전환합니다.

93번 줄 : cancel() 함수를 호출해 멈춥니다. (백그라운드 스레드의 큐를 비웁니다.)

 

[코드]_ pause()

private fun pause() {
        // 텍스트 속성 변경
        btn_start.text = "시작"
        btn_start.setBackgroundColor(getColor(R.color.blue))

        isRunning = false   // 멈춤 상태로 전환
        timer?.cancel()     // 타이머 멈추기
    }

7. 초기화 기능 구현하기

[app] &rarr; [java]&nbsp; &rarr;&nbsp;[com.example.stopwatch]&nbsp;&rarr;&nbsp;MainActivity.kt &nbsp;의 코드 중 일부

refresh() 함수에 코드를 추가합니다.

 

97줄 : cancel을 통해 멈춥니다.

99줄 : 버튼의 텍스트를 시작으로 바꿉니다.

100줄 : 버튼의 색상을 파란색으로 바꿉니다.

 

104줄 : time 함수를 0으로 초기화 합니다.

105줄 : 밀리초의 텍스트를 .00으로 바꿉니다.

106줄 : 초의 텍스트를 :00으로 바꿉니다.

107줄 : 분의 텍스트를 00으로 바꿉니다.

 

[코드]_ refresh()

private fun refresh() {
        timer?.cancel()     // 백그라운드 타이머 멈추기

        btn_start.text = "시작"
        btn_start.setBackgroundColor(getColor(R.color.blue))
        isRunning = false   // 멈춤 상태로 변경

        // 타이머 초기화
        time = 0
        tv_millisecond.text = ".00"
        tv_second.text = ":00"
        tv_minute.text = "00"
    }

8. 실행화면


[코드] 최종 MainActivity.kt

package com.example.stopwatch

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.TextView
import java.util.*
import kotlin.concurrent.timer

// 클릭 이벤트 처리 인터페이스
class MainActivity : AppCompatActivity(), View.OnClickListener {

    var isRunning = false       // 실행 여부 확인용 변수 false로 초기화
    var timer : Timer? = null   // timer 변수 추가
    var time = 0                // time 변수 추가

     private lateinit var btn_start: Button
     private lateinit var btn_refresh: Button
     private lateinit var tv_millisecond: TextView
     private lateinit var tv_second: TextView
     private lateinit var tv_minute: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 레이아웃에서 정의한 뷰 가져오기
        btn_start = findViewById(R.id.btn_start)
        btn_refresh = findViewById(R.id.btn_refresh)
        tv_millisecond = findViewById(R.id.tv_millisecond)
        tv_second = findViewById(R.id.tv_second)
        tv_minute   = findViewById(R.id.tv_minute)

        // 버튼별 리스너 등록, 등록을해야 클릭이 가능
        btn_start.setOnClickListener(this)
        btn_refresh.setOnClickListener(this)
    }

    // 클릭 이벤트시 수행할 기능 구현
    // setOnClickListener 인터페이스는 반드시 onClick() 함수를 오버라이드해야 한다.
    override fun onClick(v: View?) {
        when(v?.id) {
            R.id.btn_start -> { // 클릭 이벤트 시 뷰id가 R.id.btn_start 이며
                if(isRunning) { // 스톱워치가 동작 중이라면
                    pause()     // 일시정지 메서드를 실행하고
                } else {        // 동작 중이 아니라면
                    start()     // 시작 메서드를 실행한다.
                }
            }
            R.id.btn_refresh -> {   // 뷰id가 R.id.btn_refresh이면
                refresh()       // 초기화 메서드를 실행한다.
            }
        }
    }

    private fun start() {

        btn_start.text = "일시정지"
        btn_start.setBackgroundColor(getColor(R.color.red))
        isRunning = true

        // 스톱워치를 시작하는 로직
        timer = timer(period = 10) {

            time++  // 10밀리초 단위 타이머

            // 시간 계산
            val milli_second = time % 100
            val second = (time % 6000) / 100
            val minute = time / 6000

            runOnUiThread {     // UI 스레드 생성
                if (isRunning){ // UI 업데이트 조건 설정
                    // 밀리초
                    tv_millisecond.text = if (milli_second < 10)
                                          ".0${milli_second}" else ".${milli_second}"
                    // 초
                    tv_second.text = if (second < 10) ":0${second}" else ":${second}"
                    // 분
                    tv_minute.text = "${minute}"
                }
            };
        }
    }

    private fun pause() {
        // 텍스트 속성 변경
        btn_start.text = "시작"
        btn_start.setBackgroundColor(getColor(R.color.blue))

        isRunning = false   // 멈춤 상태로 전환
        timer?.cancel()     // 타이머 멈추기
    }

    private fun refresh() {
        timer?.cancel()     // 백그라운드 타이머 멈추기
        
        btn_start.text = "시작"
        btn_start.setBackgroundColor(getColor(R.color.blue))
        isRunning = false   // 멈춤 상태로 변경

        // 타이머 초기화
        time = 0
        tv_millisecond.text = ".00"
        tv_second.text = ":00"
        tv_minute.text = "00"
    }
}

 


※ 본 게시글은 'Joyce의 안드로이드 앱 프로그래밍 - 홍정아' 책의 내용과

Android Developers의 공식문서 내용을 참고하여 작성하였습니다.

 

 

전체 코드가 궁금하다면 아래 링크를 참고하시길 바랍니다.

 

원본 코드 : Joyce의 안드로이드 앱 프로그래밍 - 홍정아

현재 블로그 작성 코드 : Gnow

 

 

728x90
반응형