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