• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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