1 /*
<lambda>null2  * Copyright 2022 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 androidx.compose.ui.tooling.animation
18 
19 import androidx.compose.animation.core.Animatable
20 import androidx.compose.animation.core.AnimationSpec
21 import androidx.compose.animation.core.AnimationVector
22 import androidx.compose.animation.core.DecayAnimation
23 import androidx.compose.animation.core.InfiniteTransition
24 import androidx.compose.animation.core.TargetBasedAnimation
25 import androidx.compose.animation.core.Transition
26 import androidx.compose.runtime.MutableState
27 import androidx.compose.runtime.State
28 import androidx.compose.ui.tooling.data.CallGroup
29 import androidx.compose.ui.tooling.data.Group
30 import androidx.compose.ui.tooling.data.UiToolingDataApi
31 import androidx.compose.ui.tooling.findAll
32 import androidx.compose.ui.tooling.firstOrNull
33 import kotlin.reflect.KClass
34 import kotlin.reflect.safeCast
35 
36 private const val UPDATE_TRANSITION = "updateTransition"
37 private const val ANIMATED_CONTENT = "AnimatedContent"
38 private const val ANIMATED_VISIBILITY = "AnimatedVisibility"
39 private const val ANIMATE_VALUE_AS_STATE = "animateValueAsState"
40 private const val REMEMBER = "remember"
41 private const val REMEMBER_INFINITE_TRANSITION = "rememberInfiniteTransition"
42 private const val REMEMBER_UPDATED_STATE = "rememberUpdatedState"
43 private const val SIZE_ANIMATION_MODIFIER =
44     "androidx.compose.animation.SizeAnimationModifierElement"
45 
46 /** Find first data with type [T] within all remember calls. */
47 @OptIn(UiToolingDataApi::class)
48 private inline fun <reified T> Collection<Group>.findRememberedData(): List<T> {
49     val selfData = mapNotNull { it.data.firstOrNull { data -> data is T } as? T }
50     val rememberCalls = mapNotNull { it.firstOrNull { call -> call.name == "remember" } }
51     return selfData + rememberCalls.mapNotNull { it.data.firstOrNull { data -> data is T } as? T }
52 }
53 
54 @OptIn(UiToolingDataApi::class)
findRememberedDatanull55 private inline fun <reified T> Group.findRememberedData(): List<T> {
56     val thisData = data.firstOrNull() { it is T } as? T
57     return (thisData?.let { listOf(it) } ?: emptyList()) + children.findRememberedData<T>()
58 }
59 
60 @OptIn(UiToolingDataApi::class)
findDatanull61 private inline fun <reified T> Group.findData(includeGrandchildren: Boolean = false): T? {
62     // Search in self data and children data
63     val dataToSearch =
64         data +
65             children
66                 .let {
67                     if (includeGrandchildren) (it + it.flatMap { child -> child.children }) else it
68                 }
69                 .flatMap { it.data }
70     return dataToSearch.firstOrNull { data -> data is T } as? T
71 }
72 
73 /** Contains tree parsers for different animation types. */
74 @OptIn(UiToolingDataApi::class)
75 internal class AnimationSearch(
76     private val clock: () -> PreviewAnimationClock,
77     private val onSeek: () -> Unit,
78 ) {
<lambda>null79     private val transitionSearch = TransitionSearch { clock().trackTransition(it) }
<lambda>null80     private val animatedContentSearch = AnimatedContentSearch { clock().trackAnimatedContent(it) }
<lambda>null81     private val animatedVisibilitySearch = AnimatedVisibilitySearch {
82         clock().trackAnimatedVisibility(it, onSeek)
83     }
84 
animateXAsStateSearchnull85     private fun animateXAsStateSearch() =
86         if (AnimateXAsStateComposeAnimation.apiAvailable)
87             setOf(AnimateXAsStateSearch() { clock().trackAnimateXAsState(it) })
88         else emptyList()
89 
infiniteTransitionSearchnull90     private fun infiniteTransitionSearch() =
91         if (InfiniteTransitionComposeAnimation.apiAvailable)
92             setOf(InfiniteTransitionSearch() { clock().trackInfiniteTransition(it) })
93         else emptySet()
94 
95     /** All supported animations. */
supportedSearchnull96     private fun supportedSearch() =
97         setOf(
98             transitionSearch,
99             animatedVisibilitySearch,
100         ) +
101             animateXAsStateSearch() +
102             infiniteTransitionSearch() +
103             (if (AnimatedContentComposeAnimation.apiAvailable) setOf(animatedContentSearch)
104             else emptySet())
105 
106     private fun unsupportedSearch() =
107         if (UnsupportedComposeAnimation.apiAvailable)
108             setOf(
109                 AnimateContentSizeSearch { clock().trackAnimateContentSize(it) },
<lambda>null110                 TargetBasedSearch { clock().trackTargetBasedAnimations(it) },
<lambda>null111                 DecaySearch { clock().trackDecayAnimations(it) }
112             )
113         else emptyList()
114 
115     /** All supported animations. */
116     private val supportedSearch = supportedSearch()
117 
118     /** Animations to track in PreviewAnimationClock. */
119     private val setToTrack = supportedSearch + unsupportedSearch()
120 
121     /**
122      * Animations to search. animatedContentSearch is included even if it's not going to be tracked
123      * as it should be excluded from transitionSearch.
124      */
125     private val setToSearch = setToTrack + setOf(animatedContentSearch)
126 
127     /**
128      * Finds if any of supported animations are available. It DOES NOT map detected animations to
129      * corresponding animation types.
130      */
searchAnynull131     fun searchAny(slotTrees: Collection<Group>): Boolean {
132         return slotTrees.any { tree ->
133             val groups = tree.findAll { true }
134             supportedSearch.any { search -> search.hasAnimations(groups) }
135         }
136     }
137 
138     /**
139      * Finds all animations defined in the Compose tree where the root is the `@Composable` being
140      * previewed. Each found animation is mapped to corresponding animation type and tracked. It
141      * should NOT be called if no animation is found with [searchAny].
142      */
attachAllAnimationsnull143     fun attachAllAnimations(slotTrees: Collection<Group>) {
144         // Check all the slot tables, since some animations might not be present in the same
145         // table as the one containing the `@Composable` being previewed, e.g. when they're
146         // defined using sub-composition.
147         slotTrees.forEach { tree ->
148             val groups = tree.findAll { true }
149             setToSearch.forEach { it.addAnimations(groups) }
150             // Remove all AnimatedVisibility parent transitions from the transitions list,
151             // otherwise we'd duplicate them in the Android Studio Animation Preview because we
152             // will track them separately.
153             transitionSearch.animations.removeAll(animatedVisibilitySearch.animations)
154             // Remove all AnimatedContent parent transitions from the transitions list, so we can
155             // ignore these animations while support is not added to Animation Preview.
156             transitionSearch.animations.removeAll(animatedContentSearch.animations)
157         }
158         // Make the clock track all the animations found.
159         setToTrack.forEach { it.track() }
160     }
161 
162     /** Search for animations with type [T]. */
163     abstract class Search<T : Any>(private val trackAnimation: (T) -> Unit) {
164 
hasAnimationsnull165         fun hasAnimations(groups: Collection<Group>): Boolean {
166             return groups.any { hasAnimation(it) }
167         }
168 
hasAnimationnull169         abstract fun hasAnimation(group: Group): Boolean
170 
171         val animations = mutableSetOf<T>()
172 
173         /**
174          * Add all found animations to [animations] list. Should only be called in Animation
175          * Preview, otherwise other tools (e.g. Interactive Preview) might not render animations
176          * correctly.
177          */
178         open fun addAnimations(groups: Collection<Group>) {}
179 
tracknull180         fun track() {
181             // Animations are found in reversed order in the tree,
182             // reverse it back so they are tracked in the order they appear in the code.
183             animations.reversed().forEach(trackAnimation)
184         }
185     }
186 
187     /** Search for animations with type [T]. */
188     open class RememberSearch<T : Any>(private val clazz: KClass<T>, trackAnimation: (T) -> Unit) :
189         Search<T>(trackAnimation) {
addAnimationsnull190         override fun addAnimations(groups: Collection<Group>) {
191             val groupsWithLocation = groups.filter { it.location != null }
192             animations.addAll(groupsWithLocation.findRememberCallWithType(clazz).toSet())
193         }
194 
hasAnimationnull195         override fun hasAnimation(group: Group): Boolean {
196             return group.location != null && group.findRememberCallWithType(clazz) != null
197         }
198 
findRememberCallWithTypenull199         private fun <T : Any> Collection<Group>.findRememberCallWithType(clazz: KClass<T>) =
200             mapNotNull {
201                 it.findRememberCallWithType(clazz)
202             }
203 
findRememberCallWithTypenull204         private fun <T : Any> Group.findRememberCallWithType(clazz: KClass<T>): T? {
205             return clazz.safeCast(
206                 this.data.firstOrNull { data -> data?.javaClass?.kotlin == clazz }
207             )
208         }
209     }
210 
211     class TargetBasedSearch(trackAnimation: (TargetBasedAnimation<*, *>) -> Unit) :
212         RememberSearch<TargetBasedAnimation<*, *>>(TargetBasedAnimation::class, trackAnimation)
213 
214     class DecaySearch(trackAnimation: (DecayAnimation<*, *>) -> Unit) :
215         RememberSearch<DecayAnimation<*, *>>(DecayAnimation::class, trackAnimation)
216 
217     data class InfiniteTransitionSearchInfo(
218         val infiniteTransition: InfiniteTransition,
219         val toolingState: ToolingState<Long>
220     )
221 
222     class InfiniteTransitionSearch(trackAnimation: (InfiniteTransitionSearchInfo) -> Unit) :
223         Search<InfiniteTransitionSearchInfo>(trackAnimation) {
224 
hasAnimationnull225         override fun hasAnimation(group: Group): Boolean {
226             return toAnimationGroup(group)?.let {
227                 group.findData<InfiniteTransition>() != null && findToolingOverride(group) != null
228             } ?: false
229         }
230 
addAnimationsnull231         override fun addAnimations(groups: Collection<Group>) {
232             animations.addAll(findAnimations(groups))
233         }
234 
toAnimationGroupnull235         private fun toAnimationGroup(group: Group): CallGroup? {
236             return group
237                 .takeIf { group.location != null && group.name == REMEMBER_INFINITE_TRANSITION }
238                 ?.let { it as? CallGroup }
239         }
240 
findAnimationsnull241         private fun findAnimations(groups: Collection<Group>): List<InfiniteTransitionSearchInfo> {
242 
243             return groups
244                 .mapNotNull { toAnimationGroup(it) }
245                 .mapNotNull {
246                     val infiniteTransition = it.findData<InfiniteTransition>()
247                     val toolingOverride = findToolingOverride(it)
248                     if (infiniteTransition != null && toolingOverride != null) {
249                         if (toolingOverride.value == null) {
250                             toolingOverride.value = ToolingState(0L)
251                         }
252                         InfiniteTransitionSearchInfo(
253                             infiniteTransition,
254                             (toolingOverride.value as? ToolingState<Long>) ?: ToolingState(0L)
255                         )
256                     } else null
257                 }
258         }
259 
260         /**
261          * InfiniteTransition declares a mutableStateOf<State<Long>?>, starting as null, that we can
262          * use to override the animatable value in Animation Preview. We do that by getting the
263          * [MutableState] from the slot table and directly setting its value. InfiniteTransition
264          * will use the tooling override if this value is not null.
265          */
findToolingOverridenull266         private fun findToolingOverride(group: Group) =
267             group.findData<MutableState<State<Long>?>>(true)
268     }
269 
270     data class AnimateXAsStateSearchInfo<T, V : AnimationVector>(
271         val animatable: Animatable<T, V>,
272         val animationSpec: AnimationSpec<T>,
273         val toolingState: ToolingState<T>
274     )
275 
276     /** Search for animateXAsState() and animateValueAsState() animations. */
277     class AnimateXAsStateSearch(trackAnimation: (AnimateXAsStateSearchInfo<*, *>) -> Unit) :
278         Search<AnimateXAsStateSearchInfo<*, *>>(trackAnimation) {
279 
280         override fun hasAnimation(group: Group): Boolean {
281             return toAnimationGroup(group)?.let {
282                 findAnimatable<Any?>(it) != null &&
283                     findAnimationSpec<Any?>(it) != null &&
284                     findToolingOverride<Any?>(it) != null
285             } ?: false
286         }
287 
288         override fun addAnimations(groups: Collection<Group>) {
289             animations.addAll(findAnimations<Any?>(groups))
290         }
291 
292         /** Find the [Group] containing animation. */
293         private fun toAnimationGroup(group: Group): CallGroup? {
294             return group
295                 .takeIf { it.location != null && it.name == ANIMATE_VALUE_AS_STATE }
296                 ?.let { it as? CallGroup }
297         }
298 
299         private fun <T> findAnimations(
300             groups: Collection<Group>
301         ): List<AnimateXAsStateSearchInfo<T, AnimationVector>> {
302             // How "animateXAsState" calls organized:
303             // Group with name "animateXAsState", for example animateDpAsState, animateIntAsState
304             //    children
305             //    * Group with name "animateValueAsState"
306             //          children
307             //          * Group with name "remember" and data with type Animatable
308             //
309             // To distinguish Animatable within "animateXAsState" calls from other Animatables,
310             // first "animateValueAsState" calls are found.
311             //  Find Animatable within "animateValueAsState" call.
312             return groups
313                 .mapNotNull { toAnimationGroup(it) }
314                 .mapNotNull {
315                     val animatable = findAnimatable<T>(it)
316                     val spec = findAnimationSpec<T>(it)
317                     val toolingOverride = findToolingOverride<T>(it)
318                     if (animatable != null && spec != null && toolingOverride != null) {
319                         if (toolingOverride.value == null) {
320                             toolingOverride.value = ToolingState(animatable.value)
321                         }
322                         AnimateXAsStateSearchInfo(
323                             animatable,
324                             spec,
325                             (toolingOverride.value as? ToolingState<T>)
326                                 ?: ToolingState(animatable.value)
327                         )
328                     } else null
329                 }
330         }
331 
332         /**
333          * animateValueAsState declares a mutableStateOf<State<T>?>, starting as null, that we can
334          * use to override the animatable value in Animation Preview. We do that by getting the
335          * [MutableState] from the slot table and directly setting its value. animateValueAsState
336          * will use the tooling override if this value is not null.
337          */
338         private fun <T> findToolingOverride(group: Group): MutableState<State<T>?>? {
339             return group.findRememberedData<MutableState<State<T>?>>().firstOrNull()
340         }
341 
342         @Suppress("UNCHECKED_CAST")
343         private fun <T> findAnimationSpec(group: CallGroup): AnimationSpec<T>? {
344             val rememberStates = group.children.filter { it.name == REMEMBER_UPDATED_STATE }
345             return (rememberStates + rememberStates.flatMap { it.children })
346                 .flatMap { it.data }
347                 .filterIsInstance<State<T>>()
348                 .map { it.value }
349                 .filterIsInstance<AnimationSpec<T>>()
350                 .firstOrNull()
351         }
352 
353         private fun <T> findAnimatable(group: CallGroup): Animatable<T, AnimationVector>? {
354             return group.findRememberedData<Animatable<T, AnimationVector>>().firstOrNull()
355         }
356     }
357 
358     /** Search for animateContentSize() animations. */
359     class AnimateContentSizeSearch(trackAnimation: (Any) -> Unit) : Search<Any>(trackAnimation) {
360 
hasAnimationnull361         override fun hasAnimation(group: Group): Boolean {
362             return group.modifierInfo.isNotEmpty() &&
363                 group.modifierInfo.any {
364                     it.modifier.any { mod -> mod.javaClass.name == SIZE_ANIMATION_MODIFIER }
365                 }
366         }
367 
368         // It's important not to pre-filter the groups by location, as there's no guarantee
369         // that the group containing the modifierInfo we are looking for has a non-null location.
addAnimationsnull370         override fun addAnimations(groups: Collection<Group>) {
371             groups
372                 .filter { it.modifierInfo.isNotEmpty() }
373                 .forEach { group ->
374                     group.modifierInfo.forEach {
375                         it.modifier.any { mod ->
376                             if (mod.javaClass.name == SIZE_ANIMATION_MODIFIER) {
377                                 animations.add(mod)
378                                 true
379                             } else false
380                         }
381                     }
382                 }
383         }
384     }
385 
386     /** Search for updateTransition() animations. */
387     class TransitionSearch(trackAnimation: (Transition<*>) -> Unit) :
388         Search<Transition<*>>(trackAnimation) {
389 
hasAnimationnull390         override fun hasAnimation(group: Group): Boolean {
391             return toAnimationGroup(group) != null
392         }
393 
addAnimationsnull394         override fun addAnimations(groups: Collection<Group>) {
395             animations.addAll(groups.mapNotNull { toAnimationGroup(it) }.findRememberedData())
396         }
397 
398         /** Find the [Group] containing animation. */
toAnimationGroupnull399         private fun toAnimationGroup(group: Group): Group? {
400             // Find `updateTransition` calls.
401             return group.takeIf { it.location != null && it.name == UPDATE_TRANSITION }
402         }
403     }
404 
405     /** Search for AnimatedVisibility animations. */
406     class AnimatedVisibilitySearch(trackAnimation: (Transition<*>) -> Unit) :
407         Search<Transition<*>>(trackAnimation) {
408 
hasAnimationnull409         override fun hasAnimation(group: Group): Boolean {
410             return toAnimationGroup(group) != null
411         }
412 
addAnimationsnull413         override fun addAnimations(groups: Collection<Group>) {
414             animations.addAll(groups.mapNotNull { toAnimationGroup(it) }.findRememberedData())
415         }
416 
417         /** Find the [Group] containing animation. */
toAnimationGroupnull418         private fun toAnimationGroup(group: Group): Group? {
419             // Find `AnimatedVisibility` call.
420             return group
421                 .takeIf { it.location != null && it.name == ANIMATED_VISIBILITY }
422                 ?.let {
423                     // Then, find the underlying `updateTransition` it uses.
424                     it.children.firstOrNull { updateTransitionCall ->
425                         updateTransitionCall.name == UPDATE_TRANSITION
426                     }
427                 }
428         }
429     }
430 
431     /** Search for AnimatedContent animations. */
432     class AnimatedContentSearch(trackAnimation: (Transition<*>) -> Unit) :
433         Search<Transition<*>>(trackAnimation) {
434 
hasAnimationnull435         override fun hasAnimation(group: Group): Boolean {
436             return toAnimationGroup(group) != null
437         }
438 
addAnimationsnull439         override fun addAnimations(groups: Collection<Group>) {
440             animations.addAll(groups.mapNotNull { toAnimationGroup(it) }.findRememberedData())
441         }
442 
443         /** Find the [Group] containing animation. */
toAnimationGroupnull444         private fun toAnimationGroup(group: Group): Group? {
445             return group
446                 .takeIf { group.location != null && group.name == ANIMATED_CONTENT }
447                 ?.let {
448                     it.children.firstOrNull { updateTransitionCall ->
449                         updateTransitionCall.name == UPDATE_TRANSITION
450                     }
451                 }
452         }
453     }
454 }
455