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