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