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