Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .idea/.name

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/AndroidProjectSystem.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/compiler.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions .idea/deploymentTargetSelector.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions .idea/deviceManager.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions .idea/migrations.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions .idea/runConfigurations.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ plugins {
}

android {
viewBinding {
enable = true
}
namespace = "com.example.android_2026_1"
compileSdk = 36

Expand Down Expand Up @@ -37,6 +40,7 @@ dependencies {
implementation(libs.material)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.recyclerview)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
Expand Down
26 changes: 26 additions & 0 deletions app/src/main/java/com/example/android_2026_1/LapAdapter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.example.android_2026_1

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView

class LapAdapter(private val lapList: List<String>) : RecyclerView.Adapter<LapAdapter.LapViewHolder>() {

class LapViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val textViewLap: TextView = itemView as TextView
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LapViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(android.R.layout.simple_list_item_1, parent, false)
return LapViewHolder(view)
}

override fun onBindViewHolder(holder: LapViewHolder, position: Int) {
holder.textViewLap.text = lapList[position]
}

override fun getItemCount(): Int = lapList.size
}
105 changes: 103 additions & 2 deletions app/src/main/java/com/example/android_2026_1/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,20 +1,121 @@
package com.example.android_2026_1

import android.annotation.SuppressLint
import android.os.Bundle
import android.os.SystemClock
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.android_2026_1.databinding.ActivityMainBinding
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch

class MainActivity : AppCompatActivity() {

private lateinit var binding: ActivityMainBinding

private var timeCentiseconds = 0
private var timeJob: Job? = null

private var startTime = 0L
private var pausedTime = 0L

private lateinit var lapAdapter: LapAdapter
private val lapList = mutableListOf<String>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_main)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->

binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

ViewCompat.setOnApplyWindowInsetsListener(binding.main) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}

lapAdapter = LapAdapter(lapList)
binding.recyclerViewLaps.apply {
adapter = lapAdapter
layoutManager = LinearLayoutManager(this@MainActivity)
}

binding.buttonStartPause.setOnClickListener {
if (timeJob?.isActive == true) pauseTimer() else startTimer()
}

binding.buttonLap.setOnClickListener {
val currentText = getFormattedTime()
val secondsElapsed = timeCentiseconds / 100
val lapRecord = getString(R.string.lap_record_format, secondsElapsed, currentText)

lapList.add(0, lapRecord)
lapAdapter.notifyItemInserted(0)
binding.recyclerViewLaps.scrollToPosition(0)
}
Comment on lines +54 to +62

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

리스트 변경은 LapAdapter에서 변경하고 mainactivity에서는 데이터를 불러만 오도록 하시는 것이 캡슐화 원칙에 더 부합해 보입니다.


binding.buttonStop.setOnClickListener {
resetTimer()
clearLapRecords()
}
}

private fun startTimer() {
startTime = SystemClock.elapsedRealtime() - pausedTime

timeJob = lifecycleScope.launch {
binding.buttonStartPause.text = getString(R.string.btn_pause)

while (isActive) {
val elapsedMillis = SystemClock.elapsedRealtime() - startTime
timeCentiseconds = (elapsedMillis / 10).toInt()

updateTimerText()
delay(10L)
}
}
}

private fun pauseTimer() {
timeJob?.cancel()
pausedTime = SystemClock.elapsedRealtime() - startTime
binding.buttonStartPause.text = getString(R.string.btn_start)
}

private fun resetTimer() {
timeJob?.cancel()
timeCentiseconds = 0
pausedTime = 0L
updateTimerText()
binding.buttonStartPause.text = getString(R.string.btn_start)
}

private fun getFormattedTime(): String {
val m = (timeCentiseconds / 6000).toString().padStart(2, '0')
val s = ((timeCentiseconds % 6000) / 100).toString().padStart(2, '0')
val ms = (timeCentiseconds % 100).toString().padStart(2, '0')

return "$m:$s:$ms"
}

private fun updateTimerText() {
val m = (timeCentiseconds / 6000).toString().padStart(2, '0')
val s = ((timeCentiseconds % 6000) / 100).toString().padStart(2, '0')
val ms = (timeCentiseconds % 100).toString().padStart(2, '0')

binding.textViewTime.text = "$m : $s : $ms"
}
Comment on lines +100 to +114

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getFormattedTime(), updateTimerText() 함수의 return 모두 string resource에서 선언하신 time_format, time_format_simple을 사용하셔도 좋아보입니다.


@SuppressLint("NotifyDataSetChanged")
private fun clearLapRecords() {
lapList.clear()
lapAdapter.notifyDataSetChanged()
}
}
59 changes: 59 additions & 0 deletions app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,63 @@
android:layout_height="match_parent"
tools:context=".MainActivity">

<TextView
android:id="@+id/textViewTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/default_time"
android:textSize="32sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@+id/linearLayoutButtons"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.25" />

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerViewLaps"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/linearLayoutButtons" />

<LinearLayout
android:id="@+id/linearLayoutButtons"
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:padding="16dp"
app:layout_constraintBottom_toTopOf="@+id/recyclerViewLaps"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textViewTime">

<Button
android:id="@+id/buttonStartPause"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="@string/btn_start" />

<Button
android:id="@+id/buttonStop"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="@string/btn_stop" />

<Button
android:id="@+id/buttonLap"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/btn_lap" />

</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
11 changes: 10 additions & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
<resources>
<string name="app_name">android_2026_1</string>
<string name="app_name">스톱워치</string>
<string name="btn_start">Start</string>
<string name="btn_pause">Pause</string>
<string name="btn_stop">Stop</string>
<string name="btn_lap">Lap</string>
<string name="default_time">00 : 00 : 00</string>

<string name="lap_record_format">%1$d초 경과 (%2$s)</string>
<string name="time_format">%02d : %02d : %02d</string>
<string name="time_format_simple">%02d:%02d:%02d</string>
</resources>
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ junitVersion = "1.3.0"
espressoCore = "3.7.0"
appcompat = "1.7.1"
material = "1.14.0"
recyclerview = "1.4.0"

[libraries]
androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" }
Expand All @@ -19,6 +20,7 @@ androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "j
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { module = "com.google.android.material:material", version.ref = "material" }
androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" }

[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
Expand Down