1 /*
2  * Copyright (C) 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 com.android.systemui.dreams
18 
19 import android.animation.Animator
20 import android.animation.AnimatorSet
21 import android.animation.ValueAnimator
22 import android.view.View
23 import android.view.animation.Interpolator
24 import androidx.core.animation.doOnCancel
25 import androidx.core.animation.doOnEnd
26 import androidx.lifecycle.Lifecycle
27 import androidx.lifecycle.repeatOnLifecycle
28 import com.android.app.animation.Interpolators
29 import com.android.dream.lowlight.util.TruncatedInterpolator
30 import com.android.systemui.R
31 import com.android.systemui.complication.ComplicationHostViewController
32 import com.android.systemui.complication.ComplicationLayoutParams
33 import com.android.systemui.complication.ComplicationLayoutParams.POSITION_BOTTOM
34 import com.android.systemui.complication.ComplicationLayoutParams.POSITION_TOP
35 import com.android.systemui.complication.ComplicationLayoutParams.Position
36 import com.android.systemui.dreams.dagger.DreamOverlayModule
37 import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel
38 import com.android.systemui.lifecycle.repeatWhenAttached
39 import com.android.systemui.log.LogBuffer
40 import com.android.systemui.log.core.Logger
41 import com.android.systemui.log.dagger.DreamLog
42 import com.android.systemui.statusbar.BlurUtils
43 import com.android.systemui.statusbar.CrossFadeHelper
44 import com.android.systemui.statusbar.policy.ConfigurationController
45 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener
46 import javax.inject.Inject
47 import javax.inject.Named
48 import kotlinx.coroutines.flow.MutableStateFlow
49 import kotlinx.coroutines.flow.flatMapLatest
50 import kotlinx.coroutines.launch
51 
52 /** Controller for dream overlay animations. */
53 class DreamOverlayAnimationsController
54 @Inject
55 constructor(
56     private val mBlurUtils: BlurUtils,
57     private val mComplicationHostViewController: ComplicationHostViewController,
58     private val mStatusBarViewController: DreamOverlayStatusBarViewController,
59     private val mOverlayStateController: DreamOverlayStateController,
60     @Named(DreamOverlayModule.DREAM_BLUR_RADIUS) private val mDreamBlurRadius: Int,
61     private val transitionViewModel: DreamingToLockscreenTransitionViewModel,
62     private val configController: ConfigurationController,
63     @Named(DreamOverlayModule.DREAM_IN_BLUR_ANIMATION_DURATION)
64     private val mDreamInBlurAnimDurationMs: Long,
65     @Named(DreamOverlayModule.DREAM_IN_COMPLICATIONS_ANIMATION_DURATION)
66     private val mDreamInComplicationsAnimDurationMs: Long,
67     @Named(DreamOverlayModule.DREAM_IN_TRANSLATION_Y_DISTANCE)
68     private val mDreamInTranslationYDistance: Int,
69     @Named(DreamOverlayModule.DREAM_IN_TRANSLATION_Y_DURATION)
70     private val mDreamInTranslationYDurationMs: Long,
71     @DreamLog logBuffer: LogBuffer,
72 ) {
73     companion object {
74         private const val TAG = "DreamOverlayAnimationsController"
75     }
76 
77     private val logger = Logger(logBuffer, TAG)
78 
79     private var mAnimator: Animator? = null
80     private lateinit var view: View
81 
82     /**
83      * Store the current alphas at the various positions. This is so that we may resume an animation
84      * at the current alpha.
85      */
86     private var mCurrentAlphaAtPosition = mutableMapOf<Int, Float>()
87 
88     private var mCurrentBlurRadius: Float = 0f
89 
90     fun init(view: View) {
91         this.view = view
92 
93         view.repeatWhenAttached {
94             val configurationBasedDimensions = MutableStateFlow(loadFromResources(view))
95             val configCallback =
96                 object : ConfigurationListener {
97                     override fun onDensityOrFontScaleChanged() {
98                         configurationBasedDimensions.value = loadFromResources(view)
99                     }
100                 }
101 
102             configController.addCallback(configCallback)
103 
104             repeatOnLifecycle(Lifecycle.State.CREATED) {
105                 /* Translation animations, when moving from DREAMING->LOCKSCREEN state */
106                 launch {
107                     configurationBasedDimensions
108                         .flatMapLatest {
109                             transitionViewModel.dreamOverlayTranslationY(it.translationYPx)
110                         }
111                         .collect { px ->
112                             ComplicationLayoutParams.iteratePositions(
113                                 { position: Int ->
114                                     setElementsTranslationYAtPosition(px, position)
115                                 },
116                                 POSITION_TOP or POSITION_BOTTOM
117                             )
118                         }
119                 }
120 
121                 /* Alpha animations, when moving from DREAMING->LOCKSCREEN state */
122                 launch {
123                     transitionViewModel.dreamOverlayAlpha.collect { alpha ->
124                         ComplicationLayoutParams.iteratePositions(
125                             { position: Int ->
126                                 setElementsAlphaAtPosition(
127                                     alpha = alpha,
128                                     position = position,
129                                     fadingOut = true,
130                                 )
131                             },
132                             POSITION_TOP or POSITION_BOTTOM
133                         )
134                     }
135                 }
136 
137                 launch {
138                     transitionViewModel.transitionEnded.collect { _ ->
139                         mOverlayStateController.setExitAnimationsRunning(false)
140                     }
141                 }
142             }
143 
144             configController.removeCallback(configCallback)
145         }
146     }
147 
148     /**
149      * Starts the dream content and dream overlay entry animations.
150      *
151      * @param downwards if true, the entry animation translations downwards into position rather
152      *   than upwards.
153      */
154     @JvmOverloads
155     fun startEntryAnimations(
156         downwards: Boolean,
157         animatorBuilder: () -> AnimatorSet = { AnimatorSet() }
158     ) {
159         cancelAnimations()
160 
161         mAnimator =
162             animatorBuilder().apply {
163                 playTogether(
164                     blurAnimator(
165                         view = view,
166                         fromBlurRadius = mDreamBlurRadius.toFloat(),
167                         toBlurRadius = 0f,
168                         durationMs = mDreamInBlurAnimDurationMs,
169                         interpolator = Interpolators.EMPHASIZED_DECELERATE
170                     ),
171                     alphaAnimator(
172                         from = 0f,
173                         to = 1f,
174                         durationMs = mDreamInComplicationsAnimDurationMs,
175                         interpolator = Interpolators.LINEAR
176                     ),
177                     translationYAnimator(
178                         from = mDreamInTranslationYDistance.toFloat() * (if (downwards) -1 else 1),
179                         to = 0f,
180                         durationMs = mDreamInTranslationYDurationMs,
181                         interpolator = Interpolators.EMPHASIZED_DECELERATE
182                     ),
183                 )
184                 doOnEnd {
185                     mAnimator = null
186                     mOverlayStateController.setEntryAnimationsFinished(true)
187                     logger.d("Dream overlay entry animations finished.")
188                 }
189                 doOnCancel { logger.d("Dream overlay entry animations canceled.") }
190                 start()
191                 logger.d("Dream overlay entry animations started.")
192             }
193     }
194 
195     /**
196      * Starts the dream content and dream overlay exit animations.
197      *
198      * This should only be used when the low light dream is entering, animations to/from other SysUI
199      * views is controlled by `transitionViewModel`.
200      */
201     // TODO(b/256916668): integrate with the keyguard transition model once dream surfaces work is
202     // done.
203     @JvmOverloads
204     fun startExitAnimations(animatorBuilder: () -> AnimatorSet = { AnimatorSet() }): Animator {
205         cancelAnimations()
206 
207         mAnimator =
208             animatorBuilder().apply {
209                 playTogether(
210                     translationYAnimator(
211                         from = 0f,
212                         to = -mDreamInTranslationYDistance.toFloat(),
213                         durationMs = mDreamInComplicationsAnimDurationMs,
214                         delayMs = 0,
215                         // Truncate the animation from the full duration to match the alpha
216                         // animation so that the whole animation ends at the same time.
217                         interpolator =
218                             TruncatedInterpolator(
219                                 Interpolators.EMPHASIZED,
220                                 /*originalDuration=*/ mDreamInTranslationYDurationMs.toFloat(),
221                                 /*newDuration=*/ mDreamInComplicationsAnimDurationMs.toFloat()
222                             )
223                     ),
224                     alphaAnimator(
225                         from =
226                             mCurrentAlphaAtPosition.getOrDefault(
227                                 key = POSITION_BOTTOM,
228                                 defaultValue = 1f
229                             ),
230                         to = 0f,
231                         durationMs = mDreamInComplicationsAnimDurationMs,
232                         delayMs = 0,
233                         positions = POSITION_BOTTOM
234                     ),
235                     alphaAnimator(
236                         from =
237                             mCurrentAlphaAtPosition.getOrDefault(
238                                 key = POSITION_TOP,
239                                 defaultValue = 1f
240                             ),
241                         to = 0f,
242                         durationMs = mDreamInComplicationsAnimDurationMs,
243                         delayMs = 0,
244                         positions = POSITION_TOP
245                     )
246                 )
247                 doOnEnd {
248                     mAnimator = null
249                     mOverlayStateController.setExitAnimationsRunning(false)
250                     logger.d("Dream overlay exit animations finished.")
251                 }
252                 doOnCancel { logger.d("Dream overlay exit animations canceled.") }
253                 start()
254                 logger.d("Dream overlay exit animations started.")
255             }
256         mOverlayStateController.setExitAnimationsRunning(true)
257         return mAnimator as AnimatorSet
258     }
259 
260     /** Starts the dream content and dream overlay exit animations. */
261     fun wakeUp() {
262         cancelAnimations()
263         mOverlayStateController.setExitAnimationsRunning(true)
264     }
265 
266     /** Cancels the dream content and dream overlay animations, if they're currently running. */
267     fun cancelAnimations() {
268         mAnimator =
269             mAnimator?.let {
270                 it.cancel()
271                 null
272             }
273     }
274 
275     private fun blurAnimator(
276         view: View,
277         fromBlurRadius: Float,
278         toBlurRadius: Float,
279         durationMs: Long,
280         delayMs: Long = 0,
281         interpolator: Interpolator = Interpolators.LINEAR
282     ): Animator {
283         return ValueAnimator.ofFloat(fromBlurRadius, toBlurRadius).apply {
284             duration = durationMs
285             startDelay = delayMs
286             this.interpolator = interpolator
287             addUpdateListener { animator: ValueAnimator ->
288                 mCurrentBlurRadius = animator.animatedValue as Float
289                 mBlurUtils.applyBlur(
290                     viewRootImpl = view.viewRootImpl,
291                     radius = mCurrentBlurRadius.toInt(),
292                     opaque = false
293                 )
294             }
295         }
296     }
297 
298     private fun alphaAnimator(
299         from: Float,
300         to: Float,
301         durationMs: Long,
302         delayMs: Long = 0,
303         @Position positions: Int = POSITION_TOP or POSITION_BOTTOM,
304         interpolator: Interpolator = Interpolators.LINEAR
305     ): Animator {
306         return ValueAnimator.ofFloat(from, to).apply {
307             duration = durationMs
308             startDelay = delayMs
309             this.interpolator = interpolator
310             addUpdateListener { va: ValueAnimator ->
311                 ComplicationLayoutParams.iteratePositions(
312                     { position: Int ->
313                         setElementsAlphaAtPosition(
314                             alpha = va.animatedValue as Float,
315                             position = position,
316                             fadingOut = to < from
317                         )
318                     },
319                     positions
320                 )
321             }
322         }
323     }
324 
325     private fun translationYAnimator(
326         from: Float,
327         to: Float,
328         durationMs: Long,
329         delayMs: Long = 0,
330         @Position positions: Int = POSITION_TOP or POSITION_BOTTOM,
331         interpolator: Interpolator = Interpolators.LINEAR
332     ): Animator {
333         return ValueAnimator.ofFloat(from, to).apply {
334             duration = durationMs
335             startDelay = delayMs
336             this.interpolator = interpolator
337             addUpdateListener { va: ValueAnimator ->
338                 ComplicationLayoutParams.iteratePositions(
339                     { position: Int ->
340                         setElementsTranslationYAtPosition(va.animatedValue as Float, position)
341                     },
342                     positions
343                 )
344             }
345         }
346     }
347 
348     /** Sets alpha of complications at the specified position. */
349     private fun setElementsAlphaAtPosition(alpha: Float, position: Int, fadingOut: Boolean) {
350         mCurrentAlphaAtPosition[position] = alpha
351         mComplicationHostViewController.getViewsAtPosition(position).forEach { view ->
352             if (fadingOut) {
353                 CrossFadeHelper.fadeOut(view, 1 - alpha, /* remap= */ false)
354             } else {
355                 CrossFadeHelper.fadeIn(view, alpha, /* remap= */ false)
356             }
357         }
358         if (position == POSITION_TOP) {
359             mStatusBarViewController.setFadeAmount(alpha, fadingOut)
360         }
361     }
362 
363     /** Sets y translation of complications at the specified position. */
364     private fun setElementsTranslationYAtPosition(translationY: Float, position: Int) {
365         mComplicationHostViewController.getViewsAtPosition(position).forEach { v ->
366             v.translationY = translationY
367         }
368         if (position == POSITION_TOP) {
369             mStatusBarViewController.setTranslationY(translationY)
370         }
371     }
372 
373     private fun loadFromResources(view: View): ConfigurationBasedDimensions {
374         return ConfigurationBasedDimensions(
375             translationYPx =
376                 view.resources.getDimensionPixelSize(R.dimen.dream_overlay_exit_y_offset),
377         )
378     }
379 
380     private data class ConfigurationBasedDimensions(
381         val translationYPx: Int,
382     )
383 }
384