1 /*
<lambda>null2 * Copyright (C) 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package com.android.systemui.inputdevice.tutorial.ui.composable
18
19 import androidx.annotation.RawRes
20 import androidx.compose.animation.AnimatedContent
21 import androidx.compose.animation.EnterTransition
22 import androidx.compose.animation.core.LinearEasing
23 import androidx.compose.animation.core.tween
24 import androidx.compose.animation.fadeOut
25 import androidx.compose.animation.togetherWith
26 import androidx.compose.foundation.clickable
27 import androidx.compose.foundation.layout.Box
28 import androidx.compose.foundation.layout.fillMaxSize
29 import androidx.compose.foundation.layout.fillMaxWidth
30 import androidx.compose.runtime.Composable
31 import androidx.compose.runtime.getValue
32 import androidx.compose.runtime.mutableStateOf
33 import androidx.compose.runtime.remember
34 import androidx.compose.runtime.saveable.rememberSaveable
35 import androidx.compose.runtime.setValue
36 import androidx.compose.ui.Alignment
37 import androidx.compose.ui.Modifier
38 import androidx.compose.ui.node.Ref
39 import androidx.compose.ui.res.stringResource
40 import androidx.compose.ui.semantics.contentDescription
41 import androidx.compose.ui.semantics.semantics
42 import androidx.compose.ui.util.lerp
43 import com.airbnb.lottie.LottieComposition
44 import com.airbnb.lottie.compose.LottieAnimation
45 import com.airbnb.lottie.compose.LottieCompositionSpec
46 import com.airbnb.lottie.compose.LottieConstants
47 import com.airbnb.lottie.compose.LottieDynamicProperties
48 import com.airbnb.lottie.compose.animateLottieCompositionAsState
49 import com.airbnb.lottie.compose.rememberLottieComposition
50 import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Error
51 import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.Finished
52 import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.InProgress
53 import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.InProgressAfterError
54 import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionState.NotStarted
55 import com.android.systemui.res.R
56
57 @Composable
58 fun TutorialAnimation(
59 actionState: TutorialActionState,
60 config: TutorialScreenConfig,
61 modifier: Modifier = Modifier,
62 ) {
63 Box(contentAlignment = Alignment.Center, modifier = modifier.fillMaxWidth()) {
64 AnimatedContent(
65 targetState = actionState::class,
66 transitionSpec = {
67 EnterTransition.None.togetherWith(
68 fadeOut(animationSpec = tween(durationMillis = 10, easing = LinearEasing))
69 )
70 // we don't want size transform because when targetState animation is loaded for
71 // the first time, AnimatedContent thinks target size is smaller and tries to
72 // shrink initial state
73 .using(sizeTransform = null)
74 },
75 ) { state ->
76 when (state) {
77 NotStarted::class,
78 Error::class ->
79 EducationAnimation(
80 config.animations.educationResId,
81 config.colors.animationColors,
82 )
83 InProgress::class,
84 InProgressAfterError::class ->
85 InProgressAnimation(
86 // actionState can be already of different class while this composable is
87 // transitioning to another one
88 actionState as? Progress,
89 config.animations.educationResId,
90 config.colors.animationColors,
91 )
92 Finished::class ->
93 // Below cast is safe as Finished state is the last state and afterwards we can
94 // only leave the screen so this composable would be no longer displayed
95 SuccessAnimation(actionState as Finished, config.colors.animationColors)
96 }
97 }
98 }
99 }
100
101 @Composable
EducationAnimationnull102 private fun EducationAnimation(
103 @RawRes educationAnimationId: Int,
104 animationProperties: LottieDynamicProperties,
105 ) {
106 val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(educationAnimationId))
107 var isPlaying by remember { mutableStateOf(true) }
108 val progress by
109 animateLottieCompositionAsState(
110 composition,
111 iterations = LottieConstants.IterateForever,
112 isPlaying = isPlaying,
113 restartOnPlay = false,
114 )
115 val animationDescription = stringResource(R.string.tutorial_animation_content_description)
116 LottieAnimation(
117 composition = composition,
118 progress = { progress },
119 dynamicProperties = animationProperties,
120 modifier =
121 Modifier.fillMaxSize()
122 .clickable { isPlaying = !isPlaying }
123 .semantics { contentDescription = animationDescription },
124 )
125 }
126
127 @Composable
SuccessAnimationnull128 private fun SuccessAnimation(
129 finishedState: Finished,
130 animationProperties: LottieDynamicProperties,
131 ) {
132 val composition by
133 rememberLottieComposition(LottieCompositionSpec.RawRes(finishedState.successAnimation))
134 var animationFinished by rememberSaveable(key = "animationFinished") { mutableStateOf(false) }
135 val progress by animateLottieCompositionAsState(composition, iterations = 1)
136 if (progress == 1f) {
137 animationFinished = true
138 }
139 LottieAnimation(
140 composition = composition,
141 progress = { if (animationFinished) 1f else progress },
142 dynamicProperties = animationProperties,
143 modifier = Modifier.fillMaxSize(),
144 )
145 }
146
147 @Composable
InProgressAnimationnull148 private fun InProgressAnimation(
149 state: Progress?,
150 @RawRes inProgressAnimationId: Int,
151 animationProperties: LottieDynamicProperties,
152 ) {
153 // Caching latest progress for when we're animating this view away and state is null.
154 // Without this there's jumpcut in the animation while it's animating away.
155 // state should never be null when composable appears, only when disappearing
156 val cached = remember { Ref<Progress>() }
157 cached.value = state ?: cached.value
158 val progress = cached.value?.progress ?: 0f
159
160 val composition by
161 rememberLottieComposition(LottieCompositionSpec.RawRes(inProgressAnimationId))
162 val startProgress =
163 rememberSaveable(composition, cached.value?.startMarker) {
164 composition.progressForMarker(cached.value?.startMarker)
165 }
166 val endProgress =
167 rememberSaveable(composition, cached.value?.endMarker) {
168 composition.progressForMarker(cached.value?.endMarker)
169 }
170 LottieAnimation(
171 composition = composition,
172 progress = { lerp(start = startProgress, stop = endProgress, fraction = progress) },
173 dynamicProperties = animationProperties,
174 modifier = Modifier.fillMaxSize(),
175 )
176 }
177
progressForMarkernull178 private fun LottieComposition?.progressForMarker(marker: String?): Float {
179 if (marker == null) return 0f
180 val startFrame = this?.getMarker(marker)?.startFrame ?: 0f
181 return this?.getProgressForFrame(startFrame) ?: 0f
182 }
183