1 /* <lambda>null2 * Copyright (C) 2023 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.launcher3.util 18 19 import android.animation.AnimatorSet 20 import android.animation.ValueAnimator 21 import android.util.Log 22 import android.view.ViewTreeObserver.OnGlobalLayoutListener 23 import androidx.core.view.OneShotPreDrawListener 24 import com.android.app.animation.Interpolators.LINEAR 25 import com.android.launcher3.anim.AnimatorListeners 26 import com.android.launcher3.anim.AnimatorPlaybackController 27 import com.android.launcher3.anim.PendingAnimation 28 import com.android.launcher3.statemanager.StatefulActivity 29 import com.android.launcher3.states.StateAnimationConfig.HANDLE_STATE_APPLY 30 import com.android.launcher3.states.StateAnimationConfig.USER_CONTROLLED 31 import java.util.function.Consumer 32 33 private const val TAG = "CannedAnimCoordinator" 34 35 /** 36 * Utility class to run a canned animation on Launcher. 37 * 38 * This class takes care to registering animations with stateManager and ensures that only one 39 * animation is playing at a time. 40 */ 41 class CannedAnimationCoordinator(private val activity: StatefulActivity<*>) { 42 43 private val launcherLayoutListener = OnGlobalLayoutListener { scheduleRecreateAnimOnPreDraw() } 44 private var recreatePending = false 45 46 private var animationProvider: Any? = null 47 48 private var animationDuration: Long = 0L 49 private var animationFactory: Consumer<PendingAnimation>? = null 50 private var animationController: AnimatorPlaybackController? = null 51 52 private var currentAnim: AnimatorPlaybackController? = null 53 54 /** 55 * Sets the current animation cancelling any previously set animation. 56 * 57 * Callers can control the animation using {@link #getPlaybackController}. The state is 58 * automatically cleared when the playback controller ends. The animation is automatically 59 * recreated when any layout change happens. Callers can also ask for recreation by calling 60 * {@link #recreateAnimation} 61 */ 62 fun setAnimation(provider: Any, factory: Consumer<PendingAnimation>, duration: Long) { 63 if (provider != animationProvider) { 64 Log.e(TAG, "Trying to play two animations together, $provider and $animationProvider") 65 } 66 67 // Cancel any previously running animation 68 endCurrentAnimation(false) 69 animationController?.dispatchOnCancel()?.dispatchOnEnd() 70 71 animationProvider = provider 72 animationFactory = factory 73 animationDuration = duration 74 75 // Setup a new controller and link it with launcher state animation 76 val anim = AnimatorSet() 77 anim.play( 78 ValueAnimator.ofFloat(0f, 1f).apply { 79 interpolator = LINEAR 80 this.duration = duration 81 addUpdateListener { anim -> currentAnim?.setPlayFraction(anim.animatedFraction) } 82 } 83 ) 84 val controller = AnimatorPlaybackController.wrap(anim, duration) 85 anim.addListener( 86 AnimatorListeners.forEndCallback { success -> 87 if (animationController != controller) { 88 return@forEndCallback 89 } 90 91 endCurrentAnimation(success) 92 animationController = null 93 animationFactory = null 94 animationProvider = null 95 96 activity.rootView.viewTreeObserver.apply { 97 if (isAlive) { 98 removeOnGlobalLayoutListener(launcherLayoutListener) 99 } 100 } 101 } 102 ) 103 104 // Recreate animation whenever layout happens in case transforms change during layout 105 activity.rootView.viewTreeObserver.apply { 106 if (isAlive) { 107 addOnGlobalLayoutListener(launcherLayoutListener) 108 } 109 } 110 // Link this to the state manager so that it auto-cancels when state changes 111 recreatePending = false 112 // Animator coordinator takes care of reapplying the animation due to state reset. Set the 113 // flags accordingly 114 animationController = 115 controller.apply { 116 activity 117 .stateManager 118 .setCurrentAnimation(this, USER_CONTROLLED or HANDLE_STATE_APPLY) 119 } 120 recreateAnimation(provider) 121 } 122 123 private fun endCurrentAnimation(success: Boolean) { 124 currentAnim?.apply { 125 // When cancelling an animation, apply final progress so that all transformations 126 // are restored 127 setPlayFraction(1f) 128 if (!success) dispatchOnCancel() 129 dispatchOnEnd() 130 } 131 currentAnim = null 132 } 133 134 /** Returns the current animation controller to control the animation */ 135 fun getPlaybackController(provider: Any): AnimatorPlaybackController? { 136 return if (provider == animationProvider) animationController 137 else { 138 Log.d(TAG, "Wrong controller access from $provider, actual provider $animationProvider") 139 null 140 } 141 } 142 143 private fun scheduleRecreateAnimOnPreDraw() { 144 if (!recreatePending) { 145 recreatePending = true 146 OneShotPreDrawListener.add(activity.rootView) { 147 if (recreatePending) { 148 recreatePending = false 149 animationProvider?.apply { recreateAnimation(this) } 150 } 151 } 152 } 153 } 154 155 /** Notify the controller to recreate the animation. The animation progress is preserved */ 156 fun recreateAnimation(provider: Any) { 157 if (provider != animationProvider) { 158 Log.e(TAG, "Ignore recreate request from $provider, actual provider $animationProvider") 159 return 160 } 161 endCurrentAnimation(false /* success */) 162 163 if (animationFactory == null || animationController == null) { 164 return 165 } 166 currentAnim = 167 PendingAnimation(animationDuration) 168 .apply { animationFactory?.accept(this) } 169 .createPlaybackController() 170 .apply { setPlayFraction(animationController!!.progressFraction) } 171 } 172 } 173