본문 바로가기

[Kotlin] 기본 스톱워치 - 'timer / runOnUiThread()'

Kotlin, 스톱워치 구현하기

timer 와 runOnUiThread()를 사용해서 스톱워치 구현

둥근모양의 버튼은 FloatingActionButton 사용

스톱워치 구현

 [ Github, 깃허브 전체소스 ]

 

posth2071/StopWatch_Kotlin

Contribute to posth2071/StopWatch_Kotlin development by creating an account on GitHub.

github.com

 

timer 

코틀린에서 기본으로 제공하는 타이머(timer) 기능

timer는 UI스레드(메인)가 아닌 백그라운드 스레드(워커 스레드)에서 동작하는 기능이므로 기본적으로 UI 조작이 불가능
백그라운드 스레드에서 UI조작을 위한 방법에는 Handler를 호출 및 runOnUiThread() 함수를 호출하는 방법 존재하는데
여기서는 runOnUiThread() 함수를 사용해서 워커스레드에서 메인스레드를 통해 UI 조작을 구현 

timer - 일정한 시간을 주기로 반복 동작을 수행할때 쓰는 기능 ( 반복주기 속성 'period')

 

timer 기본 사용방법

반복주기는 peroid 프로퍼티로 설정, 단위는 1000분의 1초 (period = 1000, 1초)

timer(preoid = 1000){
    // 1초마다 실행할 블록
    // 백그라운드로 실행되는 부분, UI조작 X
}

 timer 블록 내부는 워커스레드(백그라운드) 공간이기에 runOnUiThread() 메서드를 통해 UI조작
    : 시간이 오래걸릴 수 있는 로직은 워커스레드 공간에서 처리하고 UI조작만 전달

timer(preoid = 1000){
    // 오래 걸리는 작업 수행부분
    runOnUiThread {
    	// UI 조작 로직
    }
}

 

메서드 구현

start() 함수 - 시작

start버튼을 누르면 호출되는 메서드
타이머를 시작하고 0.01초마다 화면에 시간을 갱신하는 로직, runOnUiThread()로 화면 시간 갱신

private fun start() {
    fab_start.setImageResource(R.drawable.ic_pause_black_24dp)	// 시작버튼을 일시정지 이미지로 변경

    timerTask = kotlin.concurrent.timer(period = 10) {	// timer() 호출
	time++	// period=10, 0.01초마다 time를 1씩 증가
        val sec = time / 100	// time/100, 나눗셈의 몫 (초 부분)
        val milli = time % 100	// time%100, 나눗셈의 나머지 (밀리초 부분)

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

 

pause() 함수 - 일시정지

타이머를 일시정지하는 함수 (초기화 X)
현재 timerTask가 진행중인지 체크 한 뒤, 진행 중이라면 cancel() 메서드를 호출해 timer를 정지

안전한 호출(?.)을 통해 간단하게 timerTask 상태를 처리가능

private fun pause() {
    fab_start.setImageResource(R.drawable.ic_play)	// 일시정지 아이콘에서 start아이콘으로 변경
    timerTask?.cancel();	// 안전한 호출(?.)로 timerTask가 null이 아니면 cancel() 호출
}

 

lapTime() 함수 - 시간 기록

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

기록버튼을 클릭 시 Timer가 진행 중인 상태라면 기록 저장, 아니라면 저장 X

// 기록버튼 클릭리스너 등록
btn_lab.setOnClickListener {
    if(time!=0) lapTime()	// 시간 저장변수 time이 0이라면 함수호출하지 않음
}

기록 저장
   : TextView를 동적으로 생성해서 LinearLayout에 추가하는 방법
     apply() 함수로 TextView 선언과 동시에 초기화

private fun lapTime() {
    val lapTime = time		// 함수 호출 시 시간(time) 저장
    
    // apply() 스코프 함수로, TextView를 생성과 동시에 초기화
    val textView = TextView(this).apply {	
        setTextSize(20f)	// fontSize 20 설정
        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 세팅
    fab_start.setImageResource(R.drawable.ic_play)	// start아이콘 설정
    secText.text = "0"		// TextView 초기화
    milliText.text = "00"

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

 


전체 코드 Activity

class MainActivity : AppCompatActivity() {
    private var time = 0
    private var isRunning = false
    private var timerTask: Timer? = null
    private var index :Int = 1

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

        fab_start.setOnClickListener {
            isRunning = !isRunning
            if (isRunning) start() else pause()
        }

        fab_reset.setOnClickListener {
            reset()
        }

        btn_lab.setOnClickListener {
            if(time!=0) lapTime()
        }
    }

    private fun start() {
        fab_start.setImageResource(R.drawable.ic_pause_black_24dp)

        timerTask = kotlin.concurrent.timer(period = 10) {
            time++
            val sec = time / 100
            val milli = time % 100

            runOnUiThread {
                secText.text = "$sec"
                milliText.text = "$milli"
            }
        }
    }

    private fun pause() {
        fab_start.setImageResource(R.drawable.ic_play)
        timerTask?.cancel();
    }

    private fun reset() {
        timerTask?.cancel()

        time = 0
        isRunning = false
        fab_start.setImageResource(R.drawable.ic_play)
        secText.text = "0"
        milliText.text = "00"

        lap_Layout.removeAllViews()
        index = 1
    }

    private fun lapTime() {
        val lapTime = time
        val textView = TextView(this).apply {
            setTextSize(20f)
        }
        textView.text = "${lapTime / 100}.${lapTime % 100}"

        lap_Layout.addView(textView,0)
        index++
    }
}

Layout 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">

    <Button
        android:id="@+id/btn_lab"
        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" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab_start"
        android:layout_width="56dp"
        android:layout_height="wrap_content"
        android:backgroundTint="#5DB160"
        android:clickable="true"
        android:tint="#FFFFFF"
        app:layout_constraintBottom_toBottomOf="@+id/btn_lab"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@+id/btn_lab"
        app:layout_constraintVertical_bias="0.0"
        app:srcCompat="@drawable/ic_play" />

    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab_reset"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:backgroundTint="#D64D7C"
        android:clickable="true"
        android:tint="#FFFFFF"
        app:layout_constraintBottom_toBottomOf="@+id/fab_start"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.12"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@+id/btn_lab"
        app:layout_constraintVertical_bias="1.0"
        app:srcCompat="@drawable/ic_refresh" />

    <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/fab_start"
        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>