From d340d80f749173ecec5d3031689fee20c7ecab09 Mon Sep 17 00:00:00 2001 From: wjchoi96 Date: Sun, 29 Sep 2024 22:19:33 +0900 Subject: [PATCH 01/16] feat:TOP-107 wip --- .../feature/tohot/component/card/ToHotCard.kt | 21 ++--- .../ToHotAnimateTimeProgressContainer.kt | 86 ++++++++++++++----- .../feature/tohot/tohot/route/ToHotRoute.kt | 2 +- .../feature/tohot/tohot/screen/ToHotScreen.kt | 8 +- .../tohot/tohot/viewmodel/ToHotViewModel.kt | 5 ++ 5 files changed, 81 insertions(+), 41 deletions(-) diff --git a/feature/tohot/src/main/java/tht/feature/tohot/component/card/ToHotCard.kt b/feature/tohot/src/main/java/tht/feature/tohot/component/card/ToHotCard.kt index 39409f24..818220d4 100644 --- a/feature/tohot/src/main/java/tht/feature/tohot/component/card/ToHotCard.kt +++ b/feature/tohot/src/main/java/tht/feature/tohot/component/card/ToHotCard.kt @@ -29,6 +29,8 @@ import tht.feature.tohot.component.progress.ToHotHeartTimeProgressContainer import tht.feature.tohot.component.userinfo.ToHotUserInfoCard import tht.feature.tohot.model.CardTimerUiModel import tht.feature.tohot.model.ImmutableListWrapper +import kotlin.time.DurationUnit +import kotlin.time.toDuration @OptIn(ExperimentalFoundationApi::class) @Composable @@ -43,14 +45,12 @@ fun ToHotCard( introduce: String, timer: CardTimerUiModel.ToHotTimer, maxTimeSec: Int, - currentSec: Float, - destinationSec: Float, enable: Boolean, fallingAnimationEnable: Boolean = false, isHoldCard: Boolean, isShakingCard: Boolean, onFallingAnimationFinish: () -> Unit = { }, - ticChanged: (Float) -> Unit = { }, + onTimerEnd: () -> Unit = { }, userCardClick: () -> Unit = { }, onLikeClick: () -> Unit = { }, onUnLikeClick: () -> Unit = { }, @@ -97,10 +97,9 @@ fun ToHotCard( ToHotAnimateTimeProgressContainer( modifier = timerModifier, enable = enable && !isHoldCard, - maxTimeSec = maxTimeSec, - currentSec = currentSec, - ticChanged = ticChanged, - destinationSec = destinationSec + duration = maxTimeSec.toDuration(DurationUnit.SECONDS), + oneTicDuration = 1.toDuration(DurationUnit.SECONDS), + onEnd = onTimerEnd ) } @@ -172,8 +171,6 @@ private fun ToHotCardPreview() { introduce = "introduce", timer = CardTimerUiModel.ToHotTimer.Timer, maxTimeSec = 5, - currentSec = 5f, - destinationSec = 4f, enable = true, isHoldCard = false, isShakingCard = false @@ -200,8 +197,6 @@ private fun ToHotCardHoldCardPreview() { introduce = "introduce", timer = CardTimerUiModel.ToHotTimer.Timer, maxTimeSec = 5, - currentSec = 5f, - destinationSec = 4f, enable = true, isHoldCard = true, isShakingCard = false @@ -228,8 +223,6 @@ private fun ToHotHeartCardPreview() { introduce = "introduce", timer = CardTimerUiModel.ToHotTimer.Heart, maxTimeSec = 5, - currentSec = 5f, - destinationSec = 4f, enable = true, isHoldCard = false, isShakingCard = false @@ -256,8 +249,6 @@ private fun ToHotDislikeCardPreview() { introduce = "introduce", timer = CardTimerUiModel.ToHotTimer.Dislike, maxTimeSec = 5, - currentSec = 5f, - destinationSec = 4f, enable = true, isHoldCard = false, isShakingCard = false diff --git a/feature/tohot/src/main/java/tht/feature/tohot/component/progress/ToHotAnimateTimeProgressContainer.kt b/feature/tohot/src/main/java/tht/feature/tohot/component/progress/ToHotAnimateTimeProgressContainer.kt index a59468c5..ea1407dc 100644 --- a/feature/tohot/src/main/java/tht/feature/tohot/component/progress/ToHotAnimateTimeProgressContainer.kt +++ b/feature/tohot/src/main/java/tht/feature/tohot/component/progress/ToHotAnimateTimeProgressContainer.kt @@ -9,26 +9,72 @@ import androidx.compose.foundation.layout.padding 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.graphics.Color import androidx.compose.ui.res.colorResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.max +import kotlin.time.Duration +import kotlin.time.DurationUnit + +/** + * 1. 처음 init 될 때 enable 이 false 면 currentSec, destinationSec가 같아야 함 + * 2. enable 이 true가 되면 destinationSec-=1 진행 + * 3. 단, 중간에 enable이 false가 되었다가 다시 true가 될 땐 반응하면 안됨 + * + * 상위에서 처리해줘야 할 것 같기도 하네 + * Timer 모델 건드리면서 해보자 + */ +@Composable +fun ToHotAnimateTimeProgressContainer( + enable: Boolean, + duration: Duration, + onEnd: () -> Unit, + oneTicDuration: Duration, + modifier: Modifier = Modifier, +) { + val maxSec = remember(duration) { duration.toInt(DurationUnit.SECONDS) } + var currentSec by remember(maxSec) { mutableIntStateOf(maxSec) } + var destinationSec by remember { mutableStateOf(maxSec.toFloat()) } + LaunchedEffect(enable, currentSec) { + if (enable) { + destinationSec = 0f.coerceAtLeast((currentSec - 1).toFloat()) + } + } + + ToHotAnimateTimeProgressContainerInternal( + enable = enable, + modifier = modifier, + maxTimeSec = maxSec, + currentSec = currentSec.toFloat(), + destinationSec = destinationSec, + duration = oneTicDuration.toLong(DurationUnit.MILLISECONDS).toFloat(), + onTicChanged = { + val nextSec = currentSec - 1 + Log.d("cwj_debug", "onTicChanged -> $nextSec") + if (nextSec > 0) { + currentSec = nextSec + } else { + Log.d("cwj_debug", "onEnd") + onEnd() + } + } + ) +} /** * 1. maxTime, currentSec 를 받아 ProgressBar 구성 * 2. maxTime, destinationSec 로 목표 progress 산출 * 3. 애니메이션 수행 * 4. 애니메이션 수행 후 ticChanged 호출 - * - * TODO: Timer 가 tic 마다 끊기는 듯한 UI 문제 확인 - * - Release Build 테스트 - * - Recomposition 최적화 확인 */ @Composable -fun ToHotAnimateTimeProgressContainer( +private fun ToHotAnimateTimeProgressContainerInternal( modifier: Modifier = Modifier, enable: Boolean, maxTimeSec: Int, @@ -41,7 +87,7 @@ fun ToHotAnimateTimeProgressContainer( ), progressBackgroundColor: Color = colorResource(id = tht.core.ui.R.color.black_353535), duration: Float = ((currentSec - destinationSec) * 1000), - ticChanged: (Float) -> Unit = { } + onTicChanged: (Float) -> Unit = { } ) { Log.d("Timer", "cSec[$currentSec], dSec[$destinationSec]") val destinationProgress = destinationSec / maxTimeSec.toFloat() @@ -110,7 +156,7 @@ fun ToHotAnimateTimeProgressContainer( ) ) } - ticChanged((progressAnimatable.value * maxTimeSec)) + onTicChanged((progressAnimatable.value * maxTimeSec)) } } @@ -138,15 +184,15 @@ fun ToHotAnimateTimeProgressContainer( } } -@Composable -@Preview -private fun ToHotAnimateTimeProgressContainerPreview() { - ToHotAnimateTimeProgressContainer( - modifier = Modifier.padding(horizontal = 13.dp, vertical = 12.dp), - enable = true, - maxTimeSec = 5, - currentSec = 5f, - ticChanged = {}, - destinationSec = 4f - ) -} +//@Composable +//@Preview +//private fun ToHotAnimateTimeProgressContainerPreview() { +// ToHotAnimateTimeProgressContainer( +// modifier = Modifier.padding(horizontal = 13.dp, vertical = 12.dp), +// enable = true, +// maxTimeSec = 5, +// currentSec = 5f, +// ticChanged = {}, +// destinationSec = 4f +// ) +//} diff --git a/feature/tohot/src/main/java/tht/feature/tohot/tohot/route/ToHotRoute.kt b/feature/tohot/src/main/java/tht/feature/tohot/tohot/route/ToHotRoute.kt index 4de50681..b0a29408 100644 --- a/feature/tohot/src/main/java/tht/feature/tohot/tohot/route/ToHotRoute.kt +++ b/feature/tohot/src/main/java/tht/feature/tohot/tohot/route/ToHotRoute.kt @@ -224,7 +224,7 @@ internal fun ToHotRoute( topicSelectListener = toHotViewModel::topicChangeClickEvent, alarmClickListener = toHotViewModel::alarmClickEvent, pageChanged = toHotViewModel::userChangeEvent, - ticChanged = toHotViewModel::ticChangeEvent, + onTimerEnd = toHotViewModel::onTimerEnd, loadFinishListener = toHotViewModel::userCardLoadFinishEvent, onLikeClick = toHotViewModel::userHeartEvent, onUnLikeClick = toHotViewModel::userDislikeEvent, diff --git a/feature/tohot/src/main/java/tht/feature/tohot/tohot/screen/ToHotScreen.kt b/feature/tohot/src/main/java/tht/feature/tohot/tohot/screen/ToHotScreen.kt index b1b0ef08..bc4b5772 100644 --- a/feature/tohot/src/main/java/tht/feature/tohot/tohot/screen/ToHotScreen.kt +++ b/feature/tohot/src/main/java/tht/feature/tohot/tohot/screen/ToHotScreen.kt @@ -50,7 +50,7 @@ internal fun ToHotScreen( topicSelectListener: () -> Unit = { }, alarmClickListener: () -> Unit = { }, pageChanged: (Int) -> Unit, - ticChanged: (Float, Int) -> Unit, + onTimerEnd: (Int) -> Unit, onLikeClick: (Int) -> Unit = { }, onUnLikeClick: (Int) -> Unit = { }, onReportMenuClick: () -> Unit = { }, @@ -101,8 +101,6 @@ internal fun ToHotScreen( introduce = card.introduce, timer = timers.list[idx].timerType, maxTimeSec = timers.list[idx].maxSec, - currentSec = timers.list[idx].currentSec, - destinationSec = timers.list[idx].destinationSec, enable = currentUserIdx == pagerState.currentPage && timers.list[idx].startAble && cardMoveAllow, fallingAnimationEnable = idx == fallingAnimationTargetIdx, @@ -111,7 +109,7 @@ internal fun ToHotScreen( onFallingAnimationFinish = { onFallingAnimationFinish(idx) }, userCardClick = { }, onReportMenuClick = onReportMenuClick, - ticChanged = { ticChanged(it, idx) }, + onTimerEnd = { onTimerEnd(idx) }, onLikeClick = { onLikeClick(idx) }, onUnLikeClick = { onUnLikeClick(idx) }, loadFinishListener = { s, e -> loadFinishListener(idx, s, e) }, @@ -175,7 +173,7 @@ fun ToHotScreenPreview() { topicSelectListener = { }, alarmClickListener = { }, pageChanged = { }, - ticChanged = { _, _ -> }, + onTimerEnd = { }, loadFinishListener = { _, _, _ -> }, onLikeClick = { }, onUnLikeClick = { }, diff --git a/feature/tohot/src/main/java/tht/feature/tohot/tohot/viewmodel/ToHotViewModel.kt b/feature/tohot/src/main/java/tht/feature/tohot/tohot/viewmodel/ToHotViewModel.kt index f99be11f..4164ad29 100644 --- a/feature/tohot/src/main/java/tht/feature/tohot/tohot/viewmodel/ToHotViewModel.kt +++ b/feature/tohot/src/main/java/tht/feature/tohot/tohot/viewmodel/ToHotViewModel.kt @@ -519,6 +519,11 @@ class ToHotViewModel @Inject constructor( } } + fun onTimerEnd(userIdx: Int) = with(store.state.value) { + Log.d("Timer", "onTimerEnd => $userIdx => enableTimerIdx[$enableTimerIdx]") + if (userIdx != enableTimerIdx) return@with + tryScrollToNext(userIdx) + } /** * timer tic 이 변경될 때 호출 * - timer 가 0이면 다음 유저 스크롤 From 0808177cde3af3721bd99a296bd066413509d0b5 Mon Sep 17 00:00:00 2001 From: wjchoi96 Date: Sat, 28 Dec 2024 16:51:59 +0900 Subject: [PATCH 02/16] =?UTF-8?q?feat:TOP-170=20=ED=98=84=EC=9E=AC=20visib?= =?UTF-8?q?leIdx=EA=B0=80=20=EC=95=84=EB=8B=8C=20UserCard=EC=9D=98=20Timer?= =?UTF-8?q?=EA=B0=80=20=EB=8F=8C=EC=95=84=EA=B0=80=EB=8A=94=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/tht/feature/tohot/tohot/screen/ToHotScreen.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/feature/tohot/src/main/java/tht/feature/tohot/tohot/screen/ToHotScreen.kt b/feature/tohot/src/main/java/tht/feature/tohot/tohot/screen/ToHotScreen.kt index bc4b5772..aeb891a3 100644 --- a/feature/tohot/src/main/java/tht/feature/tohot/tohot/screen/ToHotScreen.kt +++ b/feature/tohot/src/main/java/tht/feature/tohot/tohot/screen/ToHotScreen.kt @@ -88,6 +88,9 @@ internal fun ToHotScreen( key = { cardList.list[it].id } ) { idx -> val card = cardList.list[idx] + val enable = idx == currentUserIdx && + currentUserIdx == pagerState.currentPage && + timers.list[idx].startAble && cardMoveAllow ToHotCard( modifier = Modifier .fillMaxSize() @@ -101,8 +104,7 @@ internal fun ToHotScreen( introduce = card.introduce, timer = timers.list[idx].timerType, maxTimeSec = timers.list[idx].maxSec, - enable = currentUserIdx == pagerState.currentPage && - timers.list[idx].startAble && cardMoveAllow, + enable = enable, fallingAnimationEnable = idx == fallingAnimationTargetIdx, isHoldCard = isHoldCard, isShakingCard = isShakingCard, From a45fbf09b5f71d2f2f1317490b296f1644c5600a Mon Sep 17 00:00:00 2001 From: wjchoi96 Date: Sat, 28 Dec 2024 19:27:53 +0900 Subject: [PATCH 03/16] =?UTF-8?q?feat:TOP-107=20UserList=EA=B0=80=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EB=90=A0=20=EB=95=8C=20?= =?UTF-8?q?IndexOutOfBoundsException=20=EB=B0=9C=EC=83=9D=20=EC=9E=84?= =?UTF-8?q?=EC=8B=9C=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/tht/feature/tohot/tohot/screen/ToHotScreen.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/feature/tohot/src/main/java/tht/feature/tohot/tohot/screen/ToHotScreen.kt b/feature/tohot/src/main/java/tht/feature/tohot/tohot/screen/ToHotScreen.kt index aeb891a3..1ab865ce 100644 --- a/feature/tohot/src/main/java/tht/feature/tohot/tohot/screen/ToHotScreen.kt +++ b/feature/tohot/src/main/java/tht/feature/tohot/tohot/screen/ToHotScreen.kt @@ -85,7 +85,15 @@ internal fun ToHotScreen( VerticalPager( userScrollEnabled = false, state = pagerState, - key = { cardList.list[it].id } + key = { + // List 가 업데이트 되기 이전에 PagerState.pageCount 블록이 업데이트 된 ListSize를 리턴해서 IndexOutOfBoundsException 발생 + // 원인 파악을 아직 하지 못해서 임시 방편 처리 + if (it in cardList.list.indices) { + cardList.list[it].id + } else { + it + } + } ) { idx -> val card = cardList.list[idx] val enable = idx == currentUserIdx && From ab249a6fea9bdefc763b1b10001df2fe333f0620 Mon Sep 17 00:00:00 2001 From: wjchoi96 Date: Sat, 28 Dec 2024 19:30:37 +0900 Subject: [PATCH 04/16] =?UTF-8?q?feat:TOP-107=20LogComposition=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../compose_ui/common/LogComposition.kt | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 core/compose-ui/src/main/java/com/example/compose_ui/common/LogComposition.kt diff --git a/core/compose-ui/src/main/java/com/example/compose_ui/common/LogComposition.kt b/core/compose-ui/src/main/java/com/example/compose_ui/common/LogComposition.kt new file mode 100644 index 00000000..e283bd4f --- /dev/null +++ b/core/compose-ui/src/main/java/com/example/compose_ui/common/LogComposition.kt @@ -0,0 +1,25 @@ +package com.example.compose_ui.common + +import android.util.Log +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.remember + +class RecompositionCounter(var value: Int) + +@Composable +inline fun LogComposition( + tag: String, + msg: String = "", +) { + DisposableEffect(Unit) { + onDispose { + Log.d(tag, "Dispose: $msg") + } + } + val recompositionCounter = remember { RecompositionCounter(0) } + SideEffect { recompositionCounter.value++ } + Log.d(tag, "Composition: $msg ${recompositionCounter.value}") +} + From 109cc71f76140922f6e767a009269750a1ec67fb Mon Sep 17 00:00:00 2001 From: wjchoi96 Date: Sun, 29 Dec 2024 14:46:49 +0900 Subject: [PATCH 05/16] =?UTF-8?q?feat:TOP-107=20tohot=20=EB=AA=A8=EB=93=88?= =?UTF-8?q?=EC=97=90=20immutable=20collection=20=EC=A2=85=EC=86=8D?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- feature/tohot/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/feature/tohot/build.gradle.kts b/feature/tohot/build.gradle.kts index 52aea010..b4d89c18 100644 --- a/feature/tohot/build.gradle.kts +++ b/feature/tohot/build.gradle.kts @@ -76,4 +76,5 @@ dependencies { implementation(libs.lottie.compose) implementation(libs.renderscript.intrinsics.replacement.toolkit) + implementation(libs.kotlin.collections.immutable) } From 47db94c96e3cffc3cbc83e121b467ad4910716aa Mon Sep 17 00:00:00 2001 From: wjchoi96 Date: Sun, 29 Dec 2024 14:51:11 +0900 Subject: [PATCH 06/16] =?UTF-8?q?feat:TOP-107=20Timer=20Progress=EA=B0=80?= =?UTF-8?q?=20tic(1=EC=B4=88)=EB=A7=88=EB=8B=A4=20=EB=9A=9D=EB=9A=9D=20?= =?UTF-8?q?=EB=81=8A=EA=B8=B0=EB=8A=94=20=EB=93=AF=ED=95=9C=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 1초마다 state를 update해줘서 다음 animation을 실행하는 방식에서 문제가 발생한것으로 추측 - maxSec, duration만 전달해서 전체 progress에 대한 animation을 한번에 수행 --- .../feature/tohot/component/card/ToHotCard.kt | 1 - .../ToHotAnimateTimeProgressContainer.kt | 64 +++++++++---------- 2 files changed, 31 insertions(+), 34 deletions(-) diff --git a/feature/tohot/src/main/java/tht/feature/tohot/component/card/ToHotCard.kt b/feature/tohot/src/main/java/tht/feature/tohot/component/card/ToHotCard.kt index 818220d4..328c6035 100644 --- a/feature/tohot/src/main/java/tht/feature/tohot/component/card/ToHotCard.kt +++ b/feature/tohot/src/main/java/tht/feature/tohot/component/card/ToHotCard.kt @@ -98,7 +98,6 @@ fun ToHotCard( modifier = timerModifier, enable = enable && !isHoldCard, duration = maxTimeSec.toDuration(DurationUnit.SECONDS), - oneTicDuration = 1.toDuration(DurationUnit.SECONDS), onEnd = onTimerEnd ) } diff --git a/feature/tohot/src/main/java/tht/feature/tohot/component/progress/ToHotAnimateTimeProgressContainer.kt b/feature/tohot/src/main/java/tht/feature/tohot/component/progress/ToHotAnimateTimeProgressContainer.kt index ea1407dc..6e7df1b1 100644 --- a/feature/tohot/src/main/java/tht/feature/tohot/component/progress/ToHotAnimateTimeProgressContainer.kt +++ b/feature/tohot/src/main/java/tht/feature/tohot/component/progress/ToHotAnimateTimeProgressContainer.kt @@ -18,7 +18,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.colorResource import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.max +import com.example.compose_ui.common.LogComposition +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlin.math.ceil import kotlin.time.Duration import kotlin.time.DurationUnit @@ -35,32 +38,18 @@ fun ToHotAnimateTimeProgressContainer( enable: Boolean, duration: Duration, onEnd: () -> Unit, - oneTicDuration: Duration, modifier: Modifier = Modifier, ) { + LogComposition("cwj_debug", "ToHotAnimateTimeProgressContainer") val maxSec = remember(duration) { duration.toInt(DurationUnit.SECONDS) } - var currentSec by remember(maxSec) { mutableIntStateOf(maxSec) } - var destinationSec by remember { mutableStateOf(maxSec.toFloat()) } - LaunchedEffect(enable, currentSec) { - if (enable) { - destinationSec = 0f.coerceAtLeast((currentSec - 1).toFloat()) - } - } ToHotAnimateTimeProgressContainerInternal( enable = enable, modifier = modifier, maxTimeSec = maxSec, - currentSec = currentSec.toFloat(), - destinationSec = destinationSec, - duration = oneTicDuration.toLong(DurationUnit.MILLISECONDS).toFloat(), + destinationSec = 0f, onTicChanged = { - val nextSec = currentSec - 1 - Log.d("cwj_debug", "onTicChanged -> $nextSec") - if (nextSec > 0) { - currentSec = nextSec - } else { - Log.d("cwj_debug", "onEnd") + if (it <= 0) { onEnd() } } @@ -78,34 +67,39 @@ private fun ToHotAnimateTimeProgressContainerInternal( modifier: Modifier = Modifier, enable: Boolean, maxTimeSec: Int, - currentSec: Float, destinationSec: Float, - progressColor: List = listOf( + progressColor: ImmutableList = persistentListOf( Color(0xFFF9CC2E), Color(0xFFF98F2E), Color(0xFFF93A2E) ), progressBackgroundColor: Color = colorResource(id = tht.core.ui.R.color.black_353535), - duration: Float = ((currentSec - destinationSec) * 1000), + duration: Float = ((maxTimeSec - destinationSec) * 1000), onTicChanged: (Float) -> Unit = { } ) { - Log.d("Timer", "cSec[$currentSec], dSec[$destinationSec]") + var currentSec by remember { mutableIntStateOf(maxTimeSec) } val destinationProgress = destinationSec / maxTimeSec.toFloat() - var color = progressColor.lastOrNull() ?: Color.Yellow - for (i in progressColor.indices) { - val value = progressColor.size - i - 1 - if (destinationProgress >= (1.0f / progressColor.size) * value) { - color = progressColor[i] - break + var color by remember(progressColor) { + mutableStateOf(progressColor.firstOrNull() ?: Color.Yellow ) + } + LaunchedEffect(currentSec) { + for (i in progressColor.indices) { + // currentSec로 하면 색상 변경이 좀 늦어져서, 1초 뒤 변경될 progress 기준으로 계산 + val currentProgress = (currentSec - 1).coerceAtLeast(0).toFloat() / maxTimeSec + val value = progressColor.size - 1 - i + if (currentProgress >= (1.0f / progressColor.size) * value) { + color = progressColor[i] + break + } } } val animateProgressColor by animateColorAsState( targetValue = color, - animationSpec = tween(durationMillis = duration.toInt()), + animationSpec = tween(durationMillis = 1000), label = "animateProgressColor" ) - val progressAnimatable = remember { Animatable((currentSec / maxTimeSec.toFloat())) } + val progressAnimatable = remember { Animatable(1f) } LaunchedEffect(key1 = destinationSec, key2 = enable) { if (enable) { if (progressAnimatable.targetValue == destinationProgress) { @@ -141,7 +135,9 @@ private fun ToHotAnimateTimeProgressContainerInternal( durationMillis = remainingDuration.toInt(), easing = LinearEasing ) - ) + ) { + currentSec = ceil((this.value * maxTimeSec)).toInt() + } } else { Log.d( "Timer", @@ -154,7 +150,9 @@ private fun ToHotAnimateTimeProgressContainerInternal( durationMillis = duration.toInt(), easing = LinearEasing ) - ) + ) { + currentSec = ceil((this.value * maxTimeSec)).toInt() + } } onTicChanged((progressAnimatable.value * maxTimeSec)) } @@ -171,7 +169,7 @@ private fun ToHotAnimateTimeProgressContainerInternal( progressColor = animateProgressColor, backgroundColor = progressBackgroundColor, progress = 1 - progressAnimatable.value, - sec = currentSec.toInt() + sec = currentSec ) ToHotTimeProgressBar( From c37d780a386b3d72fd246cd44fc5ebb2b7b6d781 Mon Sep 17 00:00:00 2001 From: wjchoi96 Date: Sun, 29 Dec 2024 14:53:59 +0900 Subject: [PATCH 07/16] =?UTF-8?q?feat:TOP-107=20timer=EC=9D=98=20=EC=8B=A4?= =?UTF-8?q?=EC=A0=9C=20duration=EC=9D=80=20maxSec+1=20=EB=A1=9C=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/progress/ToHotAnimateTimeProgressContainer.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/feature/tohot/src/main/java/tht/feature/tohot/component/progress/ToHotAnimateTimeProgressContainer.kt b/feature/tohot/src/main/java/tht/feature/tohot/component/progress/ToHotAnimateTimeProgressContainer.kt index 6e7df1b1..347d27f3 100644 --- a/feature/tohot/src/main/java/tht/feature/tohot/component/progress/ToHotAnimateTimeProgressContainer.kt +++ b/feature/tohot/src/main/java/tht/feature/tohot/component/progress/ToHotAnimateTimeProgressContainer.kt @@ -42,14 +42,16 @@ fun ToHotAnimateTimeProgressContainer( ) { LogComposition("cwj_debug", "ToHotAnimateTimeProgressContainer") val maxSec = remember(duration) { duration.toInt(DurationUnit.SECONDS) } + val destinationSec = 0f ToHotAnimateTimeProgressContainerInternal( enable = enable, modifier = modifier, maxTimeSec = maxSec, - destinationSec = 0f, + destinationSec = destinationSec, + duration = (maxSec + 1) * 1000f, // 실제 duration 은 1초 추가 onTicChanged = { - if (it <= 0) { + if (it <= destinationSec) { onEnd() } } From ba2b7325334527397a82d7b453ad80cca8847d4f Mon Sep 17 00:00:00 2001 From: wjchoi96 Date: Sun, 29 Dec 2024 15:34:45 +0900 Subject: [PATCH 08/16] =?UTF-8?q?feat:TOP-107=20Timer=20=EC=9D=BC=EC=8B=9C?= =?UTF-8?q?=20=EC=A0=95=EC=A7=80=20=ED=9B=84=20duration=EC=9D=B4=20?= =?UTF-8?q?=EA=BC=AC=EC=9D=B4=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EB=8C=80?= =?UTF-8?q?=EC=9D=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feature/tohot/component/card/ToHotCard.kt | 4 +- .../ToHotAnimateTimeProgressContainer.kt | 104 +++++------------- 2 files changed, 26 insertions(+), 82 deletions(-) diff --git a/feature/tohot/src/main/java/tht/feature/tohot/component/card/ToHotCard.kt b/feature/tohot/src/main/java/tht/feature/tohot/component/card/ToHotCard.kt index 328c6035..d9ccf6be 100644 --- a/feature/tohot/src/main/java/tht/feature/tohot/component/card/ToHotCard.kt +++ b/feature/tohot/src/main/java/tht/feature/tohot/component/card/ToHotCard.kt @@ -29,8 +29,6 @@ import tht.feature.tohot.component.progress.ToHotHeartTimeProgressContainer import tht.feature.tohot.component.userinfo.ToHotUserInfoCard import tht.feature.tohot.model.CardTimerUiModel import tht.feature.tohot.model.ImmutableListWrapper -import kotlin.time.DurationUnit -import kotlin.time.toDuration @OptIn(ExperimentalFoundationApi::class) @Composable @@ -97,7 +95,7 @@ fun ToHotCard( ToHotAnimateTimeProgressContainer( modifier = timerModifier, enable = enable && !isHoldCard, - duration = maxTimeSec.toDuration(DurationUnit.SECONDS), + durationMill = maxTimeSec * 1000L, onEnd = onTimerEnd ) } diff --git a/feature/tohot/src/main/java/tht/feature/tohot/component/progress/ToHotAnimateTimeProgressContainer.kt b/feature/tohot/src/main/java/tht/feature/tohot/component/progress/ToHotAnimateTimeProgressContainer.kt index 347d27f3..19943002 100644 --- a/feature/tohot/src/main/java/tht/feature/tohot/component/progress/ToHotAnimateTimeProgressContainer.kt +++ b/feature/tohot/src/main/java/tht/feature/tohot/component/progress/ToHotAnimateTimeProgressContainer.kt @@ -1,6 +1,5 @@ package tht.feature.tohot.component.progress -import android.util.Log import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.LinearEasing @@ -17,31 +16,22 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.colorResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.example.compose_ui.common.LogComposition import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlin.math.ceil -import kotlin.time.Duration -import kotlin.time.DurationUnit -/** - * 1. 처음 init 될 때 enable 이 false 면 currentSec, destinationSec가 같아야 함 - * 2. enable 이 true가 되면 destinationSec-=1 진행 - * 3. 단, 중간에 enable이 false가 되었다가 다시 true가 될 땐 반응하면 안됨 - * - * 상위에서 처리해줘야 할 것 같기도 하네 - * Timer 모델 건드리면서 해보자 - */ @Composable fun ToHotAnimateTimeProgressContainer( enable: Boolean, - duration: Duration, + durationMill: Long, onEnd: () -> Unit, modifier: Modifier = Modifier, ) { LogComposition("cwj_debug", "ToHotAnimateTimeProgressContainer") - val maxSec = remember(duration) { duration.toInt(DurationUnit.SECONDS) } + val maxSec = remember(durationMill) { (durationMill / 1000).toInt() } val destinationSec = 0f ToHotAnimateTimeProgressContainerInternal( @@ -59,10 +49,9 @@ fun ToHotAnimateTimeProgressContainer( } /** - * 1. maxTime, currentSec 를 받아 ProgressBar 구성 - * 2. maxTime, destinationSec 로 목표 progress 산출 - * 3. 애니메이션 수행 - * 4. 애니메이션 수행 후 ticChanged 호출 + * 2. 시작, 끝 전 후로 delay + state 추가 + * - initialState + * - delayState */ @Composable private fun ToHotAnimateTimeProgressContainerInternal( @@ -104,57 +93,16 @@ private fun ToHotAnimateTimeProgressContainerInternal( val progressAnimatable = remember { Animatable(1f) } LaunchedEffect(key1 = destinationSec, key2 = enable) { if (enable) { - if (progressAnimatable.targetValue == destinationProgress) { - /** - * Timer Animation 중단 후 재개할 경우, progress 는 줄어 들어 있는 상태 에서 - * duration 은 기존 값대로 유지 되어 Animation 이 빠르게 진행 되는 문제 - * -> 현재 progress 에 걸맞는 새로운 duration 계산 - * - * 1. 한 Tic 동안 줄어 들어야 하는 progress 계산 - * - 총 5초일 경우 1.0 / 5 -> 0.2 - * - * 2. 한 Tic 동안 돌아야 하는 잔여 progress 계산 - * - progress 가 0.85 에서 중단 된 후 재개 된다면 현재 Tic 에서 0.5 progress 진행 필요 - * - (0.85 - (destinationSec[4] * oneTicProgress[0.25])) - * -> destinationSec[4] * oneTicProgress[0.25]) 값은, destinationSec 에 도달 했을 progress[0.8] 을 의미 - * - * 3. 잔여 progress 를 oneTicProgress 에 곱한 후 본래 duration 에 곱해서 잔여 duration 계산 - * - 0.05 / 0.2 => 0.25 - * - 0.25 * 1000 => 250 - */ - val oneTicProgress = 1 / maxTimeSec.toFloat() // 한 틱 동안 줄어 들어야 하는 progress.value - val oneTicRemainingProgress = progressAnimatable.value - (destinationSec * oneTicProgress) - val remainingDuration = oneTicRemainingProgress / oneTicProgress * duration - Log.d( - "Timer remaining", - "progressAnimatable.value[${progressAnimatable.value}], " + - " oneTicRemainingProgress[$oneTicRemainingProgress], " + - "remainingDuration[$remainingDuration]" - ) - progressAnimatable.animateTo( - targetValue = destinationProgress, - animationSpec = tween( - durationMillis = remainingDuration.toInt(), - easing = LinearEasing - ) - ) { - currentSec = ceil((this.value * maxTimeSec)).toInt() - } - } else { - Log.d( - "Timer", - "duration => $duration, progress => $destinationProgress," + - " target : ${progressAnimatable.targetValue}, value : ${progressAnimatable.value}" + // 현재 progressValue -> 0.0 까지 필요한 destination 계산 + val progressDuration = duration * (progressAnimatable.value / 1f) + progressAnimatable.animateTo( + targetValue = destinationProgress, + animationSpec = tween( + durationMillis = progressDuration.toInt(), + easing = LinearEasing ) - progressAnimatable.animateTo( - targetValue = destinationProgress, - animationSpec = tween( - durationMillis = duration.toInt(), - easing = LinearEasing - ) - ) { - currentSec = ceil((this.value * maxTimeSec)).toInt() - } + ) { + currentSec = ceil((this.value * maxTimeSec)).toInt() } onTicChanged((progressAnimatable.value * maxTimeSec)) } @@ -184,15 +132,13 @@ private fun ToHotAnimateTimeProgressContainerInternal( } } -//@Composable -//@Preview -//private fun ToHotAnimateTimeProgressContainerPreview() { -// ToHotAnimateTimeProgressContainer( -// modifier = Modifier.padding(horizontal = 13.dp, vertical = 12.dp), -// enable = true, -// maxTimeSec = 5, -// currentSec = 5f, -// ticChanged = {}, -// destinationSec = 4f -// ) -//} +@Composable +@Preview +private fun ToHotAnimateTimeProgressContainerPreview() { + ToHotAnimateTimeProgressContainer( + modifier = Modifier.padding(horizontal = 13.dp, vertical = 12.dp), + enable = true, + onEnd = {}, + durationMill = 1000 + ) +} From 45b15b17ab03b1534578f6ee28fe906985852e65 Mon Sep 17 00:00:00 2001 From: wjchoi96 Date: Sun, 29 Dec 2024 15:52:41 +0900 Subject: [PATCH 09/16] =?UTF-8?q?feat:TOP-107=20Timer=EC=97=90=20initialDe?= =?UTF-8?q?lay,=20completionDelay=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feature/tohot/component/card/ToHotCard.kt | 4 +- .../ToHotAnimateTimeProgressContainer.kt | 60 ++++++++++++------- 2 files changed, 42 insertions(+), 22 deletions(-) diff --git a/feature/tohot/src/main/java/tht/feature/tohot/component/card/ToHotCard.kt b/feature/tohot/src/main/java/tht/feature/tohot/component/card/ToHotCard.kt index d9ccf6be..1d08e5ce 100644 --- a/feature/tohot/src/main/java/tht/feature/tohot/component/card/ToHotCard.kt +++ b/feature/tohot/src/main/java/tht/feature/tohot/component/card/ToHotCard.kt @@ -94,8 +94,10 @@ fun ToHotCard( CardTimerUiModel.ToHotTimer.Timer -> { ToHotAnimateTimeProgressContainer( modifier = timerModifier, + initialDelay = 1000, + completionDelay = 1000, enable = enable && !isHoldCard, - durationMill = maxTimeSec * 1000L, + duration = maxTimeSec * 1000L, onEnd = onTimerEnd ) } diff --git a/feature/tohot/src/main/java/tht/feature/tohot/component/progress/ToHotAnimateTimeProgressContainer.kt b/feature/tohot/src/main/java/tht/feature/tohot/component/progress/ToHotAnimateTimeProgressContainer.kt index 19943002..666b892f 100644 --- a/feature/tohot/src/main/java/tht/feature/tohot/component/progress/ToHotAnimateTimeProgressContainer.kt +++ b/feature/tohot/src/main/java/tht/feature/tohot/component/progress/ToHotAnimateTimeProgressContainer.kt @@ -11,6 +11,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -21,38 +22,53 @@ import androidx.compose.ui.unit.dp import com.example.compose_ui.common.LogComposition import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import kotlin.math.ceil @Composable fun ToHotAnimateTimeProgressContainer( enable: Boolean, - durationMill: Long, + duration: Long, onEnd: () -> Unit, modifier: Modifier = Modifier, + initialDelay: Long = 0L, + completionDelay: Long = 0L, ) { + val coroutineScope = rememberCoroutineScope() LogComposition("cwj_debug", "ToHotAnimateTimeProgressContainer") - val maxSec = remember(durationMill) { (durationMill / 1000).toInt() } - val destinationSec = 0f + var progressState by remember { mutableStateOf(false) } // disActive + LaunchedEffect(initialDelay) { + delay(initialDelay) + progressState = true + } + if (progressState) { + val maxSec = remember(duration) { (duration / 1000).toInt() } + val destinationSec = 0f - ToHotAnimateTimeProgressContainerInternal( - enable = enable, - modifier = modifier, - maxTimeSec = maxSec, - destinationSec = destinationSec, - duration = (maxSec + 1) * 1000f, // 실제 duration 은 1초 추가 - onTicChanged = { - if (it <= destinationSec) { - onEnd() + ToHotAnimateTimeProgressContainerInternal( + enable = enable, + modifier = modifier, + maxTimeSec = maxSec, + destinationSec = destinationSec, + duration = (maxSec + 1) * 1000f, // 실제 duration 은 1초 추가 + onTicChanged = { + coroutineScope.launch { + if (completionDelay > 0) { + progressState = false + } + delay(completionDelay) + if (it <= destinationSec) { + onEnd() + } + } } - } - ) + ) + } else { + ToHotEmptyTimeProgressContainer(modifier = modifier) + } } -/** - * 2. 시작, 끝 전 후로 delay + state 추가 - * - initialState - * - delayState - */ @Composable private fun ToHotAnimateTimeProgressContainerInternal( modifier: Modifier = Modifier, @@ -66,7 +82,8 @@ private fun ToHotAnimateTimeProgressContainerInternal( ), progressBackgroundColor: Color = colorResource(id = tht.core.ui.R.color.black_353535), duration: Float = ((maxTimeSec - destinationSec) * 1000), - onTicChanged: (Float) -> Unit = { } + onTicChanged: (Float) -> Unit = { }, + completionDelayMillis: Long = 0L, ) { var currentSec by remember { mutableIntStateOf(maxTimeSec) } val destinationProgress = destinationSec / maxTimeSec.toFloat() @@ -104,6 +121,7 @@ private fun ToHotAnimateTimeProgressContainerInternal( ) { currentSec = ceil((this.value * maxTimeSec)).toInt() } + delay(completionDelayMillis) onTicChanged((progressAnimatable.value * maxTimeSec)) } } @@ -139,6 +157,6 @@ private fun ToHotAnimateTimeProgressContainerPreview() { modifier = Modifier.padding(horizontal = 13.dp, vertical = 12.dp), enable = true, onEnd = {}, - durationMill = 1000 + duration = 1000 ) } From c21a3d61188e25f89809970cb537ae279cadc4d8 Mon Sep 17 00:00:00 2001 From: wjchoi96 Date: Sun, 29 Dec 2024 15:56:38 +0900 Subject: [PATCH 10/16] =?UTF-8?q?fix:TOP-107=20=EC=A2=8B=EC=95=84=EC=9A=94?= =?UTF-8?q?,=20=EC=8B=AB=EC=96=B4=EC=9A=94=20=EA=B8=B0=EB=8A=A5=EC=97=90?= =?UTF-8?q?=20=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feature/tohot/tohot/viewmodel/ToHotViewModel.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/feature/tohot/src/main/java/tht/feature/tohot/tohot/viewmodel/ToHotViewModel.kt b/feature/tohot/src/main/java/tht/feature/tohot/tohot/viewmodel/ToHotViewModel.kt index 4164ad29..da4813e3 100644 --- a/feature/tohot/src/main/java/tht/feature/tohot/tohot/viewmodel/ToHotViewModel.kt +++ b/feature/tohot/src/main/java/tht/feature/tohot/tohot/viewmodel/ToHotViewModel.kt @@ -555,7 +555,11 @@ class ToHotViewModel @Inject constructor( } fun userHeartEvent(idx: Int) { - if (heartLoading || store.state.value.currentTopic == null) return + if (heartLoading) return + if (store.state.value.currentTopic == null) { + // TODO: Toast + return + } viewModelScope.launch { heartLoading = true sendHeartUseCase( @@ -588,6 +592,10 @@ class ToHotViewModel @Inject constructor( fun userDislikeEvent(idx: Int) { if (heartLoading) return + if (store.state.value.currentTopic == null) { + // TODO: Toast + return + } viewModelScope.launch { heartLoading = true sendDislikeUseCase( From 4ca1c4ab2014a732ae1d34864cf4fcb843ff5106 Mon Sep 17 00:00:00 2001 From: wjchoi96 Date: Sun, 29 Dec 2024 16:15:52 +0900 Subject: [PATCH 11/16] =?UTF-8?q?feat:TOP-107=20enable=20=EC=86=8D?= =?UTF-8?q?=EC=84=B1=EC=97=90=20=EB=94=B0=EB=9D=BC=20initialDelay=EA=B0=80?= =?UTF-8?q?=20=EB=8F=99=EC=9E=91=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../progress/ToHotAnimateTimeProgressContainer.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/feature/tohot/src/main/java/tht/feature/tohot/component/progress/ToHotAnimateTimeProgressContainer.kt b/feature/tohot/src/main/java/tht/feature/tohot/component/progress/ToHotAnimateTimeProgressContainer.kt index 666b892f..401e4ab4 100644 --- a/feature/tohot/src/main/java/tht/feature/tohot/component/progress/ToHotAnimateTimeProgressContainer.kt +++ b/feature/tohot/src/main/java/tht/feature/tohot/component/progress/ToHotAnimateTimeProgressContainer.kt @@ -38,9 +38,11 @@ fun ToHotAnimateTimeProgressContainer( val coroutineScope = rememberCoroutineScope() LogComposition("cwj_debug", "ToHotAnimateTimeProgressContainer") var progressState by remember { mutableStateOf(false) } // disActive - LaunchedEffect(initialDelay) { - delay(initialDelay) - progressState = true + LaunchedEffect(initialDelay, enable) { + if (enable) { + delay(initialDelay) + progressState = true + } } if (progressState) { val maxSec = remember(duration) { (duration / 1000).toInt() } From 4761eda9a86b883fb4768deef4db0c81751a23be Mon Sep 17 00:00:00 2001 From: wjchoi96 Date: Sun, 29 Dec 2024 16:41:16 +0900 Subject: [PATCH 12/16] =?UTF-8?q?[TOP-107]=20CardTimerUiModel=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 불필요한 속성 제거 - ToHotState에서 list가 아닌 단일 객체로 관리(현재 visible한 Card의 TimerState를 표시) --- .../feature/tohot/component/card/ToHotCard.kt | 68 +++++++++--- .../ToHotAnimateTimeProgressContainer.kt | 13 +-- .../feature/tohot/model/CardTimerUiModel.kt | 10 +- .../feature/tohot/tohot/route/ToHotRoute.kt | 2 +- .../feature/tohot/tohot/screen/ToHotScreen.kt | 38 +++---- .../feature/tohot/tohot/state/ToHotState.kt | 2 +- .../tohot/tohot/viewmodel/ToHotViewModel.kt | 102 ++++++------------ 7 files changed, 114 insertions(+), 121 deletions(-) diff --git a/feature/tohot/src/main/java/tht/feature/tohot/component/card/ToHotCard.kt b/feature/tohot/src/main/java/tht/feature/tohot/component/card/ToHotCard.kt index 1d08e5ce..481968ed 100644 --- a/feature/tohot/src/main/java/tht/feature/tohot/component/card/ToHotCard.kt +++ b/feature/tohot/src/main/java/tht/feature/tohot/component/card/ToHotCard.kt @@ -2,7 +2,6 @@ package tht.feature.tohot.component.card import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.tween -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -29,11 +28,13 @@ import tht.feature.tohot.component.progress.ToHotHeartTimeProgressContainer import tht.feature.tohot.component.userinfo.ToHotUserInfoCard import tht.feature.tohot.model.CardTimerUiModel import tht.feature.tohot.model.ImmutableListWrapper +import kotlin.time.DurationUnit +import kotlin.time.toDuration -@OptIn(ExperimentalFoundationApi::class) @Composable fun ToHotCard( modifier: Modifier = Modifier, + timer: CardTimerUiModel?, imageUrls: ImmutableListWrapper, name: String, age: Int, @@ -41,8 +42,6 @@ fun ToHotCard( interests: ImmutableListWrapper, idealTypes: ImmutableListWrapper, introduce: String, - timer: CardTimerUiModel.ToHotTimer, - maxTimeSec: Int, enable: Boolean, fallingAnimationEnable: Boolean = false, isHoldCard: Boolean, @@ -90,14 +89,23 @@ fun ToHotCard( val timerModifier = Modifier .align(Alignment.TopCenter) .padding(horizontal = 13.dp, vertical = 12.dp) - when (timer) { + when (timer?.timerType) { CardTimerUiModel.ToHotTimer.Timer -> { ToHotAnimateTimeProgressContainer( modifier = timerModifier, - initialDelay = 1000, - completionDelay = 1000, + maxTimer = remember(timer) { + timer.maxTimer.toLong(DurationUnit.SECONDS).toInt() + }, + initialDelay = remember(timer) { + timer.initialDelay.toLong(DurationUnit.MILLISECONDS) + }, + completionDelay = remember(timer) { + timer.completionDelay.toLong(DurationUnit.MILLISECONDS) + }, enable = enable && !isHoldCard, - duration = maxTimeSec * 1000L, + duration = remember(timer) { + timer.duration.toLong(DurationUnit.MILLISECONDS) + }, onEnd = onTimerEnd ) } @@ -113,6 +121,12 @@ fun ToHotCard( modifier = timerModifier ) } + + null -> { + ToHotEmptyTimeProgressContainer( + modifier = timerModifier + ) + } } if (isHoldCard) return@FallingCard @@ -168,8 +182,13 @@ private fun ToHotCardPreview() { interests = ImmutableListWrapper(emptyList()), idealTypes = ImmutableListWrapper(emptyList()), introduce = "introduce", - timer = CardTimerUiModel.ToHotTimer.Timer, - maxTimeSec = 5, + timer = CardTimerUiModel( + maxTimer = 5.toDuration(DurationUnit.NANOSECONDS), + initialDelay = 1.toDuration(DurationUnit.NANOSECONDS), + completionDelay = 1.toDuration(DurationUnit.NANOSECONDS), + duration = 6.toDuration(DurationUnit.NANOSECONDS), + startAble = true, + ), enable = true, isHoldCard = false, isShakingCard = false @@ -194,8 +213,13 @@ private fun ToHotCardHoldCardPreview() { interests = ImmutableListWrapper(emptyList()), idealTypes = ImmutableListWrapper(emptyList()), introduce = "introduce", - timer = CardTimerUiModel.ToHotTimer.Timer, - maxTimeSec = 5, + timer = CardTimerUiModel( + maxTimer = 5.toDuration(DurationUnit.NANOSECONDS), + initialDelay = 1.toDuration(DurationUnit.NANOSECONDS), + completionDelay = 1.toDuration(DurationUnit.NANOSECONDS), + duration = 6.toDuration(DurationUnit.NANOSECONDS), + startAble = true, + ), enable = true, isHoldCard = true, isShakingCard = false @@ -220,8 +244,14 @@ private fun ToHotHeartCardPreview() { interests = ImmutableListWrapper(emptyList()), idealTypes = ImmutableListWrapper(emptyList()), introduce = "introduce", - timer = CardTimerUiModel.ToHotTimer.Heart, - maxTimeSec = 5, + timer = CardTimerUiModel( + maxTimer = 5.toDuration(DurationUnit.NANOSECONDS), + initialDelay = 1.toDuration(DurationUnit.NANOSECONDS), + completionDelay = 1.toDuration(DurationUnit.NANOSECONDS), + duration = 6.toDuration(DurationUnit.NANOSECONDS), + startAble = true, + timerType = CardTimerUiModel.ToHotTimer.Heart + ), enable = true, isHoldCard = false, isShakingCard = false @@ -246,8 +276,14 @@ private fun ToHotDislikeCardPreview() { interests = ImmutableListWrapper(emptyList()), idealTypes = ImmutableListWrapper(emptyList()), introduce = "introduce", - timer = CardTimerUiModel.ToHotTimer.Dislike, - maxTimeSec = 5, + timer = CardTimerUiModel( + maxTimer = 5.toDuration(DurationUnit.NANOSECONDS), + initialDelay = 1.toDuration(DurationUnit.NANOSECONDS), + completionDelay = 1.toDuration(DurationUnit.NANOSECONDS), + duration = 6.toDuration(DurationUnit.NANOSECONDS), + startAble = true, + timerType = CardTimerUiModel.ToHotTimer.Dislike + ), enable = true, isHoldCard = false, isShakingCard = false diff --git a/feature/tohot/src/main/java/tht/feature/tohot/component/progress/ToHotAnimateTimeProgressContainer.kt b/feature/tohot/src/main/java/tht/feature/tohot/component/progress/ToHotAnimateTimeProgressContainer.kt index 401e4ab4..91ab3de9 100644 --- a/feature/tohot/src/main/java/tht/feature/tohot/component/progress/ToHotAnimateTimeProgressContainer.kt +++ b/feature/tohot/src/main/java/tht/feature/tohot/component/progress/ToHotAnimateTimeProgressContainer.kt @@ -28,8 +28,9 @@ import kotlin.math.ceil @Composable fun ToHotAnimateTimeProgressContainer( - enable: Boolean, + maxTimer: Int, duration: Long, + enable: Boolean, onEnd: () -> Unit, modifier: Modifier = Modifier, initialDelay: Long = 0L, @@ -45,15 +46,14 @@ fun ToHotAnimateTimeProgressContainer( } } if (progressState) { - val maxSec = remember(duration) { (duration / 1000).toInt() } val destinationSec = 0f ToHotAnimateTimeProgressContainerInternal( enable = enable, modifier = modifier, - maxTimeSec = maxSec, + maxTimeSec = maxTimer, destinationSec = destinationSec, - duration = (maxSec + 1) * 1000f, // 실제 duration 은 1초 추가 + duration = duration, onTicChanged = { coroutineScope.launch { if (completionDelay > 0) { @@ -77,13 +77,13 @@ private fun ToHotAnimateTimeProgressContainerInternal( enable: Boolean, maxTimeSec: Int, destinationSec: Float, + duration: Long, progressColor: ImmutableList = persistentListOf( Color(0xFFF9CC2E), Color(0xFFF98F2E), Color(0xFFF93A2E) ), progressBackgroundColor: Color = colorResource(id = tht.core.ui.R.color.black_353535), - duration: Float = ((maxTimeSec - destinationSec) * 1000), onTicChanged: (Float) -> Unit = { }, completionDelayMillis: Long = 0L, ) { @@ -159,6 +159,7 @@ private fun ToHotAnimateTimeProgressContainerPreview() { modifier = Modifier.padding(horizontal = 13.dp, vertical = 12.dp), enable = true, onEnd = {}, - duration = 1000 + duration = 1000, + maxTimer = 5, ) } diff --git a/feature/tohot/src/main/java/tht/feature/tohot/model/CardTimerUiModel.kt b/feature/tohot/src/main/java/tht/feature/tohot/model/CardTimerUiModel.kt index 3d789148..34d86d3f 100644 --- a/feature/tohot/src/main/java/tht/feature/tohot/model/CardTimerUiModel.kt +++ b/feature/tohot/src/main/java/tht/feature/tohot/model/CardTimerUiModel.kt @@ -1,14 +1,16 @@ package tht.feature.tohot.model import androidx.compose.runtime.Immutable +import kotlin.time.Duration @Immutable data class CardTimerUiModel( - val maxSec: Int, - val currentSec: Float, - val destinationSec: Float, + val maxTimer: Duration, + val initialDelay: Duration, + val completionDelay: Duration, + val duration: Duration, val startAble: Boolean, // card image loading 이 완료 후 timer 실행을 위한 속성 - val timerType: ToHotTimer = ToHotTimer.Timer + val timerType: ToHotTimer = ToHotTimer.Timer, ) { enum class ToHotTimer { Timer, diff --git a/feature/tohot/src/main/java/tht/feature/tohot/tohot/route/ToHotRoute.kt b/feature/tohot/src/main/java/tht/feature/tohot/tohot/route/ToHotRoute.kt index b0a29408..9992acdf 100644 --- a/feature/tohot/src/main/java/tht/feature/tohot/tohot/route/ToHotRoute.kt +++ b/feature/tohot/src/main/java/tht/feature/tohot/tohot/route/ToHotRoute.kt @@ -210,7 +210,7 @@ internal fun ToHotRoute( cardList = toHotState.userList, toHotCardState = toHotState.userCardState, pagerState = pagerState, - timers = toHotState.timers, + timer = toHotState.timer, currentUserIdx = toHotState.enableTimerIdx, cardMoveAllow = toHotState.cardMoveAllow, topicIconUrl = toHotState.currentTopic?.iconUrl, diff --git a/feature/tohot/src/main/java/tht/feature/tohot/tohot/screen/ToHotScreen.kt b/feature/tohot/src/main/java/tht/feature/tohot/tohot/screen/ToHotScreen.kt index 1ab865ce..9c4a0a1e 100644 --- a/feature/tohot/src/main/java/tht/feature/tohot/tohot/screen/ToHotScreen.kt +++ b/feature/tohot/src/main/java/tht/feature/tohot/tohot/screen/ToHotScreen.kt @@ -1,6 +1,5 @@ package tht.feature.tohot.tohot.screen -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -15,9 +14,9 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import tht.feature.tohot.component.card.ToHotCard import tht.feature.tohot.component.card.ToHotEnterCard -import tht.feature.tohot.component.card.ToHotNoneNextUserCard -import tht.feature.tohot.component.card.ToHotNoneInitialUserCard import tht.feature.tohot.component.card.ToHotErrorCard +import tht.feature.tohot.component.card.ToHotNoneInitialUserCard +import tht.feature.tohot.component.card.ToHotNoneNextUserCard import tht.feature.tohot.component.card.ToHotQuerySuccessCard import tht.feature.tohot.component.toolbar.ToHotToolBar import tht.feature.tohot.component.toolbar.ToHotToolBarContent @@ -28,15 +27,16 @@ import tht.feature.tohot.model.ToHotUserUiModel import tht.feature.tohot.tohot.state.ToHotCardState import tht.feature.tohot.tohot.state.ToHotLoading import tht.feature.tohot.tohot.state.ToHotState +import kotlin.time.DurationUnit +import kotlin.time.toDuration -@OptIn(ExperimentalFoundationApi::class) @Composable internal fun ToHotScreen( modifier: Modifier = Modifier, toHotCardState: ToHotCardState, pagerState: PagerState, cardList: ImmutableListWrapper, - timers: ImmutableListWrapper, + timer: CardTimerUiModel, currentUserIdx: Int, cardMoveAllow: Boolean, topicIconUrl: String?, @@ -96,9 +96,8 @@ internal fun ToHotScreen( } ) { idx -> val card = cardList.list[idx] - val enable = idx == currentUserIdx && - currentUserIdx == pagerState.currentPage && - timers.list[idx].startAble && cardMoveAllow + val isCurrentCard = currentUserIdx == pagerState.currentPage && + idx == currentUserIdx ToHotCard( modifier = Modifier .fillMaxSize() @@ -110,9 +109,8 @@ internal fun ToHotScreen( interests = card.interests, idealTypes = card.idealTypes, introduce = card.introduce, - timer = timers.list[idx].timerType, - maxTimeSec = timers.list[idx].maxSec, - enable = enable, + timer = if (isCurrentCard) timer else null, + enable = isCurrentCard && timer.startAble && cardMoveAllow, fallingAnimationEnable = idx == fallingAnimationTargetIdx, isHoldCard = isHoldCard, isShakingCard = isShakingCard, @@ -135,22 +133,18 @@ internal fun ToHotScreen( } } -@OptIn(ExperimentalFoundationApi::class) @Composable @Preview fun ToHotScreenPreview() { val toHotState = ToHotState( userList = ImmutableListWrapper(mockUserList.toList()), userCardState = ToHotCardState.Running, - timers = ImmutableListWrapper( - Array(mockUserList.size) { - CardTimerUiModel( - maxSec = 5, - currentSec = 5f, - destinationSec = 4.5f, - startAble = false - ) - }.toList() + timer = CardTimerUiModel( + maxTimer = 5.toDuration(DurationUnit.NANOSECONDS), + initialDelay = 1.toDuration(DurationUnit.NANOSECONDS), + completionDelay = 1.toDuration(DurationUnit.NANOSECONDS), + duration = 6.toDuration(DurationUnit.NANOSECONDS), + startAble = true, ), enableTimerIdx = 0, cardMoveAllow = true, @@ -169,7 +163,7 @@ fun ToHotScreenPreview() { pagerState = rememberPagerState( pageCount = { toHotState.userList.list.size } ), - timers = toHotState.timers, + timer = toHotState.timer, currentUserIdx = toHotState.enableTimerIdx, cardMoveAllow = toHotState.cardMoveAllow, topicIconUrl = toHotState.currentTopic?.iconUrl, diff --git a/feature/tohot/src/main/java/tht/feature/tohot/tohot/state/ToHotState.kt b/feature/tohot/src/main/java/tht/feature/tohot/tohot/state/ToHotState.kt index 1cdf15a9..baf3c924 100644 --- a/feature/tohot/src/main/java/tht/feature/tohot/tohot/state/ToHotState.kt +++ b/feature/tohot/src/main/java/tht/feature/tohot/tohot/state/ToHotState.kt @@ -12,7 +12,7 @@ data class ToHotState( val loading: ToHotLoading, val userList: ImmutableListWrapper, val userCardState: ToHotCardState = ToHotCardState.NoneSelectTopic, // Start, Empty 경우 보여줄 View 를 정함 - val timers: ImmutableListWrapper, + val timer: CardTimerUiModel, // 현재 표시 중인 Card TimerState val enableTimerIdx: Int, // 현재 표시 되는 Card Idx -> 해당 Card 의 Timer 진행됨 val fallingAnimationIdx: Int = -1, // 신고, 차단 Animation Idx val cardMoveAllow: Boolean, // card suspend 기능. false 일 경우 Timer 중단. Dialog 등이 표시 될 때 사용 diff --git a/feature/tohot/src/main/java/tht/feature/tohot/tohot/viewmodel/ToHotViewModel.kt b/feature/tohot/src/main/java/tht/feature/tohot/tohot/viewmodel/ToHotViewModel.kt index da4813e3..b92bf36e 100644 --- a/feature/tohot/src/main/java/tht/feature/tohot/tohot/viewmodel/ToHotViewModel.kt +++ b/feature/tohot/src/main/java/tht/feature/tohot/tohot/viewmodel/ToHotViewModel.kt @@ -36,6 +36,8 @@ import tht.feature.tohot.tohot.state.ToHotSideEffect import tht.feature.tohot.tohot.state.ToHotState import java.util.Stack import javax.inject.Inject +import kotlin.time.DurationUnit +import kotlin.time.toDuration /** * TODO: UseCase Test Code 작성 @@ -55,7 +57,7 @@ class ToHotViewModel @Inject constructor( ) : ViewModel(), Container { private val initializeState get() = ToHotState( userList = ImmutableListWrapper(emptyList()), - timers = ImmutableListWrapper(emptyList()), + timer = createDefaultTimer(), enableTimerIdx = 0, cardMoveAllow = true, loading = ToHotLoading.None, @@ -111,16 +113,7 @@ class ToHotViewModel @Inject constructor( it.copy( userList = ImmutableListWrapper(newList), userCardState = cardState, - timers = ImmutableListWrapper( - List(toHotState.cards.size) { - CardTimerUiModel( - maxSec = MAX_TIMER_SEC.toInt(), - currentSec = MAX_TIMER_SEC, - destinationSec = MAX_TIMER_SEC, - startAble = false - ) - } - ), + timer = createDefaultTimer(), enableTimerIdx = 0, topicList = ImmutableListWrapper(toHotState.topic.topics.map { t -> t.toUiModel() }), topicModalShow = toHotState.needSelectTopic, @@ -252,7 +245,6 @@ class ToHotViewModel @Inject constructor( reduce { it.copy( userList = ImmutableListWrapper(emptyList()), - timers = ImmutableListWrapper(emptyList()), userCardState = ToHotCardState.NoneNextUser, enableTimerIdx = 0 ) @@ -314,17 +306,7 @@ class ToHotViewModel @Inject constructor( userList = ImmutableListWrapper( store.state.value.userList.list + dailyUserCardList.cards.map { c -> c.toUiModel() } ), - timers = ImmutableListWrapper( - store.state.value.timers.list + - List(dailyUserCardList.cards.size) { - CardTimerUiModel( - maxSec = MAX_TIMER_SEC.toInt(), - currentSec = MAX_TIMER_SEC, - destinationSec = MAX_TIMER_SEC, - startAble = false - ) - } - ), + timer = createDefaultTimer(), topicResetRemainingTime = parseRemainingTime(dailyUserCardList.topicResetTimeMill), topicResetTimeMill = dailyUserCardList.topicResetTimeMill ) @@ -354,17 +336,6 @@ class ToHotViewModel @Inject constructor( store.state.value.userList.list + dailyUserCardList.cards.map { c -> c.toUiModel() } ), userCardState = ToHotCardState.Running, - timers = ImmutableListWrapper( - store.state.value.timers.list + - List(dailyUserCardList.cards.size) { - CardTimerUiModel( - maxSec = MAX_TIMER_SEC.toInt(), - currentSec = MAX_TIMER_SEC, - destinationSec = MAX_TIMER_SEC, - startAble = false - ) - } - ), enableTimerIdx = if (pagingLoading) it.enableTimerIdx else 0, loading = ToHotLoading.None, topicResetRemainingTime = parseRemainingTime(dailyUserCardList.topicResetTimeMill), @@ -479,16 +450,8 @@ class ToHotViewModel @Inject constructor( intent { reduce { it.copy( - timers = ImmutableListWrapper( - it.timers.list.toMutableList().apply { - this[userIdx] = this[userIdx].copy( - maxSec = MAX_TIMER_SEC.toInt(), - currentSec = MAX_TIMER_SEC, - destinationSec = MAX_TIMER_SEC - TIMER_INTERVAL - ) - } - ), enableTimerIdx = userIdx, + timer = createDefaultTimer(startAble = false), // card loading 끝난 이후 true 처리 cardMoveAllow = passedCardCountBetweenTouch <= CARD_COUNT_ALLOW_WITHOUT_TOUCH && it.matchingFullScreenUser == null, reportMenuDialogShow = false, @@ -507,13 +470,7 @@ class ToHotViewModel @Inject constructor( intent { reduce { it.copy( - timers = ImmutableListWrapper( - it.timers.list.toMutableList().apply { - this[idx] = this[idx].copy( - startAble = true - ) - } - ) + timer = createDefaultTimer(startAble = true), ) } } @@ -540,14 +497,6 @@ class ToHotViewModel @Inject constructor( intent { reduce { it.copy( - timers = ImmutableListWrapper( - it.timers.list.toMutableList().apply { - this[userIdx] = this[userIdx].copy( - currentSec = this[userIdx].destinationSec, - destinationSec = this[userIdx].destinationSec - TIMER_INTERVAL - ) - } - ), shakingCard = tic <= SHAKING_ANIMATION_START_TIC ) } @@ -576,10 +525,8 @@ class ToHotViewModel @Inject constructor( intent { reduce { it.copy( - timers = ImmutableListWrapper( - it.timers.list.toMutableList().apply { - this[idx] = this[idx].copy(timerType = CardTimerUiModel.ToHotTimer.Heart) - } + timer = createDefaultTimer( + timerType = CardTimerUiModel.ToHotTimer.Heart ), shakingCard = false ) @@ -612,10 +559,8 @@ class ToHotViewModel @Inject constructor( intent { reduce { it.copy( - timers = ImmutableListWrapper( - it.timers.list.toMutableList().apply { - this[idx] = this[idx].copy(timerType = CardTimerUiModel.ToHotTimer.Dislike) - } + timer = createDefaultTimer( + timerType = CardTimerUiModel.ToHotTimer.Dislike ), shakingCard = false ) @@ -838,9 +783,6 @@ class ToHotViewModel @Inject constructor( userList = ImmutableListWrapper( it.userList.list.toMutableList().apply { removeAt(userIdx) } ), - timers = ImmutableListWrapper( - it.timers.list.toMutableList().apply { removeAt(userIdx) } - ), enableTimerIdx = if (enableTimerIdx >= userIdx) { enableTimerIdx - 1 } else { @@ -899,10 +841,28 @@ class ToHotViewModel @Inject constructor( } } + private fun createDefaultTimer( + startAble: Boolean = false, + timerType: CardTimerUiModel.ToHotTimer = CardTimerUiModel.ToHotTimer.Timer, + ): CardTimerUiModel { + return CardTimerUiModel( + maxTimer = MAX_TIMER_MILL.toDuration(DurationUnit.MILLISECONDS), + initialDelay = TIMER_INITIAL_DELAY_MILL.toDuration(DurationUnit.MILLISECONDS), + completionDelay = TIMER_COMPLETION_DELAY_MILL.toDuration(DurationUnit.MILLISECONDS), + duration = TIMER_DURATION_MILL.toDuration(DurationUnit.MILLISECONDS), + startAble = startAble, + timerType = timerType, + ) + } + companion object { - private const val MAX_TIMER_SEC = 5f + private const val MAX_TIMER_MILL = 5000L + + private const val TIMER_INITIAL_DELAY_MILL = 1000L + + private const val TIMER_COMPLETION_DELAY_MILL = 1000L - private const val TIMER_INTERVAL = 1f + private const val TIMER_DURATION_MILL = 6000L private const val SHAKING_ANIMATION_START_TIC = 3f From efa4186943d9f126bdba9b3f2aee12f824268adf Mon Sep 17 00:00:00 2001 From: wjchoi96 Date: Sun, 29 Dec 2024 16:59:58 +0900 Subject: [PATCH 13/16] =?UTF-8?q?feat:TOP-107=20CardImage=EA=B0=80=20?= =?UTF-8?q?=EB=A8=BC=EC=A0=80=20Load=EB=90=98=EA=B3=A0,=20CardChangedEvent?= =?UTF-8?q?=EA=B0=80=20=EB=B0=9C=EC=83=9D=ED=95=9C=20=EA=B2=BD=EC=9A=B0=20?= =?UTF-8?q?=ED=83=80=EC=9D=B4=EB=A8=B8=20=EB=8F=99=EC=9E=91=EC=9D=B4=20?= =?UTF-8?q?=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tht/feature/tohot/tohot/viewmodel/ToHotViewModel.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/feature/tohot/src/main/java/tht/feature/tohot/tohot/viewmodel/ToHotViewModel.kt b/feature/tohot/src/main/java/tht/feature/tohot/tohot/viewmodel/ToHotViewModel.kt index b92bf36e..54ef96d3 100644 --- a/feature/tohot/src/main/java/tht/feature/tohot/tohot/viewmodel/ToHotViewModel.kt +++ b/feature/tohot/src/main/java/tht/feature/tohot/tohot/viewmodel/ToHotViewModel.kt @@ -82,6 +82,8 @@ class ToHotViewModel @Inject constructor( private val fetchUserListPagingResultChannel = Channel() + private val userCardLoadedIdxSet = mutableSetOf() + private val currentUserListRange: IntRange get() = store.state.value.userList.list.indices @@ -451,7 +453,9 @@ class ToHotViewModel @Inject constructor( reduce { it.copy( enableTimerIdx = userIdx, - timer = createDefaultTimer(startAble = false), // card loading 끝난 이후 true 처리 + timer = createDefaultTimer( + startAble = userCardLoadedIdxSet.contains(userIdx) + ), cardMoveAllow = passedCardCountBetweenTouch <= CARD_COUNT_ALLOW_WITHOUT_TOUCH && it.matchingFullScreenUser == null, reportMenuDialogShow = false, @@ -467,6 +471,7 @@ class ToHotViewModel @Inject constructor( fun userCardLoadFinishEvent(idx: Int, result: Boolean, error: Throwable?) { Log.d("TAG", "userCardLoadFinishEvent => $idx, $result") error?.printStackTrace() + userCardLoadedIdxSet.add(idx) intent { reduce { it.copy( From 15f85e605a128cc95832a905d61ea2f68242f631 Mon Sep 17 00:00:00 2001 From: wjchoi96 Date: Sun, 29 Dec 2024 17:00:11 +0900 Subject: [PATCH 14/16] =?UTF-8?q?chore:TOP-107=20=EB=94=94=EB=B2=84?= =?UTF-8?q?=EA=B7=B8=20=EB=A1=9C=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tht/feature/tohot/tohot/viewmodel/ToHotViewModel.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/feature/tohot/src/main/java/tht/feature/tohot/tohot/viewmodel/ToHotViewModel.kt b/feature/tohot/src/main/java/tht/feature/tohot/tohot/viewmodel/ToHotViewModel.kt index 54ef96d3..6dda2273 100644 --- a/feature/tohot/src/main/java/tht/feature/tohot/tohot/viewmodel/ToHotViewModel.kt +++ b/feature/tohot/src/main/java/tht/feature/tohot/tohot/viewmodel/ToHotViewModel.kt @@ -435,7 +435,7 @@ class ToHotViewModel @Inject constructor( * 중복 데이터 처리를 위해 passedCardIdSet 추가 */ fun userChangeEvent(userIdx: Int) { - Log.d("ToHot", "userChangeEvent => $userIdx") + Log.d(TAG, "userChangeEvent => $userIdx") if (userIdx !in currentUserListRange) return with(store.state.value) { if (!passedCardIdSet.contains(userList.list[userIdx].id)) { @@ -467,9 +467,9 @@ class ToHotViewModel @Inject constructor( } } } - + fun userCardLoadFinishEvent(idx: Int, result: Boolean, error: Throwable?) { - Log.d("TAG", "userCardLoadFinishEvent => $idx, $result") + Log.d(TAG, "userCardLoadFinishEvent => $idx, $result") error?.printStackTrace() userCardLoadedIdxSet.add(idx) intent { @@ -861,6 +861,8 @@ class ToHotViewModel @Inject constructor( } companion object { + private const val TAG = "TO_HOT" + private const val MAX_TIMER_MILL = 5000L private const val TIMER_INITIAL_DELAY_MILL = 1000L From f467a4c3003c5e09594f991c4935d8b3ea0536ae Mon Sep 17 00:00:00 2001 From: wjchoi96 Date: Sun, 29 Dec 2024 17:05:59 +0900 Subject: [PATCH 15/16] chore:TOP-107 lint check --- .../java/tht/feature/tohot/component/card/ToHotCard.kt | 4 ++-- .../progress/ToHotAnimateTimeProgressContainer.kt | 8 ++++---- .../java/tht/feature/tohot/model/CardTimerUiModel.kt | 2 +- .../java/tht/feature/tohot/tohot/screen/ToHotScreen.kt | 2 +- .../tht/feature/tohot/tohot/viewmodel/ToHotViewModel.kt | 9 +++++---- 5 files changed, 13 insertions(+), 12 deletions(-) diff --git a/feature/tohot/src/main/java/tht/feature/tohot/component/card/ToHotCard.kt b/feature/tohot/src/main/java/tht/feature/tohot/component/card/ToHotCard.kt index 481968ed..e4ac9601 100644 --- a/feature/tohot/src/main/java/tht/feature/tohot/component/card/ToHotCard.kt +++ b/feature/tohot/src/main/java/tht/feature/tohot/component/card/ToHotCard.kt @@ -187,7 +187,7 @@ private fun ToHotCardPreview() { initialDelay = 1.toDuration(DurationUnit.NANOSECONDS), completionDelay = 1.toDuration(DurationUnit.NANOSECONDS), duration = 6.toDuration(DurationUnit.NANOSECONDS), - startAble = true, + startAble = true ), enable = true, isHoldCard = false, @@ -218,7 +218,7 @@ private fun ToHotCardHoldCardPreview() { initialDelay = 1.toDuration(DurationUnit.NANOSECONDS), completionDelay = 1.toDuration(DurationUnit.NANOSECONDS), duration = 6.toDuration(DurationUnit.NANOSECONDS), - startAble = true, + startAble = true ), enable = true, isHoldCard = true, diff --git a/feature/tohot/src/main/java/tht/feature/tohot/component/progress/ToHotAnimateTimeProgressContainer.kt b/feature/tohot/src/main/java/tht/feature/tohot/component/progress/ToHotAnimateTimeProgressContainer.kt index 91ab3de9..9dc879aa 100644 --- a/feature/tohot/src/main/java/tht/feature/tohot/component/progress/ToHotAnimateTimeProgressContainer.kt +++ b/feature/tohot/src/main/java/tht/feature/tohot/component/progress/ToHotAnimateTimeProgressContainer.kt @@ -34,7 +34,7 @@ fun ToHotAnimateTimeProgressContainer( onEnd: () -> Unit, modifier: Modifier = Modifier, initialDelay: Long = 0L, - completionDelay: Long = 0L, + completionDelay: Long = 0L ) { val coroutineScope = rememberCoroutineScope() LogComposition("cwj_debug", "ToHotAnimateTimeProgressContainer") @@ -85,12 +85,12 @@ private fun ToHotAnimateTimeProgressContainerInternal( ), progressBackgroundColor: Color = colorResource(id = tht.core.ui.R.color.black_353535), onTicChanged: (Float) -> Unit = { }, - completionDelayMillis: Long = 0L, + completionDelayMillis: Long = 0L ) { var currentSec by remember { mutableIntStateOf(maxTimeSec) } val destinationProgress = destinationSec / maxTimeSec.toFloat() var color by remember(progressColor) { - mutableStateOf(progressColor.firstOrNull() ?: Color.Yellow ) + mutableStateOf(progressColor.firstOrNull() ?: Color.Yellow) } LaunchedEffect(currentSec) { for (i in progressColor.indices) { @@ -160,6 +160,6 @@ private fun ToHotAnimateTimeProgressContainerPreview() { enable = true, onEnd = {}, duration = 1000, - maxTimer = 5, + maxTimer = 5 ) } diff --git a/feature/tohot/src/main/java/tht/feature/tohot/model/CardTimerUiModel.kt b/feature/tohot/src/main/java/tht/feature/tohot/model/CardTimerUiModel.kt index 34d86d3f..7906731b 100644 --- a/feature/tohot/src/main/java/tht/feature/tohot/model/CardTimerUiModel.kt +++ b/feature/tohot/src/main/java/tht/feature/tohot/model/CardTimerUiModel.kt @@ -10,7 +10,7 @@ data class CardTimerUiModel( val completionDelay: Duration, val duration: Duration, val startAble: Boolean, // card image loading 이 완료 후 timer 실행을 위한 속성 - val timerType: ToHotTimer = ToHotTimer.Timer, + val timerType: ToHotTimer = ToHotTimer.Timer ) { enum class ToHotTimer { Timer, diff --git a/feature/tohot/src/main/java/tht/feature/tohot/tohot/screen/ToHotScreen.kt b/feature/tohot/src/main/java/tht/feature/tohot/tohot/screen/ToHotScreen.kt index 9c4a0a1e..d51c7611 100644 --- a/feature/tohot/src/main/java/tht/feature/tohot/tohot/screen/ToHotScreen.kt +++ b/feature/tohot/src/main/java/tht/feature/tohot/tohot/screen/ToHotScreen.kt @@ -144,7 +144,7 @@ fun ToHotScreenPreview() { initialDelay = 1.toDuration(DurationUnit.NANOSECONDS), completionDelay = 1.toDuration(DurationUnit.NANOSECONDS), duration = 6.toDuration(DurationUnit.NANOSECONDS), - startAble = true, + startAble = true ), enableTimerIdx = 0, cardMoveAllow = true, diff --git a/feature/tohot/src/main/java/tht/feature/tohot/tohot/viewmodel/ToHotViewModel.kt b/feature/tohot/src/main/java/tht/feature/tohot/tohot/viewmodel/ToHotViewModel.kt index 6dda2273..e91da84a 100644 --- a/feature/tohot/src/main/java/tht/feature/tohot/tohot/viewmodel/ToHotViewModel.kt +++ b/feature/tohot/src/main/java/tht/feature/tohot/tohot/viewmodel/ToHotViewModel.kt @@ -467,7 +467,7 @@ class ToHotViewModel @Inject constructor( } } } - + fun userCardLoadFinishEvent(idx: Int, result: Boolean, error: Throwable?) { Log.d(TAG, "userCardLoadFinishEvent => $idx, $result") error?.printStackTrace() @@ -475,7 +475,7 @@ class ToHotViewModel @Inject constructor( intent { reduce { it.copy( - timer = createDefaultTimer(startAble = true), + timer = createDefaultTimer(startAble = true) ) } } @@ -486,6 +486,7 @@ class ToHotViewModel @Inject constructor( if (userIdx != enableTimerIdx) return@with tryScrollToNext(userIdx) } + /** * timer tic 이 변경될 때 호출 * - timer 가 0이면 다음 유저 스크롤 @@ -848,7 +849,7 @@ class ToHotViewModel @Inject constructor( private fun createDefaultTimer( startAble: Boolean = false, - timerType: CardTimerUiModel.ToHotTimer = CardTimerUiModel.ToHotTimer.Timer, + timerType: CardTimerUiModel.ToHotTimer = CardTimerUiModel.ToHotTimer.Timer ): CardTimerUiModel { return CardTimerUiModel( maxTimer = MAX_TIMER_MILL.toDuration(DurationUnit.MILLISECONDS), @@ -856,7 +857,7 @@ class ToHotViewModel @Inject constructor( completionDelay = TIMER_COMPLETION_DELAY_MILL.toDuration(DurationUnit.MILLISECONDS), duration = TIMER_DURATION_MILL.toDuration(DurationUnit.MILLISECONDS), startAble = startAble, - timerType = timerType, + timerType = timerType ) } From 402ecb41a26f6d42a389061cfddedaf96d130eff Mon Sep 17 00:00:00 2001 From: wjchoi96 Date: Sun, 29 Dec 2024 17:27:07 +0900 Subject: [PATCH 16/16] =?UTF-8?q?feat:TOP-107=20shakingCard=20=EC=95=A0?= =?UTF-8?q?=EB=8B=88=EB=A9=94=EC=9D=B4=EC=85=98=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feature/tohot/component/card/ToHotCard.kt | 2 ++ .../ToHotAnimateTimeProgressContainer.kt | 27 ++++++++++++------- .../feature/tohot/tohot/route/ToHotRoute.kt | 1 + .../feature/tohot/tohot/screen/ToHotScreen.kt | 3 +++ .../tohot/tohot/viewmodel/ToHotViewModel.kt | 23 +++++++--------- 5 files changed, 32 insertions(+), 24 deletions(-) diff --git a/feature/tohot/src/main/java/tht/feature/tohot/component/card/ToHotCard.kt b/feature/tohot/src/main/java/tht/feature/tohot/component/card/ToHotCard.kt index e4ac9601..5f47e49b 100644 --- a/feature/tohot/src/main/java/tht/feature/tohot/component/card/ToHotCard.kt +++ b/feature/tohot/src/main/java/tht/feature/tohot/component/card/ToHotCard.kt @@ -47,6 +47,7 @@ fun ToHotCard( isHoldCard: Boolean, isShakingCard: Boolean, onFallingAnimationFinish: () -> Unit = { }, + onTicChanged: (Float) -> Unit = { }, onTimerEnd: () -> Unit = { }, userCardClick: () -> Unit = { }, onLikeClick: () -> Unit = { }, @@ -106,6 +107,7 @@ fun ToHotCard( duration = remember(timer) { timer.duration.toLong(DurationUnit.MILLISECONDS) }, + onTicChanged = onTicChanged, onEnd = onTimerEnd ) } diff --git a/feature/tohot/src/main/java/tht/feature/tohot/component/progress/ToHotAnimateTimeProgressContainer.kt b/feature/tohot/src/main/java/tht/feature/tohot/component/progress/ToHotAnimateTimeProgressContainer.kt index 9dc879aa..d5e4c910 100644 --- a/feature/tohot/src/main/java/tht/feature/tohot/component/progress/ToHotAnimateTimeProgressContainer.kt +++ b/feature/tohot/src/main/java/tht/feature/tohot/component/progress/ToHotAnimateTimeProgressContainer.kt @@ -32,6 +32,7 @@ fun ToHotAnimateTimeProgressContainer( duration: Long, enable: Boolean, onEnd: () -> Unit, + onTicChanged: (Float) -> Unit, modifier: Modifier = Modifier, initialDelay: Long = 0L, completionDelay: Long = 0L @@ -56,12 +57,14 @@ fun ToHotAnimateTimeProgressContainer( duration = duration, onTicChanged = { coroutineScope.launch { - if (completionDelay > 0) { - progressState = false - } - delay(completionDelay) if (it <= destinationSec) { + if (completionDelay > 0) { + progressState = false + } + delay(completionDelay) onEnd() + } else { + onTicChanged(it) } } } @@ -84,14 +87,15 @@ private fun ToHotAnimateTimeProgressContainerInternal( Color(0xFFF93A2E) ), progressBackgroundColor: Color = colorResource(id = tht.core.ui.R.color.black_353535), - onTicChanged: (Float) -> Unit = { }, - completionDelayMillis: Long = 0L + onTicChanged: (Float) -> Unit = { } ) { + val progressAnimatable = remember { Animatable(1f) } var currentSec by remember { mutableIntStateOf(maxTimeSec) } val destinationProgress = destinationSec / maxTimeSec.toFloat() var color by remember(progressColor) { mutableStateOf(progressColor.firstOrNull() ?: Color.Yellow) } + LaunchedEffect(currentSec) { for (i in progressColor.indices) { // currentSec로 하면 색상 변경이 좀 늦어져서, 1초 뒤 변경될 progress 기준으로 계산 @@ -109,8 +113,8 @@ private fun ToHotAnimateTimeProgressContainerInternal( label = "animateProgressColor" ) - val progressAnimatable = remember { Animatable(1f) } LaunchedEffect(key1 = destinationSec, key2 = enable) { + var prevTic = 0 if (enable) { // 현재 progressValue -> 0.0 까지 필요한 destination 계산 val progressDuration = duration * (progressAnimatable.value / 1f) @@ -122,9 +126,11 @@ private fun ToHotAnimateTimeProgressContainerInternal( ) ) { currentSec = ceil((this.value * maxTimeSec)).toInt() + if (prevTic != currentSec) { + prevTic = currentSec + onTicChanged(prevTic.toFloat()) + } } - delay(completionDelayMillis) - onTicChanged((progressAnimatable.value * maxTimeSec)) } } @@ -160,6 +166,7 @@ private fun ToHotAnimateTimeProgressContainerPreview() { enable = true, onEnd = {}, duration = 1000, - maxTimer = 5 + maxTimer = 5, + onTicChanged = {} ) } diff --git a/feature/tohot/src/main/java/tht/feature/tohot/tohot/route/ToHotRoute.kt b/feature/tohot/src/main/java/tht/feature/tohot/tohot/route/ToHotRoute.kt index 9992acdf..a451f988 100644 --- a/feature/tohot/src/main/java/tht/feature/tohot/tohot/route/ToHotRoute.kt +++ b/feature/tohot/src/main/java/tht/feature/tohot/tohot/route/ToHotRoute.kt @@ -225,6 +225,7 @@ internal fun ToHotRoute( alarmClickListener = toHotViewModel::alarmClickEvent, pageChanged = toHotViewModel::userChangeEvent, onTimerEnd = toHotViewModel::onTimerEnd, + onTicChanged = toHotViewModel::onTicChanged, loadFinishListener = toHotViewModel::userCardLoadFinishEvent, onLikeClick = toHotViewModel::userHeartEvent, onUnLikeClick = toHotViewModel::userDislikeEvent, diff --git a/feature/tohot/src/main/java/tht/feature/tohot/tohot/screen/ToHotScreen.kt b/feature/tohot/src/main/java/tht/feature/tohot/tohot/screen/ToHotScreen.kt index d51c7611..d1a4f6ac 100644 --- a/feature/tohot/src/main/java/tht/feature/tohot/tohot/screen/ToHotScreen.kt +++ b/feature/tohot/src/main/java/tht/feature/tohot/tohot/screen/ToHotScreen.kt @@ -51,6 +51,7 @@ internal fun ToHotScreen( alarmClickListener: () -> Unit = { }, pageChanged: (Int) -> Unit, onTimerEnd: (Int) -> Unit, + onTicChanged: (Float, Int) -> Unit, onLikeClick: (Int) -> Unit = { }, onUnLikeClick: (Int) -> Unit = { }, onReportMenuClick: () -> Unit = { }, @@ -117,6 +118,7 @@ internal fun ToHotScreen( onFallingAnimationFinish = { onFallingAnimationFinish(idx) }, userCardClick = { }, onReportMenuClick = onReportMenuClick, + onTicChanged = { onTicChanged(it, idx) }, onTimerEnd = { onTimerEnd(idx) }, onLikeClick = { onLikeClick(idx) }, onUnLikeClick = { onUnLikeClick(idx) }, @@ -178,6 +180,7 @@ fun ToHotScreenPreview() { alarmClickListener = { }, pageChanged = { }, onTimerEnd = { }, + onTicChanged = { _, _ -> }, loadFinishListener = { _, _, _ -> }, onLikeClick = { }, onUnLikeClick = { }, diff --git a/feature/tohot/src/main/java/tht/feature/tohot/tohot/viewmodel/ToHotViewModel.kt b/feature/tohot/src/main/java/tht/feature/tohot/tohot/viewmodel/ToHotViewModel.kt index e91da84a..acf3c9db 100644 --- a/feature/tohot/src/main/java/tht/feature/tohot/tohot/viewmodel/ToHotViewModel.kt +++ b/feature/tohot/src/main/java/tht/feature/tohot/tohot/viewmodel/ToHotViewModel.kt @@ -487,24 +487,19 @@ class ToHotViewModel @Inject constructor( tryScrollToNext(userIdx) } - /** - * timer tic 이 변경될 때 호출 - * - timer 가 0이면 다음 유저 스크롤 - * - timer 가 0이 아니면 timer 를 1 감소 - */ - fun ticChangeEvent(tic: Float, userIdx: Int) = with(store.state.value) { + fun onTicChanged(tic: Float, userIdx: Int) = with(store.state.value) { Log.d("Timer", "ticChangeEvent => $tic from $userIdx => enableTimerIdx[$enableTimerIdx]") - if (userIdx != enableTimerIdx) return@with + if (userIdx != enableTimerIdx) return + if (userIdx !in userList.list.indices) return if (tic <= 0) { - tryScrollToNext(userIdx) + onTimerEnd(userIdx) return } - if (userIdx !in userList.list.indices) return - intent { - reduce { - it.copy( - shakingCard = tic <= SHAKING_ANIMATION_START_TIC - ) + if (tic <= SHAKING_ANIMATION_START_TIC) { + intent { + reduce { + it.copy(shakingCard = true) + } } } }