Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add achivement click animation #1235 #1265

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package io.github.droidkaigi.confsched2023.achievements

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
Expand All @@ -16,8 +18,10 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
Expand All @@ -31,8 +35,11 @@ import androidx.navigation.NavController
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import io.github.droidkaigi.confsched2023.achievements.AchievementAnimationState.Animating
import io.github.droidkaigi.confsched2023.achievements.component.AchievementHighlightAnimation
import io.github.droidkaigi.confsched2023.achievements.section.AchievementList
import io.github.droidkaigi.confsched2023.achievements.section.AchievementListUiState
import io.github.droidkaigi.confsched2023.model.Achievement
import io.github.droidkaigi.confsched2023.ui.SnackbarMessageEffect

const val achievementsScreenRoute = "achievements"
Expand Down Expand Up @@ -83,51 +90,85 @@ fun AchievementsScreen(
snackbarHostState = snackbarHostState,
contentPadding = contentPadding,
onReset = viewModel::onReset,
showAnimation = { achievement -> viewModel.onClickAchievement(achievement) },
finishAnimation = viewModel::onFinishAnimation,
Copy link
Member

Choose a reason for hiding this comment

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

We would like to promote the best practice that a StateHolder should be notified events.
So how about naming this onAchivementClicked and judge if we run a animation in ViewModel for showAnimation?
And use "onAnimationFinished" as a name for finishAnimation?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Renamed showAnimation to onAchivementClick and finishAnimation to onAnimationFinish.

onDisplayedInitialDialog = viewModel::onDisplayedInitialDialog,
)
}

data class AchievementsScreenUiState(
val achievementListUiState: AchievementListUiState,
val isShowInitialDialog: Boolean,
val clickedAchievement: AchievementAnimationState,
)

sealed interface AchievementAnimationState {
data class Animating(
val achievement: Achievement,
val animationRawId: Int,
) : AchievementAnimationState
data object NotAnimating : AchievementAnimationState
}

@Composable
private fun AchievementsScreen(
uiState: AchievementsScreenUiState,
snackbarHostState: SnackbarHostState,
contentPadding: PaddingValues,
onReset: () -> Unit,
showAnimation: (Achievement) -> Unit,
finishAnimation: () -> Unit,
onDisplayedInitialDialog: () -> Unit,
) {
val layoutDirection = LocalLayoutDirection.current
Scaffold(
modifier = Modifier.testTag(AchievementsScreenTestTag),
snackbarHost = { SnackbarHost(snackbarHostState) },
contentWindowInsets = WindowInsets(
left = contentPadding.calculateLeftPadding(layoutDirection),
top = contentPadding.calculateTopPadding(),
right = contentPadding.calculateRightPadding(layoutDirection),
bottom = contentPadding.calculateBottomPadding(),
),
content = { innerPadding ->
if (uiState.isShowInitialDialog) {
AchievementScreenDialog(
onDismissRequest = onDisplayedInitialDialog,
Box {
Scaffold(
modifier = Modifier.testTag(AchievementsScreenTestTag),
snackbarHost = { SnackbarHost(snackbarHostState) },
contentWindowInsets = WindowInsets(
left = contentPadding.calculateLeftPadding(layoutDirection),
top = contentPadding.calculateTopPadding(),
right = contentPadding.calculateRightPadding(layoutDirection),
bottom = contentPadding.calculateBottomPadding(),
),
content = { innerPadding ->
if (uiState.isShowInitialDialog) {
AchievementScreenDialog(
onDismissRequest = onDisplayedInitialDialog,
)
}
AchievementList(
uiState = uiState.achievementListUiState,
contentPadding = innerPadding,
onReset = onReset,
showAnimation = showAnimation,
modifier = Modifier.padding(
top = innerPadding.calculateTopPadding(),
start = innerPadding.calculateStartPadding(layoutDirection),
end = innerPadding.calculateEndPadding(layoutDirection),
),
)
},
)
if (uiState.clickedAchievement is Animating) {
DisposableEffect(uiState.clickedAchievement) {
onDispose {
finishAnimation()
}
}
AchievementList(
uiState = uiState.achievementListUiState,
contentPadding = innerPadding,
onReset = onReset,
modifier = Modifier.padding(
top = innerPadding.calculateTopPadding(),
start = innerPadding.calculateStartPadding(layoutDirection),
end = innerPadding.calculateEndPadding(layoutDirection),
),
)
},
)
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background.copy(alpha = 0.6F),
) {
AchievementHighlightAnimation(
animationRawId = uiState.clickedAchievement.animationRawId,
onFinishAnimation = {
finishAnimation()
},
)
}
}
}
}

@Composable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import io.github.droidkaigi.confsched2023.ui.handleErrorAndRetry
import kotlinx.collections.immutable.PersistentSet
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
Expand Down Expand Up @@ -120,13 +121,17 @@ class AchievementsScreenViewModel @Inject constructor(
)
}

private val clickedAchievement = MutableStateFlow<AchievementAnimationState>(AchievementAnimationState.NotAnimating)
Copy link
Member

Choose a reason for hiding this comment

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

Could you change this name as we change the state? 🙇

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry for overlooking. I renamed clickedAchievement to achievementAnimationState.


val uiState = buildUiState(
achievementAnimationListState,
isInitialDialogDisplayFlow,
) { achievementListUiState, isDisplayedInitialDialog ->
clickedAchievement,
) { achievementListUiState, isDisplayedInitialDialog, clickedAchievement ->
AchievementsScreenUiState(
achievementListUiState = achievementListUiState,
isShowInitialDialog = isDisplayedInitialDialog?.not() ?: false,
clickedAchievement = clickedAchievement,
)
}

Expand All @@ -141,4 +146,22 @@ class AchievementsScreenViewModel @Inject constructor(
achievementRepository.displayedInitialDialog()
}
}

fun onClickAchievement(clickedAchievement: Achievement) {
val animationRawId: Int = when (clickedAchievement) {
Achievement.ArcticFox -> R.raw.achievement_a_lottie
Achievement.Bumblebee -> R.raw.achievement_b_lottie
Achievement.Chipmunk -> R.raw.achievement_c_lottie
Achievement.Dolphin -> R.raw.achievement_d_lottie
Achievement.ElectricEel -> R.raw.achievement_e_lottie
}
this.clickedAchievement.value = AchievementAnimationState.Animating(
achievement = clickedAchievement,
animationRawId = animationRawId,
)
}

fun onFinishAnimation() {
this.clickedAchievement.value = AchievementAnimationState.NotAnimating
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec.RawRes
import com.airbnb.lottie.compose.animateLottieCompositionAsState
import com.airbnb.lottie.compose.rememberLottieComposition
import io.github.droidkaigi.confsched2023.achievements.component.AchievementHighlightAnimation
import io.github.droidkaigi.confsched2023.ui.SnackbarMessageEffect

data class AnimationScreenUiState(
Expand Down Expand Up @@ -59,19 +56,12 @@ fun AchievementAnimationScreen(
.fillMaxSize(),
) {
if (uiState.rawId != null) {
val lottieComposition by rememberLottieComposition(RawRes(uiState.rawId))
val progress by animateLottieCompositionAsState(
composition = lottieComposition,
isPlaying = true,
restartOnPlay = true,
)
if (progress == 1f) {
onReachAnimationEnd()
onFinished()
}
LottieAnimation(
composition = lottieComposition,
progress = { progress },
AchievementHighlightAnimation(
animationRawId = uiState.rawId,
onFinishAnimation = {
onReachAnimationEnd()
onFinished()
},
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.github.droidkaigi.confsched2023.achievements.component

import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec.RawRes
import com.airbnb.lottie.compose.animateLottieCompositionAsState
import com.airbnb.lottie.compose.rememberLottieComposition

@Composable
fun AchievementHighlightAnimation(
animationRawId: Int,
onFinishAnimation: () -> Unit,
) {
val lottieComposition by rememberLottieComposition(RawRes(animationRawId))
val progress by animateLottieCompositionAsState(
composition = lottieComposition,
isPlaying = true,
restartOnPlay = true,
)
if (progress == 1f) {
onFinishAnimation()
}
LottieAnimation(
composition = lottieComposition,
progress = { progress },
)
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,40 @@
package io.github.droidkaigi.confsched2023.achievements.component

import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import io.github.droidkaigi.confsched2023.model.Achievement
import io.github.droidkaigi.confsched2023.model.AchievementAnimation

@Composable
fun AchievementImage(
achievementAnimation: AchievementAnimation,
modifier: Modifier = Modifier,
showAnimation: (Achievement) -> Unit,
) {
val interactionSource = remember { MutableInteractionSource() }
Image(
painter = painterResource(id = achievementAnimation.getDrawableResId()),
contentDescription = achievementAnimation.contentDescription,
modifier = modifier
.padding(horizontal = 21.dp),
.padding(horizontal = 21.dp)
.then(
if (achievementAnimation.hasAchievement) {
Modifier.clickable(
interactionSource = interactionSource,
indication = null,
) {
showAnimation(achievementAnimation.achievement)
}
} else {
Modifier
},
),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.dp
import io.github.droidkaigi.confsched2023.achievements.component.AchievementImage
import io.github.droidkaigi.confsched2023.achievements.component.AchievementsDetail
import io.github.droidkaigi.confsched2023.model.Achievement
import io.github.droidkaigi.confsched2023.model.AchievementAnimation
import kotlinx.collections.immutable.ImmutableList

Expand All @@ -37,6 +38,7 @@ fun AchievementList(
uiState: AchievementListUiState,
contentPadding: PaddingValues,
onReset: () -> Unit,
showAnimation: (Achievement) -> Unit,
modifier: Modifier = Modifier,
) {
val layoutDirection = LocalLayoutDirection.current
Expand Down Expand Up @@ -77,6 +79,7 @@ fun AchievementList(
) { achievementAnimation ->
AchievementImage(
achievementAnimation = achievementAnimation,
showAnimation = showAnimation,
)
}
if (uiState.isResetButtonEnabled) {
Expand Down
Loading