/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.dreams import android.animation.Animator import android.animation.AnimatorSet import android.animation.ValueAnimator import android.view.View import android.view.animation.Interpolator import androidx.core.animation.doOnCancel import androidx.core.animation.doOnEnd import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.app.animation.Interpolators import com.android.dream.lowlight.util.TruncatedInterpolator import com.android.systemui.R import com.android.systemui.complication.ComplicationHostViewController import com.android.systemui.complication.ComplicationLayoutParams import com.android.systemui.complication.ComplicationLayoutParams.POSITION_BOTTOM import com.android.systemui.complication.ComplicationLayoutParams.POSITION_TOP import com.android.systemui.complication.ComplicationLayoutParams.Position import com.android.systemui.dreams.dagger.DreamOverlayModule import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.log.dagger.DreamLog import com.android.systemui.statusbar.BlurUtils import com.android.systemui.statusbar.CrossFadeHelper import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener import javax.inject.Inject import javax.inject.Named import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.launch /** Controller for dream overlay animations. */ class DreamOverlayAnimationsController @Inject constructor( private val mBlurUtils: BlurUtils, private val mComplicationHostViewController: ComplicationHostViewController, private val mStatusBarViewController: DreamOverlayStatusBarViewController, private val mOverlayStateController: DreamOverlayStateController, @Named(DreamOverlayModule.DREAM_BLUR_RADIUS) private val mDreamBlurRadius: Int, private val transitionViewModel: DreamingToLockscreenTransitionViewModel, private val configController: ConfigurationController, @Named(DreamOverlayModule.DREAM_IN_BLUR_ANIMATION_DURATION) private val mDreamInBlurAnimDurationMs: Long, @Named(DreamOverlayModule.DREAM_IN_COMPLICATIONS_ANIMATION_DURATION) private val mDreamInComplicationsAnimDurationMs: Long, @Named(DreamOverlayModule.DREAM_IN_TRANSLATION_Y_DISTANCE) private val mDreamInTranslationYDistance: Int, @Named(DreamOverlayModule.DREAM_IN_TRANSLATION_Y_DURATION) private val mDreamInTranslationYDurationMs: Long, @DreamLog logBuffer: LogBuffer, ) { companion object { private const val TAG = "DreamOverlayAnimationsController" } private val logger = Logger(logBuffer, TAG) private var mAnimator: Animator? = null private lateinit var view: View /** * Store the current alphas at the various positions. This is so that we may resume an animation * at the current alpha. */ private var mCurrentAlphaAtPosition = mutableMapOf() private var mCurrentBlurRadius: Float = 0f fun init(view: View) { this.view = view view.repeatWhenAttached { val configurationBasedDimensions = MutableStateFlow(loadFromResources(view)) val configCallback = object : ConfigurationListener { override fun onDensityOrFontScaleChanged() { configurationBasedDimensions.value = loadFromResources(view) } } configController.addCallback(configCallback) repeatOnLifecycle(Lifecycle.State.CREATED) { /* Translation animations, when moving from DREAMING->LOCKSCREEN state */ launch { configurationBasedDimensions .flatMapLatest { transitionViewModel.dreamOverlayTranslationY(it.translationYPx) } .collect { px -> ComplicationLayoutParams.iteratePositions( { position: Int -> setElementsTranslationYAtPosition(px, position) }, POSITION_TOP or POSITION_BOTTOM ) } } /* Alpha animations, when moving from DREAMING->LOCKSCREEN state */ launch { transitionViewModel.dreamOverlayAlpha.collect { alpha -> ComplicationLayoutParams.iteratePositions( { position: Int -> setElementsAlphaAtPosition( alpha = alpha, position = position, fadingOut = true, ) }, POSITION_TOP or POSITION_BOTTOM ) } } launch { transitionViewModel.transitionEnded.collect { _ -> mOverlayStateController.setExitAnimationsRunning(false) } } } configController.removeCallback(configCallback) } } /** * Starts the dream content and dream overlay entry animations. * * @param downwards if true, the entry animation translations downwards into position rather * than upwards. */ @JvmOverloads fun startEntryAnimations( downwards: Boolean, animatorBuilder: () -> AnimatorSet = { AnimatorSet() } ) { cancelAnimations() mAnimator = animatorBuilder().apply { playTogether( blurAnimator( view = view, fromBlurRadius = mDreamBlurRadius.toFloat(), toBlurRadius = 0f, durationMs = mDreamInBlurAnimDurationMs, interpolator = Interpolators.EMPHASIZED_DECELERATE ), alphaAnimator( from = 0f, to = 1f, durationMs = mDreamInComplicationsAnimDurationMs, interpolator = Interpolators.LINEAR ), translationYAnimator( from = mDreamInTranslationYDistance.toFloat() * (if (downwards) -1 else 1), to = 0f, durationMs = mDreamInTranslationYDurationMs, interpolator = Interpolators.EMPHASIZED_DECELERATE ), ) doOnEnd { mAnimator = null mOverlayStateController.setEntryAnimationsFinished(true) logger.d("Dream overlay entry animations finished.") } doOnCancel { logger.d("Dream overlay entry animations canceled.") } start() logger.d("Dream overlay entry animations started.") } } /** * Starts the dream content and dream overlay exit animations. * * This should only be used when the low light dream is entering, animations to/from other SysUI * views is controlled by `transitionViewModel`. */ // TODO(b/256916668): integrate with the keyguard transition model once dream surfaces work is // done. @JvmOverloads fun startExitAnimations(animatorBuilder: () -> AnimatorSet = { AnimatorSet() }): Animator { cancelAnimations() mAnimator = animatorBuilder().apply { playTogether( translationYAnimator( from = 0f, to = -mDreamInTranslationYDistance.toFloat(), durationMs = mDreamInComplicationsAnimDurationMs, delayMs = 0, // Truncate the animation from the full duration to match the alpha // animation so that the whole animation ends at the same time. interpolator = TruncatedInterpolator( Interpolators.EMPHASIZED, /*originalDuration=*/ mDreamInTranslationYDurationMs.toFloat(), /*newDuration=*/ mDreamInComplicationsAnimDurationMs.toFloat() ) ), alphaAnimator( from = mCurrentAlphaAtPosition.getOrDefault( key = POSITION_BOTTOM, defaultValue = 1f ), to = 0f, durationMs = mDreamInComplicationsAnimDurationMs, delayMs = 0, positions = POSITION_BOTTOM ), alphaAnimator( from = mCurrentAlphaAtPosition.getOrDefault( key = POSITION_TOP, defaultValue = 1f ), to = 0f, durationMs = mDreamInComplicationsAnimDurationMs, delayMs = 0, positions = POSITION_TOP ) ) doOnEnd { mAnimator = null mOverlayStateController.setExitAnimationsRunning(false) logger.d("Dream overlay exit animations finished.") } doOnCancel { logger.d("Dream overlay exit animations canceled.") } start() logger.d("Dream overlay exit animations started.") } mOverlayStateController.setExitAnimationsRunning(true) return mAnimator as AnimatorSet } /** Starts the dream content and dream overlay exit animations. */ fun wakeUp() { cancelAnimations() mOverlayStateController.setExitAnimationsRunning(true) } /** Cancels the dream content and dream overlay animations, if they're currently running. */ fun cancelAnimations() { mAnimator = mAnimator?.let { it.cancel() null } } private fun blurAnimator( view: View, fromBlurRadius: Float, toBlurRadius: Float, durationMs: Long, delayMs: Long = 0, interpolator: Interpolator = Interpolators.LINEAR ): Animator { return ValueAnimator.ofFloat(fromBlurRadius, toBlurRadius).apply { duration = durationMs startDelay = delayMs this.interpolator = interpolator addUpdateListener { animator: ValueAnimator -> mCurrentBlurRadius = animator.animatedValue as Float mBlurUtils.applyBlur( viewRootImpl = view.viewRootImpl, radius = mCurrentBlurRadius.toInt(), opaque = false ) } } } private fun alphaAnimator( from: Float, to: Float, durationMs: Long, delayMs: Long = 0, @Position positions: Int = POSITION_TOP or POSITION_BOTTOM, interpolator: Interpolator = Interpolators.LINEAR ): Animator { return ValueAnimator.ofFloat(from, to).apply { duration = durationMs startDelay = delayMs this.interpolator = interpolator addUpdateListener { va: ValueAnimator -> ComplicationLayoutParams.iteratePositions( { position: Int -> setElementsAlphaAtPosition( alpha = va.animatedValue as Float, position = position, fadingOut = to < from ) }, positions ) } } } private fun translationYAnimator( from: Float, to: Float, durationMs: Long, delayMs: Long = 0, @Position positions: Int = POSITION_TOP or POSITION_BOTTOM, interpolator: Interpolator = Interpolators.LINEAR ): Animator { return ValueAnimator.ofFloat(from, to).apply { duration = durationMs startDelay = delayMs this.interpolator = interpolator addUpdateListener { va: ValueAnimator -> ComplicationLayoutParams.iteratePositions( { position: Int -> setElementsTranslationYAtPosition(va.animatedValue as Float, position) }, positions ) } } } /** Sets alpha of complications at the specified position. */ private fun setElementsAlphaAtPosition(alpha: Float, position: Int, fadingOut: Boolean) { mCurrentAlphaAtPosition[position] = alpha mComplicationHostViewController.getViewsAtPosition(position).forEach { view -> if (fadingOut) { CrossFadeHelper.fadeOut(view, 1 - alpha, /* remap= */ false) } else { CrossFadeHelper.fadeIn(view, alpha, /* remap= */ false) } } if (position == POSITION_TOP) { mStatusBarViewController.setFadeAmount(alpha, fadingOut) } } /** Sets y translation of complications at the specified position. */ private fun setElementsTranslationYAtPosition(translationY: Float, position: Int) { mComplicationHostViewController.getViewsAtPosition(position).forEach { v -> v.translationY = translationY } if (position == POSITION_TOP) { mStatusBarViewController.setTranslationY(translationY) } } private fun loadFromResources(view: View): ConfigurationBasedDimensions { return ConfigurationBasedDimensions( translationYPx = view.resources.getDimensionPixelSize(R.dimen.dream_overlay_exit_y_offset), ) } private data class ConfigurationBasedDimensions( val translationYPx: Int, ) }