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
Expand Up @@ -6,6 +6,7 @@ data class AchievementAnimation(
val notHasDrawableResId: Int,
val hasAchievement: Boolean = false,
val contentDescription: String,
val testTag: String,
) {
fun getDrawableResId() = if (hasAchievement) hasDrawableResId else notHasDrawableResId
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
package io.github.droidkaigi.confsched2023.testing.robot

import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.isRoot
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.onFirst
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeUp
import com.github.takahirom.roborazzi.captureRoboImage
import io.github.droidkaigi.confsched2023.achievements.AchievementsScreen
import io.github.droidkaigi.confsched2023.achievements.component.AchievementImageATestTag
import io.github.droidkaigi.confsched2023.achievements.component.AchievementImageBTestTag
import io.github.droidkaigi.confsched2023.achievements.component.AchievementImageCTestTag
import io.github.droidkaigi.confsched2023.achievements.component.AchievementImageDTestTag
import io.github.droidkaigi.confsched2023.achievements.component.AchievementImageETestTag
import io.github.droidkaigi.confsched2023.data.achievements.AchievementsDataStore
import io.github.droidkaigi.confsched2023.designsystem.theme.KaigiTheme
import io.github.droidkaigi.confsched2023.model.Achievement
import io.github.droidkaigi.confsched2023.testing.RobotTestRule
import io.github.droidkaigi.confsched2023.testing.coroutines.runTestWithLogging
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.runTest
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds

Expand All @@ -19,6 +29,9 @@ class AchievementsScreenRobot @Inject constructor(
) {
@Inject lateinit var robotTestRule: RobotTestRule
private lateinit var composeTestRule: AndroidComposeTestRule<*, *>

@Inject lateinit var achievementsDataStore: AchievementsDataStore

operator fun invoke(
block: AchievementsScreenRobot.() -> Unit,
) {
Expand Down Expand Up @@ -49,6 +62,35 @@ class AchievementsScreenRobot @Inject constructor(
}
}

fun setupSavedAchievement(achievement: Achievement) = runTest(testDispatcher) {
achievementsDataStore.saveAchievements(achievement)
}

fun clickAchievementImageA() {
composeTestRule.onNode(hasTestTag(AchievementImageATestTag))
.performClick()
}

fun clickAchievementImageB() {
composeTestRule.onNode(hasTestTag(AchievementImageBTestTag))
.performClick()
}

fun clickAchievementImageC() {
composeTestRule.onNode(hasTestTag(AchievementImageCTestTag))
.performClick()
}

fun clickAchievementImageD() {
composeTestRule.onNode(hasTestTag(AchievementImageDTestTag))
.performClick()
}

fun clickAchievementImageE() {
composeTestRule.onNode(hasTestTag(AchievementImageETestTag))
.performClick()
}

fun checkScreenCapture() {
composeTestRule
.onAllNodes(isRoot())
Expand Down
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,
onAchievementClick = { achievement -> viewModel.onAchievementClick(achievement) },
onAnimationFinish = viewModel::onAnimationFinish,
onDisplayedInitialDialog = viewModel::onDisplayedInitialDialog,
)
}

data class AchievementsScreenUiState(
val achievementListUiState: AchievementListUiState,
val isShowInitialDialog: Boolean,
val achievementAnimationState: 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,
onAchievementClick: (Achievement) -> Unit,
onAnimationFinish: () -> 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,
onAchievementClick = onAchievementClick,
modifier = Modifier.padding(
top = innerPadding.calculateTopPadding(),
start = innerPadding.calculateStartPadding(layoutDirection),
end = innerPadding.calculateEndPadding(layoutDirection),
),
)
},
)
if (uiState.achievementAnimationState is Animating) {
DisposableEffect(uiState.achievementAnimationState) {
onDispose {
onAnimationFinish()
}
}
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.achievementAnimationState.animationRawId,
onAnimationFinish = {
onAnimationFinish()
},
)
}
}
}
}

@Composable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ package io.github.droidkaigi.confsched2023.achievements
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import io.github.droidkaigi.confsched2023.achievements.component.AchievementImageATestTag
import io.github.droidkaigi.confsched2023.achievements.component.AchievementImageBTestTag
import io.github.droidkaigi.confsched2023.achievements.component.AchievementImageCTestTag
import io.github.droidkaigi.confsched2023.achievements.component.AchievementImageDTestTag
import io.github.droidkaigi.confsched2023.achievements.component.AchievementImageETestTag
import io.github.droidkaigi.confsched2023.achievements.section.AchievementListUiState
import io.github.droidkaigi.confsched2023.data.contributors.AchievementRepository
import io.github.droidkaigi.confsched2023.designsystem.strings.AppStrings
Expand All @@ -15,6 +20,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 @@ -85,48 +91,58 @@ class AchievementsScreenViewModel @Inject constructor(
notHasDrawableResId = R.drawable.img_achievement_a_off,
hasAchievement = achievements.contains(Achievement.ArcticFox),
contentDescription = "AchievementA image",
testTag = AchievementImageATestTag,
),
AchievementAnimation(
achievement = Achievement.Bumblebee,
hasDrawableResId = R.drawable.img_achievement_b_on,
notHasDrawableResId = R.drawable.img_achievement_b_off,
hasAchievement = achievements.contains(Achievement.Bumblebee),
contentDescription = "AchievementB image",
testTag = AchievementImageBTestTag,
),
AchievementAnimation(
achievement = Achievement.Chipmunk,
hasDrawableResId = R.drawable.img_achievement_c_on,
notHasDrawableResId = R.drawable.img_achievement_c_off,
hasAchievement = achievements.contains(Achievement.Chipmunk),
contentDescription = "AchievementC image",
testTag = AchievementImageCTestTag,
),
AchievementAnimation(
achievement = Achievement.Dolphin,
hasDrawableResId = R.drawable.img_achievement_d_on,
notHasDrawableResId = R.drawable.img_achievement_d_off,
hasAchievement = achievements.contains(Achievement.Dolphin),
contentDescription = "AchievementD image",
testTag = AchievementImageDTestTag,
),
AchievementAnimation(
achievement = Achievement.ElectricEel,
hasDrawableResId = R.drawable.img_achievement_e_on,
notHasDrawableResId = R.drawable.img_achievement_e_off,
hasAchievement = achievements.contains(Achievement.ElectricEel),
contentDescription = "AchievementE image",
testTag = AchievementImageETestTag,
),
),
detailDescription = detailDescription,
isResetButtonEnabled = isResetAchievementsEnable,
)
}

private val achievementAnimationState =
MutableStateFlow<AchievementAnimationState>(AchievementAnimationState.NotAnimating)

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

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

fun onAchievementClick(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.achievementAnimationState.value = AchievementAnimationState.Animating(
achievement = clickedAchievement,
animationRawId = animationRawId,
)
}

fun onAnimationFinish() {
this.achievementAnimationState.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,
onAnimationFinish = {
onReachAnimationEnd()
onFinished()
},
)
}
}
Expand Down
Loading
Loading