Dice roll refactor with Animatable.

This commit is contained in:
Andres Gomez, Thomas (ITDV RL) 2024-07-12 11:11:18 +02:00
parent 039f124f88
commit 87c6a3f20f
4 changed files with 60 additions and 55 deletions

View file

@ -116,7 +116,7 @@ fun RollOverlay(
overlay.hideOverlay() overlay.hideOverlay()
}, },
onDice = { onDice = {
viewModel.roll() viewModel.roll(uiScope = scope)
}, },
onCard = { onCard = {
viewModel.toggleCardDetail() viewModel.toggleCardDetail()

View file

@ -1,6 +1,7 @@
package com.pixelized.rplexicon.ui.screens.rolls package com.pixelized.rplexicon.ui.screens.rolls
import android.app.Application import android.app.Application
import androidx.compose.animation.core.spring
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@ -14,14 +15,14 @@ import com.pixelized.rplexicon.data.model.CharacterSheet
import com.pixelized.rplexicon.data.model.Description import com.pixelized.rplexicon.data.model.Description
import com.pixelized.rplexicon.data.model.DiceThrow import com.pixelized.rplexicon.data.model.DiceThrow
import com.pixelized.rplexicon.data.model.alteration.AlterationStatus import com.pixelized.rplexicon.data.model.alteration.AlterationStatus
import com.pixelized.rplexicon.data.repository.firebase.RealtimeDatabaseRepository
import com.pixelized.rplexicon.data.repository.character.ActiveAlterationRepository import com.pixelized.rplexicon.data.repository.character.ActiveAlterationRepository
import com.pixelized.rplexicon.data.repository.character.AlterationRepository import com.pixelized.rplexicon.data.repository.character.AlterationRepository
import com.pixelized.rplexicon.data.repository.character.CharacterSheetRepository import com.pixelized.rplexicon.data.repository.character.CharacterSheetRepository
import com.pixelized.rplexicon.data.repository.character.DescriptionRepository import com.pixelized.rplexicon.data.repository.character.DescriptionRepository
import com.pixelized.rplexicon.data.repository.firebase.RealtimeDatabaseRepository
import com.pixelized.rplexicon.ui.screens.character.composable.actions.AlterationItemUio import com.pixelized.rplexicon.ui.screens.character.composable.actions.AlterationItemUio
import com.pixelized.rplexicon.ui.screens.character.pages.alteration.AlterationGroupUio
import com.pixelized.rplexicon.ui.screens.character.composable.dialogs.AlterationDialogDetailUio import com.pixelized.rplexicon.ui.screens.character.composable.dialogs.AlterationDialogDetailUio
import com.pixelized.rplexicon.ui.screens.character.pages.alteration.AlterationGroupUio
import com.pixelized.rplexicon.ui.screens.rolls.composable.RollDiceUio import com.pixelized.rplexicon.ui.screens.rolls.composable.RollDiceUio
import com.pixelized.rplexicon.ui.screens.rolls.composable.ThrowCardFactory import com.pixelized.rplexicon.ui.screens.rolls.composable.ThrowCardFactory
import com.pixelized.rplexicon.ui.screens.rolls.composable.ThrowsCardUio import com.pixelized.rplexicon.ui.screens.rolls.composable.ThrowsCardUio
@ -30,9 +31,9 @@ import com.pixelized.rplexicon.ui.screens.rolls.factory.DiceFactory
import com.pixelized.rplexicon.utilitary.extentions.context import com.pixelized.rplexicon.utilitary.extentions.context
import com.pixelized.rplexicon.utilitary.extentions.switch import com.pixelized.rplexicon.utilitary.extentions.switch
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@ -73,6 +74,7 @@ class RollOverlayViewModel @Inject constructor(
} }
}.collectAsState(initial = emptyList()) }.collectAsState(initial = emptyList())
private var targetRoll = 0f
private val _dice = mutableStateOf<RollDiceUio?>(null) private val _dice = mutableStateOf<RollDiceUio?>(null)
val dice: State<RollDiceUio?> get() = _dice val dice: State<RollDiceUio?> get() = _dice
@ -141,34 +143,40 @@ class RollOverlayViewModel @Inject constructor(
_alterationStatusOverride.value = override _alterationStatusOverride.value = override
} }
fun roll() { fun roll(uiScope: CoroutineScope) {
diceThrow.let { diceThrow ->
rollJob?.cancel() rollJob?.cancel()
rollJob = viewModelScope.launch { rollJob = uiScope.launch {
diceThrow.let { diceThrow ->
// roll the dice ;) // roll the dice ;)
val result = rollUseCase.roll( val result = rollUseCase.roll(
diceThrow = diceThrow, diceThrow = diceThrow,
isThrowHidden = _isThrowHidden.value, isThrowHidden = _isThrowHidden.value,
alterationId = _alterations.value.mapNotNull { if (it.checked) it.label else null }, alterationId = _alterations.value.mapNotNull { if (it.checked) it.label else null },
) )
// share the result // Roll the d20 value.
result?.throws?.let { result?.dice?.let {
firebaseRepository.sendThrow(sheet?.name, it) _dice.value = _dice.value?.copy(
} result = null
// Start the roll animation. )
_dice.value = _dice.value?.let { _dice.value?.rotation?.let { rotation ->
it.copy( targetRoll += 360f * 4
result = "", rotation.animateTo(
animationSpec = RollDiceUio.TWEEN, targetValue = targetRoll,
rotation = it.rotation + 5f, animationSpec = spring(
dampingRatio = 0.9f,
stiffness = 105f,
),
) )
} }
delay(RollDiceUio.ROLL_DURATION) _dice.value = it
// Roll the d20 value. targetRoll = 0f
result?.dice?.let { _dice.value = it } }
delay(250) result?.throws?.let {
// share the result
firebaseRepository.sendThrow(sheet?.name, it)
// Display the roll card. // Display the roll card.
result?.throws?.let { _card.value = throwCardFactory.convert(it) } _card.value = throwCardFactory.convert(it)
}
} }
} }
} }

View file

@ -2,15 +2,17 @@ package com.pixelized.rplexicon.ui.screens.rolls.composable
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.SizeTransform
import androidx.compose.animation.core.LinearOutSlowInEasing import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.SnapSpec import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.expandIn import androidx.compose.animation.expandIn
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkOut import androidx.compose.animation.shrinkOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith import androidx.compose.animation.togetherWith
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
@ -54,21 +56,13 @@ import com.pixelized.rplexicon.utilitary.extentions.lexicon
@Stable @Stable
data class RollDiceUio( data class RollDiceUio(
@DrawableRes val icon: Int = R.drawable.ic_d20_24, @DrawableRes val icon: Int = R.drawable.ic_d20_24,
val rotation: Float = 0f, val rotation: Animatable<Float, AnimationVector1D> = Animatable(0f),
val isCriticalSuccess: Boolean = false, val isCriticalSuccess: Boolean = false,
val isCriticalFailure: Boolean = false, val isCriticalFailure: Boolean = false,
val result: String? = null, val result: String? = null,
val animationSpec: AnimationSpec<Float> = SNAP,
) { ) {
companion object { companion object {
const val ROLL_DURATION = 750L const val ROLL_DURATION = 750
val SNAP = SnapSpec<Float>()
val TWEEN = tween<Float>(
durationMillis = ROLL_DURATION.toInt(),
easing = LinearOutSlowInEasing,
)
} }
} }
@ -122,11 +116,6 @@ private fun Dice(
) { ) {
dice.value?.let { dice.value?.let {
val time = rememberTimeState() val time = rememberTimeState()
val animatedRotation = animateFloatAsState(
targetValue = it.rotation * 360,
animationSpec = it.animationSpec,
label = "AnimatedRotation"
)
val transition = animateFloatAsState( val transition = animateFloatAsState(
targetValue = if (it.isCriticalSuccess) 1.0f else 0f, targetValue = if (it.isCriticalSuccess) 1.0f else 0f,
label = "animatedCritical", label = "animatedCritical",
@ -134,7 +123,7 @@ private fun Dice(
) )
Image( Image(
modifier = modifier modifier = modifier
.rotate(degrees = animatedRotation.value % 360) .rotate(degrees = it.rotation.value % 360)
.dancingColor( .dancingColor(
time = time, time = time,
transition = transition, transition = transition,
@ -153,19 +142,28 @@ private fun Result(
dice: State<RollDiceUio?>, dice: State<RollDiceUio?>,
) { ) {
AnimatedContent( AnimatedContent(
modifier = modifier, modifier = Modifier
.background(brush = MaterialTheme.lexicon.colorScheme.dice.valueBackground)
.then(other = modifier),
targetState = dice.value?.result, targetState = dice.value?.result,
transitionSpec = { transitionSpec = {
val enter = fadeIn() + expandIn(expandFrom = Alignment.Center) { it } val enter = fadeIn() +
val exit = fadeOut() + shrinkOut(shrinkTowards = Alignment.Center) { IntSize.Zero } expandIn(expandFrom = Alignment.Center) { it } +
enter togetherWith exit slideInVertically { it / 5 }
val exit = fadeOut() +
shrinkOut(shrinkTowards = Alignment.Center) { IntSize.Zero } +
slideOutVertically { -it / 5 }
enter togetherWith exit using SizeTransform(clip = false)
}, },
label = "ResultAnimation" label = "ResultAnimation"
) { ) {
when (it) { when (it) {
null -> Box(modifier = Modifier) null -> Box(
modifier = Modifier,
)
else -> Box( else -> Box(
modifier = Modifier.background(brush = MaterialTheme.lexicon.colorScheme.dice.valueBackground), modifier = Modifier,
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
Text( Text(
@ -232,7 +230,7 @@ private fun SkillRollPreview(
mutableStateOf( mutableStateOf(
RollDiceUio( RollDiceUio(
icon = R.drawable.ic_d20_24, icon = R.drawable.ic_d20_24,
rotation = 0f, rotation = Animatable(0f),
isCriticalSuccess = preview == 20, isCriticalSuccess = preview == 20,
isCriticalFailure = preview == 1, isCriticalFailure = preview == 1,
result = "$preview", result = "$preview",

View file

@ -20,7 +20,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -153,7 +152,7 @@ private fun ClassHeaderDice(
else -> MaterialTheme.colorScheme.onSurface else -> MaterialTheme.colorScheme.onSurface
}, },
animationSpec = tween( animationSpec = tween(
delayMillis = RollDiceUio.ROLL_DURATION.toInt() delayMillis = RollDiceUio.ROLL_DURATION,
), ),
label = "ClassHeaderDiceColorAnimation" label = "ClassHeaderDiceColorAnimation"
) )
@ -197,12 +196,12 @@ private fun ClassHeaderDice(
private fun labelAnimationSpec(): AnimatedContentTransitionScope<*>.() -> ContentTransform = { private fun labelAnimationSpec(): AnimatedContentTransitionScope<*>.() -> ContentTransform = {
val slideIn = slideInVertically( val slideIn = slideInVertically(
animationSpec = tween( animationSpec = tween(
delayMillis = RollDiceUio.ROLL_DURATION.toInt() delayMillis = RollDiceUio.ROLL_DURATION
) )
) { it / 2 } ) { it / 2 }
val fadeIn = fadeIn( val fadeIn = fadeIn(
animationSpec = tween( animationSpec = tween(
delayMillis = RollDiceUio.ROLL_DURATION.toInt() delayMillis = RollDiceUio.ROLL_DURATION
) )
) )
val slideOut = slideOutVertically( val slideOut = slideOutVertically(