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