본문 바로가기
안드로이드 코틀린

[Kotlin][Android] 스톱워치 만들기

by teamnova 2021. 6. 7.

안녕하세요. 이번에는 코틀린을 통해 스톱워치를 구현 해보도록 하겠습니다.

 

우선 만들고자 하는 화면을 그려보도록 하겠습니다. 화면구성은 다음과 같습니다.

스톱워치를 컨트롤 할 수 있는 시작과 초기화 버튼과 각 기록을 화면에 표시하는 기록하기 버튼 그리고 각 시간을 보여주는 TextView를 준비해둡니다.

<?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=".StopWatchActivity">

    <Button
        android:id="@+id/resetBtn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:clickable="true"
        android:tint="#FFFFFF"
        android:text="초기화"
        app:layout_constraintBottom_toBottomOf="@+id/startBtn"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.12"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@+id/recordBtn"
        app:layout_constraintVertical_bias="1.0"/>

    <Button
        android:id="@+id/startBtn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="시작"
        android:clickable="true"
        android:tint="#FFFFFF"
        app:layout_constraintBottom_toBottomOf="@+id/recordBtn"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@+id/recordBtn"
        app:layout_constraintVertical_bias="0.0" />

    <Button
        android:id="@+id/recordBtn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="기록하기"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.89"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.96" />


    <TextView
        android:id="@+id/secText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:freezesText="false"
        android:text="0"
        android:textAllCaps="false"
        android:textAppearance="@style/TextAppearance.AppCompat.Large"
        android:textSize="100sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.100000024" />

    <TextView
        android:id="@+id/milliText"
        android:layout_width="91dp"
        android:layout_height="30dp"
        android:layout_marginStart="8dp"
        android:layout_marginBottom="20dp"
        android:text="TextView"
        android:textAppearance="@style/TextAppearance.AppCompat.Large"
        app:layout_constraintBottom_toBottomOf="@+id/secText"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toEndOf="@+id/secText" />

    <ScrollView
        android:id="@+id/scroll1"
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        android:layout_marginTop="8dp"
        android:layout_marginBottom="8dp"
        app:layout_constraintBottom_toTopOf="@+id/startBtn"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/secText">

        <LinearLayout
            android:id="@+id/lap_Layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center_horizontal"
            android:orientation="vertical" />
    </ScrollView>


</androidx.constraintlayout.widget.ConstraintLayout>

예제에서 사용할 레이아웃 화면

 

이제 이를 어떻게 구현하는지 알아보도록 하겠습니다.

 

우선 타이머, 스톱워치 등을 구현하기 위해서는 'Thread'라는 녀석을 알아야 합니다. Thread란 프로세스(process) 내에서 실제로 작업을 수행하는 주체를 의미합니다. 즉 시간초를 세어주는 작업을 메인스레드(UI스레드)가 아닌 다른 스레드(워크 스레드, 백그라운드 스레드)로 동작시켜야 합니다. 

 

또한 여기서 생각을 하셔야 하는게 백그라운드 스레드(워커 스레드)에서 동작하는 건 UI에 직접 반영할 수 없습니다. 이를 해결하는 방법으로 Handler를 호출 및 runOnUiThread() 함수를 호출하는 방법 존재합니다.

 

자세한 내용은 개발자 사이트를 통해 알아보도록 합시다.

developer.android.com/reference/android/os/Handler

 

Handler  |  Android 개발자  |  Android Developers

 

developer.android.com

developer.android.com/reference/android/app/Activity

 

Activity  |  Android 개발자  |  Android Developers

 

developer.android.com

 

동작하는 방식은 다음과 같습니다. 

 

start() 함수 - 시작

start버튼을 누르면 호출되는 메서드입니다.
시간초는 0.01초마다 갱신되며 방법은 runOnUiThread()로 구현하였습니다.

 private fun start() {
        startBtn.text ="중지"
        timerTask = kotlin.concurrent.timer(period = 10) { //반복주기는 peroid 프로퍼티로 설정, 단위는 1000분의 1초 (period = 1000, 1초)
            time++ // period=10으로 0.01초마다 time를 1씩 증가하게 됩니다
            val sec = time / 100 // time/100, 나눗셈의 몫 (초 부분)
            val milli = time % 100 // time%100, 나눗셈의 나머지 (밀리초 부분)

            // UI조작을 위한 메서드
            runOnUiThread {
                secText.text = "$sec"
                milliText.text = "$milli"
            }
        }
    }

 

pause() 함수 - 일시정지

타이머를 일시정지합니다
현재 timerTask가 진행중인지 체크 한 뒤, 진행 중이라면 cancel() 메서드를 호출해 timer를 정지하게 됩니다.

    private fun pause() {
        startBtn.text ="재실행"
        timerTask?.cancel();
    }

lapTime() 함수 - 시간 기록

현재 timer의 시간을 기록하는 함수입니다.
ScrollView내부에 선언한 LinearLayout(Vertical 방향)최상단으로(index 0) TextView를 추가하는 방식

기록버튼을 클릭 시 Timer가 진행 중인 상태라면 기록 저장하게 됩니다.

 private fun lapTime() {
        val lapTime = time // 함수 호출 시 시간(time) 저장

        // apply() 스코프 함수로, TextView를 생성과 동시에 초기화
        val textView = TextView(this).apply {
            setTextSize(20f) // fontSize 20 설정
        }
        textView.text = "${lapTime / 100}.${lapTime % 100}" // 출력할 시간 설정

        lap_Layout.addView(textView,0) // layout에 추가, (View, index) 추가할 위치(0 최상단 의미)
        index++ // 추가된 View의 개수를 저장하는 index 변수
    }

reset() 함수 - 초기화

Timer 기록을 초기화 하는 함수
time(시간), index(기록 개수), timerTask(타이머 객체), TextView(UI초기화), layout(추가된 기록View 모두 제거)

private fun reset() {
        timerTask?.cancel() // timerTask가 null이 아니라면 cancel() 호출

        time = 0 // 시간저장 변수 초기화
        isRunning = false // 현재 진행중인지 판별하기 위한 Boolean변수 false 세팅
        secText.text = "0" // 시간(초) 초기화
        milliText.text = "00" // 시간(밀리초) 초기화

        startBtn.text ="시작"
        lap_Layout.removeAllViews() // Layout에 추가한 기록View 모두 삭제
        index = 1
    }
class StopWatchActivity : AppCompatActivity() {
    private var time = 0
    private var isRunning = false
    private var timerTask: Timer? = null
    private var index :Int = 1
    private lateinit var secText: TextView
    private lateinit var milliText: TextView
    private lateinit var startBtn: Button
    private lateinit var resetBtn: Button
    private lateinit var recordBtn: Button
    private lateinit var lap_Layout: LinearLayout


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

        //View inflate
        secText = findViewById(R.id.secText)
        milliText = findViewById(R.id.milliText)
        startBtn = findViewById(R.id.startBtn)
        resetBtn = findViewById(R.id.resetBtn)
        recordBtn = findViewById(R.id.recordBtn)
        lap_Layout = findViewById(R.id.lap_Layout)

        //버튼 클릭 리스너
        startBtn.setOnClickListener {
            isRunning = !isRunning
            if (isRunning) start() else pause()
        }
        resetBtn.setOnClickListener {
            reset()
        }
        recordBtn.setOnClickListener {
            if(time!=0) lapTime()
        }
    }
    private fun start() {
        startBtn.text ="중지"
        timerTask = kotlin.concurrent.timer(period = 10) { //반복주기는 peroid 프로퍼티로 설정, 단위는 1000분의 1초 (period = 1000, 1초)
            time++ // period=10으로 0.01초마다 time를 1씩 증가하게 됩니다
            val sec = time / 100 // time/100, 나눗셈의 몫 (초 부분)
            val milli = time % 100 // time%100, 나눗셈의 나머지 (밀리초 부분)

            // UI조작을 위한 메서드
            runOnUiThread {
                secText.text = "$sec"
                milliText.text = "$milli"
            }
        }
    }

    private fun pause() {
        startBtn.text ="재실행"
        timerTask?.cancel();
    }

    private fun reset() {
        timerTask?.cancel() // timerTask가 null이 아니라면 cancel() 호출

        time = 0 // 시간저장 변수 초기화
        isRunning = false // 현재 진행중인지 판별하기 위한 Boolean변수 false 세팅
        secText.text = "0" // 시간(초) 초기화
        milliText.text = "00" // 시간(밀리초) 초기화

        startBtn.text ="시작"
        lap_Layout.removeAllViews() // Layout에 추가한 기록View 모두 삭제
        index = 1
    }

    private fun lapTime() {
        val lapTime = time // 함수 호출 시 시간(time) 저장

        // apply() 스코프 함수로, TextView를 생성과 동시에 초기화
        val textView = TextView(this).apply {
            setTextSize(20f) // fontSize 20 설정
        }
        textView.text = "${lapTime / 100}.${lapTime % 100}" // 출력할 시간 설정

        lap_Layout.addView(textView,0) // layout에 추가, (View, index) 추가할 위치(0 최상단 의미)
        index++ // 추가된 View의 개수를 저장하는 index 변수
    }
}

스틱코드를 이용하면 아래 코드를 언제든지 자유롭게 가져다 사용할 수 있습니다.

stickode.com/detail.html?no=2152

 

스틱코드

 

stickode.com