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
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.runnect.runnect.presentation.countdown

import androidx.compose.ui.test.assertContentDescriptionEquals
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.test.platform.app.InstrumentationRegistry
import com.runnect.runnect.R
import com.runnect.runnect.presentation.ui.theme.RunnectTheme
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test

class CountDownScreenTest {

@get:Rule
val composeTestRule = createComposeRule()

@Test
fun 카운트다운_배경_숫자_안내문구가_노출된다() {
val context = InstrumentationRegistry.getInstrumentation().targetContext

composeTestRule.setContent {
RunnectTheme {
CountDownContent(count = 3)
}
}

composeTestRule.onNodeWithTag(CountDownScreenTestTags.BACKGROUND).assertIsDisplayed()
composeTestRule.onNodeWithTag(CountDownScreenTestTags.NUMBER)
.assertIsDisplayed()
.assertContentDescriptionEquals("3")
composeTestRule.onNodeWithTag(CountDownScreenTestTags.DESCRIPTION)
.assertIsDisplayed()
.assertTextEquals(context.getString(R.string.count_down_desc))
}

@Test
fun 카운트다운이_끝나면_완료_콜백이_호출된다() {
var finishedCount = 0
composeTestRule.mainClock.autoAdvance = false

composeTestRule.setContent {
RunnectTheme {
CountDownRoute(
onFinished = { finishedCount += 1 }
)
}
}

composeTestRule.waitForIdle()
repeat(3) {
composeTestRule.mainClock.advanceTimeBy(CountDownStateMachine.TICK_MILLIS)
composeTestRule.waitForIdle()
}
composeTestRule.waitUntil(timeoutMillis = 5_000L) {
finishedCount == 1
}

assertEquals(1, finishedCount)
}
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
package com.runnect.runnect.presentation.countdown

import android.content.Intent
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.view.animation.Animation
import android.view.animation.Animation.AnimationListener
import android.view.animation.AnimationUtils
import androidx.appcompat.content.res.AppCompatResources
import com.runnect.runnect.R
import com.runnect.runnect.binding.BindingActivity
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import com.runnect.runnect.data.dto.CourseData
import com.runnect.runnect.databinding.ActivityCountDownBinding
import com.runnect.runnect.presentation.run.RunActivity
import com.runnect.runnect.presentation.ui.theme.RunnectTheme
import com.runnect.runnect.util.analytics.Analytics
import com.runnect.runnect.util.analytics.EventName
import com.runnect.runnect.util.analytics.EventName.Param
import com.runnect.runnect.util.extension.getCompatibleParcelableExtra
import timber.log.Timber

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

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

val intentToRun = Intent(this, RunActivity::class.java)
val numList = arrayListOf(
AppCompatResources.getDrawable(this, R.drawable.anim_num1),
AppCompatResources.getDrawable(this, R.drawable.anim_num2)
)
val anim = AnimationUtils.loadAnimation(this, R.anim.anim_count)
setAnimationListener(anim, numList, intentToRun)
binding.ivCountDown.startAnimation(anim)
setContent {
RunnectTheme {
CountDownRoute(
onFinished = ::moveToRun
)
}
}
}

@Deprecated("Deprecated in Java")
override fun onBackPressed() {
Analytics.logEvent(
EventName.CLICK_CANCEL_COUNTDOWN,
Param.COURSE_ID to courseData?.courseId
)
finish()
overridePendingTransition(R.anim.slide_in_left, R.anim.slide_out_right)
overridePendingTransition(
com.runnect.runnect.R.anim.slide_in_left,
com.runnect.runnect.R.anim.slide_out_right
)
}

private fun setAnimationListener(
anim: Animation,
numList: ArrayList<Drawable?>,
intentToRun: Intent,
) {
var counter = COUNT_START

anim.setAnimationListener(object : AnimationListener {
override fun onAnimationStart(animation: Animation) {
}

override fun onAnimationEnd(animation: Animation) {
counter -= COUNT_DECREASE_UNIT
if (counter == COUNT_END) {
courseData?.let { courseData ->
intentToRun.apply {
putExtra(
EXTRA_COUNTDOWN_TO_RUN, courseData
)
}
}
startActivity(intentToRun)
finish()
} else {
binding.ivCountDown.post {
binding.ivCountDown.setImageDrawable(numList[counter])
binding.ivCountDown.startAnimation(animation)
}
}
}
override fun onAnimationRepeat(animation: Animation) {}
})
private fun moveToRun() {
val intentToRun = Intent(this, RunActivity::class.java)
courseData?.let { courseData ->
intentToRun.putExtra(EXTRA_COUNTDOWN_TO_RUN, courseData)
}
startActivity(intentToRun)
finish()
}

companion object {
const val COUNT_START = 2
const val COUNT_END = -1
const val COUNT_DECREASE_UNIT = 1
const val EXTRA_COUNTDOWN_TO_RUN = "CountToRunData"
const val EXTRA_COURSE_DATA = "CourseData"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package com.runnect.runnect.presentation.countdown

import androidx.annotation.DrawableRes
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.Easing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.repeatOnLifecycle
import com.runnect.runnect.R
import com.runnect.runnect.presentation.ui.theme.RunnectTheme
import com.runnect.runnect.presentation.ui.theme.White
import kotlinx.coroutines.delay
import kotlin.math.PI
import kotlin.math.cos

object CountDownScreenTestTags {
const val BACKGROUND = "count_down_background"
const val NUMBER = "count_down_number"
const val DESCRIPTION = "count_down_description"
}

object CountDownStateMachine {
const val INITIAL_COUNT = 3
private const val LAST_VISIBLE_COUNT = 1
const val TICK_MILLIS = 1_000L

fun nextCount(currentCount: Int): Int? =
if (currentCount > LAST_VISIBLE_COUNT) currentCount - 1 else null

@DrawableRes
fun numberDrawableRes(count: Int): Int = when (count) {
3 -> R.drawable.anim_num3
2 -> R.drawable.anim_num2
1 -> R.drawable.anim_num1
else -> error("Unsupported countdown number: $count")
}
}

object CountDownAnimationSpec {
const val INITIAL_SCALE = 0.4f
const val TARGET_SCALE = 1f

val AccelerateDecelerateEasing = Easing { fraction ->
(cos((fraction + 1f) * PI).toFloat() / 2f) + 0.5f
}
}

@Composable
fun CountDownRoute(
onFinished: () -> Unit,
modifier: Modifier = Modifier,
) {
var currentCount by remember { mutableIntStateOf(CountDownStateMachine.INITIAL_COUNT) }
var isFinished by remember { mutableStateOf(false) }
val lifecycle = LocalLifecycleOwner.current.lifecycle

LaunchedEffect(lifecycle) {
lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
while (!isFinished) {
delay(CountDownStateMachine.TICK_MILLIS)
val nextCount = CountDownStateMachine.nextCount(currentCount)
if (nextCount == null) {
isFinished = true
onFinished()
} else {
currentCount = nextCount
}
}
}
}

CountDownContent(
count = currentCount,
modifier = modifier
)
}

@Composable
fun CountDownContent(
count: Int,
modifier: Modifier = Modifier,
) {
val scale = remember { Animatable(CountDownAnimationSpec.INITIAL_SCALE) }

LaunchedEffect(count) {
scale.snapTo(CountDownAnimationSpec.INITIAL_SCALE)
scale.animateTo(
targetValue = CountDownAnimationSpec.TARGET_SCALE,
animationSpec = tween(
durationMillis = CountDownStateMachine.TICK_MILLIS.toInt(),
easing = CountDownAnimationSpec.AccelerateDecelerateEasing
)
)
}

Box(
modifier = modifier.fillMaxSize()
) {
Image(
painter = painterResource(R.drawable.star_background),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxSize()
.testTag(CountDownScreenTestTags.BACKGROUND)
)
Image(
painter = painterResource(CountDownStateMachine.numberDrawableRes(count)),
contentDescription = count.toString(),
modifier = Modifier
.align(Alignment.BottomCenter)
.offset(y = (-350).dp)
.scale(scale.value)
.testTag(CountDownScreenTestTags.NUMBER)
)
Text(
text = stringResource(R.string.count_down_desc),
style = RunnectTheme.textStyle.medium15,
color = White,
modifier = Modifier
.align(Alignment.BottomCenter)
.offset(y = (-280).dp)
.testTag(CountDownScreenTestTags.DESCRIPTION)
)
}
}
13 changes: 0 additions & 13 deletions app/src/main/res/anim/anim_count.xml

This file was deleted.

43 changes: 0 additions & 43 deletions app/src/main/res/layout/activity_count_down.xml

This file was deleted.

Loading
Loading