1 /*
<lambda>null2  * Copyright 2020 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 android.util.Log
20 import androidx.annotation.VisibleForTesting
21 import androidx.compose.animation.core.DecayAnimation
22 import androidx.compose.animation.core.TargetBasedAnimation
23 import androidx.compose.animation.core.Transition
24 import androidx.compose.animation.tooling.ComposeAnimatedProperty
25 import androidx.compose.animation.tooling.ComposeAnimation
26 import androidx.compose.animation.tooling.TransitionInfo
27 import androidx.compose.ui.tooling.animation.AnimateXAsStateComposeAnimation.Companion.parse
28 import androidx.compose.ui.tooling.animation.AnimatedContentComposeAnimation.Companion.parseAnimatedContent
29 import androidx.compose.ui.tooling.animation.InfiniteTransitionComposeAnimation.Companion.parse
30 import androidx.compose.ui.tooling.animation.clock.AnimateXAsStateClock
31 import androidx.compose.ui.tooling.animation.clock.AnimatedVisibilityClock
32 import androidx.compose.ui.tooling.animation.clock.ComposeAnimationClock
33 import androidx.compose.ui.tooling.animation.clock.InfiniteTransitionClock
34 import androidx.compose.ui.tooling.animation.clock.TransitionClock
35 import androidx.compose.ui.tooling.animation.clock.millisToNanos
36 import androidx.compose.ui.tooling.animation.states.AnimatedVisibilityState
37 import androidx.compose.ui.tooling.animation.states.TargetState
38 
39 /**
40  * Used to keep track and control animations in the context of Compose Previews. This class is
41  * expected to be controlled by the Animation Preview in Android Studio, and most of its methods
42  * will be called via reflection, either directly from Android Studio or through
43  * `ComposeViewAdapter`.
44  *
45  * Methods to be intercepted in Android Studio:
46  * * [notifySubscribe]
47  * * [notifyUnsubscribe]
48  *
49  * Methods to be called from Android Studio:
50  * * [updateFromAndToStates]
51  * * [updateAnimatedVisibilityState]
52  * * [getAnimatedVisibilityState]
53  * * [getMaxDuration]
54  * * [getMaxDurationPerIteration]
55  * * [getAnimatedProperties]
56  * * [getTransitions]
57  * * [setClockTime]
58  * * [setClockTimes]
59  */
60 internal open class PreviewAnimationClock(private val setAnimationsTimeCallback: () -> Unit = {}) {
61 
62     private val TAG = "PreviewAnimationClock"
63 
64     private val DEBUG = false
65 
66     /** Map of subscribed [TransitionComposeAnimation]s and corresponding [TransitionClock]s. */
67     @VisibleForTesting
68     internal val transitionClocks =
69         mutableMapOf<TransitionComposeAnimation<*>, TransitionClock<*>>()
70 
71     /**
72      * Map of subscribed [AnimatedVisibilityComposeAnimation]s and corresponding
73      * [AnimatedVisibilityClock].
74      */
75     @VisibleForTesting
76     internal val animatedVisibilityClocks =
77         mutableMapOf<AnimatedVisibilityComposeAnimation, AnimatedVisibilityClock>()
78 
79     /**
80      * Map of subscribed [AnimateXAsStateComposeAnimation]s and corresponding
81      * [AnimateXAsStateClock]s.
82      */
83     @VisibleForTesting
84     internal val animateXAsStateClocks =
85         mutableMapOf<AnimateXAsStateComposeAnimation<*, *>, AnimateXAsStateClock<*, *>>()
86 
87     /**
88      * Map of subscribed [InfiniteTransitionComposeAnimation]s and corresponding
89      * [InfiniteTransitionClock]s.
90      */
91     @VisibleForTesting
92     internal val infiniteTransitionClocks =
93         mutableMapOf<InfiniteTransitionComposeAnimation, InfiniteTransitionClock>()
94 
95     /**
96      * Map of subscribed [AnimatedContentComposeAnimation]s and corresponding [TransitionClock]s.
97      */
98     @VisibleForTesting
99     internal val animatedContentClocks =
100         mutableMapOf<AnimatedContentComposeAnimation<*>, TransitionClock<*>>()
101 
102     private val allClocksExceptInfinite: List<ComposeAnimationClock<*, *>>
103         get() =
104             transitionClocks.values +
105                 animatedVisibilityClocks.values +
106                 animateXAsStateClocks.values +
107                 animatedContentClocks.values
108 
109     /** All subscribed animations clocks. */
110     private val allClocks: List<ComposeAnimationClock<*, *>>
111         get() = allClocksExceptInfinite + infiniteTransitionClocks.values
112 
findClocknull113     private fun findClock(animation: ComposeAnimation): ComposeAnimationClock<*, *>? {
114         return transitionClocks[animation]
115             ?: animatedVisibilityClocks[animation]
116             ?: animateXAsStateClocks[animation]
117             ?: infiniteTransitionClocks[animation]
118             ?: animatedContentClocks[animation]
119     }
120 
trackTransitionnull121     fun trackTransition(animation: Transition<*>) {
122         trackAnimation(animation) {
123             animation.parse()?.let {
124                 transitionClocks[it] = TransitionClock(it)
125                 notifySubscribe(it)
126                 return@trackAnimation
127             }
128 
129             // If for some reason animation couldn't be parsed, track it as unsupported.
130             createUnsupported(animation.label)
131         }
132     }
133 
134     @Suppress("UNCHECKED_CAST")
<lambda>null135     fun trackAnimatedVisibility(animation: Transition<*>, onSeek: () -> Unit = {}) {
136         // All AnimatedVisibility animations should be Transition<Boolean>.
137         // If it's not the case - ignore it.
138         if (animation.currentState !is Boolean) return
<lambda>null139         trackAnimation(animation) {
140             animation as Transition<Boolean>
141             val composeAnimation = animation.parseAnimatedVisibility()
142             onSeek()
143             animatedVisibilityClocks[composeAnimation] =
144                 AnimatedVisibilityClock(composeAnimation).apply { setClockTime(0L) }
145             notifySubscribe(composeAnimation)
146         }
147     }
148 
trackAnimateXAsStatenull149     fun trackAnimateXAsState(animation: AnimationSearch.AnimateXAsStateSearchInfo<*, *>) {
150         trackAnimation(animation.animatable) {
151             animation.parse()?.let {
152                 animateXAsStateClocks[it] = AnimateXAsStateClock(it)
153                 notifySubscribe(it)
154                 return@trackAnimation
155             }
156 
157             // If for some reason animation couldn't be parsed, track it as unsupported.
158             createUnsupported(animation.animatable.label)
159         }
160     }
161 
trackAnimateContentSizenull162     fun trackAnimateContentSize(animation: Any) {
163         trackUnsupported(animation, "animateContentSize")
164     }
165 
trackTargetBasedAnimationsnull166     fun trackTargetBasedAnimations(animation: TargetBasedAnimation<*, *>) {
167         trackUnsupported(animation, "TargetBasedAnimation")
168     }
169 
trackDecayAnimationsnull170     fun trackDecayAnimations(animation: DecayAnimation<*, *>) {
171         trackUnsupported(animation, "DecayAnimation")
172     }
173 
trackAnimatedContentnull174     fun trackAnimatedContent(animation: Transition<*>) {
175         trackAnimation(animation) {
176             animation.parseAnimatedContent()?.let {
177                 animatedContentClocks[it] = TransitionClock(it)
178                 notifySubscribe(it)
179                 return@trackAnimation
180             }
181             // If for some reason animation couldn't be parsed, track it as unsupported.
182             createUnsupported(animation.label)
183         }
184     }
185 
trackInfiniteTransitionnull186     fun trackInfiniteTransition(animation: AnimationSearch.InfiniteTransitionSearchInfo) {
187         trackAnimation(animation.infiniteTransition) {
188             animation.parse()?.let {
189                 infiniteTransitionClocks[it] =
190                     InfiniteTransitionClock(it) {
191                         // Let InfiniteTransitionClock be aware about max duration of other
192                         // animations.
193                         val otherClockMaxDuration =
194                             allClocksExceptInfinite.maxOfOrNull { clock -> clock.getMaxDuration() }
195                                 ?: 0
196                         val infiniteMaxDurationPerIteration =
197                             infiniteTransitionClocks.values.maxOfOrNull { clock ->
198                                 clock.getMaxDurationPerIteration()
199                             } ?: 0
200                         maxOf(otherClockMaxDuration, infiniteMaxDurationPerIteration)
201                     }
202                 notifySubscribe(it)
203             }
204         }
205     }
206 
207     @VisibleForTesting val trackedUnsupportedAnimations = linkedSetOf<UnsupportedComposeAnimation>()
208 
trackUnsupportednull209     private fun trackUnsupported(animation: Any, label: String) {
210         trackAnimation(animation) { createUnsupported(label) }
211     }
212 
createUnsupportednull213     private fun createUnsupported(label: String?) {
214         UnsupportedComposeAnimation.create(label)?.let {
215             trackedUnsupportedAnimations.add(it)
216             notifySubscribe(it)
217         }
218     }
219 
220     /** Tracked animations. */
221     private val trackedAnimations = linkedSetOf<Any>()
222     private val lock = Any()
223 
trackAnimationnull224     private fun trackAnimation(animation: Any, createClockAndSubscribe: (Any) -> Unit): Boolean {
225         synchronized(lock) {
226             if (trackedAnimations.contains(animation)) {
227                 if (DEBUG) {
228                     Log.d(TAG, "Animation $animation is already being tracked")
229                 }
230                 return false
231             }
232             trackedAnimations.add(animation)
233         }
234 
235         createClockAndSubscribe(animation)
236 
237         if (DEBUG) {
238             Log.d(TAG, "Animation $animation is now tracked")
239         }
240 
241         return true
242     }
243 
244     @VisibleForTesting
notifySubscribenull245     protected open fun notifySubscribe(animation: ComposeAnimation) {
246         // This method is expected to be no-op. It is intercepted in Android Studio using bytecode
247         // manipulation, in order for the tools to be aware that the animation is now tracked.
248     }
249 
250     @VisibleForTesting
notifyUnsubscribenull251     protected open fun notifyUnsubscribe(animation: ComposeAnimation) {
252         // This method is expected to be no-op. It is intercepted in Android Studio using bytecode
253         // manipulation, in order for the tools to be aware that the animation is no longer
254         // tracked.
255     }
256 
257     /**
258      * Updates the [TargetState] corresponding to the given [ComposeAnimation].
259      *
260      * Expected to be called via reflection from Android Studio.
261      */
updateFromAndToStatesnull262     fun updateFromAndToStates(composeAnimation: ComposeAnimation, fromState: Any, toState: Any) {
263         findClock(composeAnimation)?.setStateParameters(fromState, toState)
264     }
265 
266     /**
267      * Updates the given [AnimatedVisibilityClock]'s with the given state.
268      *
269      * Expected to be called via reflection from Android Studio.
270      */
updateAnimatedVisibilityStatenull271     fun updateAnimatedVisibilityState(composeAnimation: ComposeAnimation, state: Any) {
272         animatedVisibilityClocks[composeAnimation]?.setStateParameters(state)
273     }
274 
275     /**
276      * Returns the [AnimatedVisibilityState] corresponding to the given [AnimatedVisibilityClock]
277      * object. Falls back to [AnimatedVisibilityState.Enter].
278      *
279      * Expected to be called via reflection from Android Studio.
280      */
getAnimatedVisibilityStatenull281     fun getAnimatedVisibilityState(composeAnimation: ComposeAnimation): AnimatedVisibilityState {
282         return animatedVisibilityClocks[composeAnimation]?.state ?: AnimatedVisibilityState.Enter
283     }
284 
285     /**
286      * Returns the duration (ms) of the longest animation being tracked.
287      *
288      * Expected to be called via reflection from Android Studio.
289      */
getMaxDurationnull290     fun getMaxDuration(): Long {
291         return allClocks.maxOfOrNull { it.getMaxDuration() } ?: 0
292     }
293 
294     /**
295      * Returns the longest duration (ms) per iteration among the animations being tracked. This can
296      * be different from [getMaxDuration], for instance, when there is one or more repeatable
297      * animations with multiple iterations.
298      *
299      * Expected to be called via reflection from Android Studio.
300      */
getMaxDurationPerIterationnull301     fun getMaxDurationPerIteration(): Long {
302         return allClocks.maxOfOrNull { it.getMaxDurationPerIteration() } ?: 0
303     }
304 
305     /**
306      * Returns a list of the given [ComposeAnimation]'s animated properties. The properties are
307      * wrapped into a [ComposeAnimatedProperty] object containing the property label and the
308      * corresponding value at the current time.
309      *
310      * Expected to be called via reflection from Android Studio.
311      */
getAnimatedPropertiesnull312     fun getAnimatedProperties(animation: ComposeAnimation): List<ComposeAnimatedProperty> {
313         return findClock(animation)?.getAnimatedProperties() ?: emptyList()
314     }
315 
316     /**
317      * Returns a list of the given [ComposeAnimation]'s animated properties. The properties are
318      * wrapped into a [TransitionInfo] object containing the property label, start and time of
319      * animation and values of the animation.
320      *
321      * Expected to be called via reflection from Android Studio.
322      */
getTransitionsnull323     fun getTransitions(animation: ComposeAnimation, stepMillis: Long): List<TransitionInfo> {
324         return findClock(animation)?.getTransitions(stepMillis) ?: emptyList()
325     }
326 
327     /**
328      * Seeks each animation being tracked to the given [animationTimeMillis].
329      *
330      * Expected to be called via reflection from Android Studio.
331      */
setClockTimenull332     fun setClockTime(animationTimeMillis: Long) {
333         val timeNanos = millisToNanos(animationTimeMillis)
334         allClocks.forEach { it.setClockTime(timeNanos) }
335         setAnimationsTimeCallback.invoke()
336     }
337 
338     /**
339      * Seeks each animation being tracked to the given [animationTimeMillis].
340      *
341      * Expected to be called via reflection from Android Studio.
342      */
setClockTimesnull343     fun setClockTimes(animationTimeMillis: Map<ComposeAnimation, Long>) {
344         animationTimeMillis.forEach { (composeAnimation, millis) ->
345             findClock(composeAnimation)?.setClockTime(millisToNanos(millis))
346         }
347         setAnimationsTimeCallback.invoke()
348     }
349 
350     /** Unsubscribes the currently tracked animations and clears all the caches. */
disposenull351     fun dispose() {
352         allClocks.forEach { notifyUnsubscribe(it.animation) }
353         trackedUnsupportedAnimations.forEach { notifyUnsubscribe(it) }
354         trackedUnsupportedAnimations.clear()
355         transitionClocks.clear()
356         animatedVisibilityClocks.clear()
357         trackedAnimations.clear()
358     }
359 }
360