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