Skip to content

Commit c3812a2

Browse files
committed
CountDownActivity Compose 전환 및 테스트 추가
1 parent abeb1ca commit c3812a2

6 files changed

Lines changed: 240 additions & 111 deletions

File tree

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package com.runnect.runnect.presentation.countdown
2+
3+
import androidx.compose.ui.test.assertContentDescriptionEquals
4+
import androidx.compose.ui.test.assertIsDisplayed
5+
import androidx.compose.ui.test.junit4.createComposeRule
6+
import androidx.compose.ui.test.onNodeWithTag
7+
import androidx.compose.ui.test.onNodeWithText
8+
import com.runnect.runnect.presentation.ui.theme.RunnectTheme
9+
import org.junit.Assert.assertEquals
10+
import org.junit.Rule
11+
import org.junit.Test
12+
13+
class CountDownScreenTest {
14+
15+
@get:Rule
16+
val composeTestRule = createComposeRule()
17+
18+
@Test
19+
fun 카운트다운_배경_숫자_안내문구가_노출된다() {
20+
composeTestRule.setContent {
21+
RunnectTheme {
22+
CountDownContent(count = 3)
23+
}
24+
}
25+
26+
composeTestRule.onNodeWithTag(CountDownScreenTestTags.BACKGROUND).assertIsDisplayed()
27+
composeTestRule.onNodeWithTag(CountDownScreenTestTags.NUMBER)
28+
.assertIsDisplayed()
29+
.assertContentDescriptionEquals("3")
30+
composeTestRule.onNodeWithText("잠시 후 러닝을 시작합니다").assertIsDisplayed()
31+
}
32+
33+
@Test
34+
fun 카운트다운이_끝나면_완료_콜백이_호출된다() {
35+
var finishedCount = 0
36+
composeTestRule.mainClock.autoAdvance = false
37+
38+
composeTestRule.setContent {
39+
RunnectTheme {
40+
CountDownRoute(
41+
onFinished = { finishedCount += 1 }
42+
)
43+
}
44+
}
45+
46+
composeTestRule.waitForIdle()
47+
repeat(3) {
48+
composeTestRule.mainClock.advanceTimeBy(CountDownStateMachine.TICK_MILLIS)
49+
composeTestRule.waitForIdle()
50+
}
51+
composeTestRule.waitUntil(timeoutMillis = 5_000L) {
52+
finishedCount == 1
53+
}
54+
55+
assertEquals(1, finishedCount)
56+
}
57+
}
Lines changed: 24 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,18 @@
11
package com.runnect.runnect.presentation.countdown
22

33
import android.content.Intent
4-
import android.graphics.drawable.Drawable
54
import android.os.Bundle
6-
import android.view.animation.Animation
7-
import android.view.animation.Animation.AnimationListener
8-
import android.view.animation.AnimationUtils
9-
import androidx.appcompat.content.res.AppCompatResources
10-
import com.runnect.runnect.R
11-
import com.runnect.runnect.binding.BindingActivity
5+
import androidx.activity.compose.setContent
6+
import androidx.appcompat.app.AppCompatActivity
127
import com.runnect.runnect.data.dto.CourseData
13-
import com.runnect.runnect.databinding.ActivityCountDownBinding
148
import com.runnect.runnect.presentation.run.RunActivity
9+
import com.runnect.runnect.presentation.ui.theme.RunnectTheme
1510
import com.runnect.runnect.util.analytics.Analytics
1611
import com.runnect.runnect.util.analytics.EventName
1712
import com.runnect.runnect.util.analytics.EventName.Param
1813
import com.runnect.runnect.util.extension.getCompatibleParcelableExtra
19-
import timber.log.Timber
2014

21-
class CountDownActivity: BindingActivity<ActivityCountDownBinding>(R.layout.activity_count_down) {
15+
class CountDownActivity : AppCompatActivity() {
2216
private val courseData: CourseData? by lazy { intent.getCompatibleParcelableExtra(EXTRA_COURSE_DATA) }
2317

2418
override fun onCreate(savedInstanceState: Bundle?) {
@@ -29,64 +23,39 @@ class CountDownActivity: BindingActivity<ActivityCountDownBinding>(R.layout.acti
2923
Param.COURSE_ID to courseData?.courseId
3024
)
3125

32-
val intentToRun = Intent(this, RunActivity::class.java)
33-
val numList = arrayListOf(
34-
AppCompatResources.getDrawable(this, R.drawable.anim_num1),
35-
AppCompatResources.getDrawable(this, R.drawable.anim_num2)
36-
)
37-
val anim = AnimationUtils.loadAnimation(this, R.anim.anim_count)
38-
setAnimationListener(anim, numList, intentToRun)
39-
binding.ivCountDown.startAnimation(anim)
26+
setContent {
27+
RunnectTheme {
28+
CountDownRoute(
29+
onFinished = ::moveToRun
30+
)
31+
}
32+
}
4033
}
4134

35+
@Deprecated("Deprecated in Java")
4236
override fun onBackPressed() {
4337
Analytics.logEvent(
4438
EventName.CLICK_CANCEL_COUNTDOWN,
4539
Param.COURSE_ID to courseData?.courseId
4640
)
4741
finish()
48-
overridePendingTransition(R.anim.slide_in_left, R.anim.slide_out_right)
42+
overridePendingTransition(
43+
com.runnect.runnect.R.anim.slide_in_left,
44+
com.runnect.runnect.R.anim.slide_out_right
45+
)
4946
}
5047

51-
private fun setAnimationListener(
52-
anim: Animation,
53-
numList: ArrayList<Drawable?>,
54-
intentToRun: Intent,
55-
) {
56-
var counter = COUNT_START
57-
58-
anim.setAnimationListener(object : AnimationListener {
59-
override fun onAnimationStart(animation: Animation) {
60-
}
61-
62-
override fun onAnimationEnd(animation: Animation) {
63-
counter -= COUNT_DECREASE_UNIT
64-
if (counter == COUNT_END) {
65-
courseData?.let { courseData ->
66-
intentToRun.apply {
67-
putExtra(
68-
EXTRA_COUNTDOWN_TO_RUN, courseData
69-
)
70-
}
71-
}
72-
startActivity(intentToRun)
73-
finish()
74-
} else {
75-
binding.ivCountDown.post {
76-
binding.ivCountDown.setImageDrawable(numList[counter])
77-
binding.ivCountDown.startAnimation(animation)
78-
}
79-
}
80-
}
81-
override fun onAnimationRepeat(animation: Animation) {}
82-
})
48+
private fun moveToRun() {
49+
val intentToRun = Intent(this, RunActivity::class.java)
50+
courseData?.let { courseData ->
51+
intentToRun.putExtra(EXTRA_COUNTDOWN_TO_RUN, courseData)
52+
}
53+
startActivity(intentToRun)
54+
finish()
8355
}
8456

8557
companion object {
86-
const val COUNT_START = 2
87-
const val COUNT_END = -1
88-
const val COUNT_DECREASE_UNIT = 1
8958
const val EXTRA_COUNTDOWN_TO_RUN = "CountToRunData"
9059
const val EXTRA_COURSE_DATA = "CourseData"
9160
}
92-
}
61+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package com.runnect.runnect.presentation.countdown
2+
3+
import androidx.annotation.DrawableRes
4+
import androidx.compose.animation.core.FastOutSlowInEasing
5+
import androidx.compose.animation.core.RepeatMode
6+
import androidx.compose.animation.core.animateFloat
7+
import androidx.compose.animation.core.infiniteRepeatable
8+
import androidx.compose.animation.core.rememberInfiniteTransition
9+
import androidx.compose.animation.core.tween
10+
import androidx.compose.foundation.Image
11+
import androidx.compose.foundation.layout.Box
12+
import androidx.compose.foundation.layout.fillMaxSize
13+
import androidx.compose.foundation.layout.offset
14+
import androidx.compose.foundation.layout.size
15+
import androidx.compose.material3.Text
16+
import androidx.compose.runtime.Composable
17+
import androidx.compose.runtime.LaunchedEffect
18+
import androidx.compose.runtime.getValue
19+
import androidx.compose.runtime.mutableIntStateOf
20+
import androidx.compose.runtime.remember
21+
import androidx.compose.runtime.setValue
22+
import androidx.compose.ui.Alignment
23+
import androidx.compose.ui.Modifier
24+
import androidx.compose.ui.draw.scale
25+
import androidx.compose.ui.layout.ContentScale
26+
import androidx.compose.ui.platform.testTag
27+
import androidx.compose.ui.res.painterResource
28+
import androidx.compose.ui.res.stringResource
29+
import androidx.compose.ui.unit.dp
30+
import com.runnect.runnect.R
31+
import com.runnect.runnect.presentation.ui.theme.RunnectTheme
32+
import com.runnect.runnect.presentation.ui.theme.White
33+
import kotlinx.coroutines.delay
34+
35+
object CountDownScreenTestTags {
36+
const val BACKGROUND = "count_down_background"
37+
const val NUMBER = "count_down_number"
38+
const val DESCRIPTION = "count_down_description"
39+
}
40+
41+
object CountDownStateMachine {
42+
const val INITIAL_COUNT = 3
43+
private const val LAST_VISIBLE_COUNT = 1
44+
const val TICK_MILLIS = 1_000L
45+
46+
fun nextCount(currentCount: Int): Int? =
47+
if (currentCount > LAST_VISIBLE_COUNT) currentCount - 1 else null
48+
49+
@DrawableRes
50+
fun numberDrawableRes(count: Int): Int = when (count) {
51+
3 -> R.drawable.anim_num3
52+
2 -> R.drawable.anim_num2
53+
1 -> R.drawable.anim_num1
54+
else -> error("Unsupported countdown number: $count")
55+
}
56+
}
57+
58+
@Composable
59+
fun CountDownRoute(
60+
onFinished: () -> Unit,
61+
modifier: Modifier = Modifier,
62+
) {
63+
var currentCount by remember { mutableIntStateOf(CountDownStateMachine.INITIAL_COUNT) }
64+
65+
LaunchedEffect(currentCount) {
66+
delay(CountDownStateMachine.TICK_MILLIS)
67+
val nextCount = CountDownStateMachine.nextCount(currentCount)
68+
if (nextCount == null) {
69+
onFinished()
70+
} else {
71+
currentCount = nextCount
72+
}
73+
}
74+
75+
CountDownContent(
76+
count = currentCount,
77+
modifier = modifier
78+
)
79+
}
80+
81+
@Composable
82+
fun CountDownContent(
83+
count: Int,
84+
modifier: Modifier = Modifier,
85+
) {
86+
val scale by rememberInfiniteTransition(label = "countDownScale")
87+
.animateFloat(
88+
initialValue = 0.4f,
89+
targetValue = 1f,
90+
animationSpec = infiniteRepeatable(
91+
animation = tween(
92+
durationMillis = CountDownStateMachine.TICK_MILLIS.toInt(),
93+
easing = FastOutSlowInEasing
94+
),
95+
repeatMode = RepeatMode.Restart
96+
),
97+
label = "countDownNumberScale"
98+
)
99+
100+
Box(
101+
modifier = modifier.fillMaxSize()
102+
) {
103+
Image(
104+
painter = painterResource(R.drawable.star_background),
105+
contentDescription = null,
106+
contentScale = ContentScale.Crop,
107+
modifier = Modifier
108+
.fillMaxSize()
109+
.testTag(CountDownScreenTestTags.BACKGROUND)
110+
)
111+
Image(
112+
painter = painterResource(CountDownStateMachine.numberDrawableRes(count)),
113+
contentDescription = count.toString(),
114+
modifier = Modifier
115+
.align(Alignment.BottomCenter)
116+
.offset(y = (-350).dp)
117+
.size(width = 88.dp, height = 117.dp)
118+
.scale(scale)
119+
.testTag(CountDownScreenTestTags.NUMBER)
120+
)
121+
Text(
122+
text = stringResource(R.string.count_down_desc),
123+
style = RunnectTheme.textStyle.medium15,
124+
color = White,
125+
modifier = Modifier
126+
.align(Alignment.BottomCenter)
127+
.offset(y = (-280).dp)
128+
.testTag(CountDownScreenTestTags.DESCRIPTION)
129+
)
130+
}
131+
}

app/src/main/res/anim/anim_count.xml

Lines changed: 0 additions & 13 deletions
This file was deleted.

app/src/main/res/layout/activity_count_down.xml

Lines changed: 0 additions & 43 deletions
This file was deleted.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.runnect.runnect.presentation.countdown
2+
3+
import com.runnect.runnect.R
4+
import org.junit.Assert.assertEquals
5+
import org.junit.Assert.assertNull
6+
import org.junit.Test
7+
8+
class CountDownStateMachineTest {
9+
10+
@Test
11+
fun `카운트다운은 3에서 시작한다`() {
12+
assertEquals(3, CountDownStateMachine.INITIAL_COUNT)
13+
}
14+
15+
@Test
16+
fun `카운트다운은 3_2_1 순서로 진행되고 이후 종료된다`() {
17+
assertEquals(2, CountDownStateMachine.nextCount(3))
18+
assertEquals(1, CountDownStateMachine.nextCount(2))
19+
assertNull(CountDownStateMachine.nextCount(1))
20+
}
21+
22+
@Test
23+
fun `카운트다운 숫자에 맞는 drawable을 반환한다`() {
24+
assertEquals(R.drawable.anim_num3, CountDownStateMachine.numberDrawableRes(3))
25+
assertEquals(R.drawable.anim_num2, CountDownStateMachine.numberDrawableRes(2))
26+
assertEquals(R.drawable.anim_num1, CountDownStateMachine.numberDrawableRes(1))
27+
}
28+
}

0 commit comments

Comments
 (0)