1 /* 2 * Copyright (C) 2020 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.controls.management 18 19 import android.animation.Animator 20 import android.animation.AnimatorListenerAdapter 21 import android.animation.AnimatorSet 22 import android.animation.ObjectAnimator 23 import android.annotation.IdRes 24 import android.content.Intent 25 import android.transition.Transition 26 import android.transition.TransitionValues 27 import android.util.Log 28 import android.view.View 29 import android.view.ViewGroup 30 import android.view.Window 31 import androidx.lifecycle.Lifecycle 32 import androidx.lifecycle.LifecycleObserver 33 import androidx.lifecycle.OnLifecycleEvent 34 import com.android.systemui.R 35 import com.android.systemui.animation.Interpolators 36 import com.android.systemui.controls.ui.ControlsUiController 37 38 object ControlsAnimations { 39 40 private const val ALPHA_EXIT_DURATION = 183L 41 private const val ALPHA_ENTER_DELAY = ALPHA_EXIT_DURATION 42 private const val ALPHA_ENTER_DURATION = 350L - ALPHA_ENTER_DELAY 43 44 private const val Y_TRANSLATION_EXIT_DURATION = 183L 45 private const val Y_TRANSLATION_ENTER_DELAY = Y_TRANSLATION_EXIT_DURATION - ALPHA_ENTER_DELAY 46 private const val Y_TRANSLATION_ENTER_DURATION = 400L - Y_TRANSLATION_EXIT_DURATION 47 private var translationY: Float = -1f 48 49 /** 50 * Setup an activity to handle enter/exit animations. [view] should be the root of the content. 51 * Fade and translate together. 52 */ 53 fun observerForAnimations(view: ViewGroup, window: Window, intent: Intent): LifecycleObserver { 54 return object : LifecycleObserver { 55 var showAnimation = intent.getBooleanExtra(ControlsUiController.EXTRA_ANIMATE, false) 56 57 init { 58 // Must flag the parent group to move it all together, and set the initial 59 // transitionAlpha to 0.0f. This property is reserved for fade animations. 60 view.setTransitionGroup(true) 61 view.transitionAlpha = 0.0f 62 63 if (translationY == -1f) { 64 translationY = view.context.resources.getDimensionPixelSize( 65 R.dimen.global_actions_controls_y_translation).toFloat() 66 } 67 } 68 69 @OnLifecycleEvent(Lifecycle.Event.ON_START) 70 fun setup() { 71 with(window) { 72 allowEnterTransitionOverlap = true 73 enterTransition = enterWindowTransition(view.getId()) 74 exitTransition = exitWindowTransition(view.getId()) 75 reenterTransition = enterWindowTransition(view.getId()) 76 returnTransition = exitWindowTransition(view.getId()) 77 } 78 } 79 80 @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) 81 fun enterAnimation() { 82 if (showAnimation) { 83 ControlsAnimations.enterAnimation(view).start() 84 showAnimation = false 85 } 86 } 87 88 @OnLifecycleEvent(Lifecycle.Event.ON_STOP) 89 fun resetAnimation() { 90 view.translationY = 0f 91 } 92 } 93 } 94 95 fun enterAnimation(view: View): Animator { 96 Log.d(ControlsUiController.TAG, "Enter animation for $view") 97 98 view.transitionAlpha = 0.0f 99 view.alpha = 1.0f 100 101 view.translationY = translationY 102 103 val alphaAnimator = ObjectAnimator.ofFloat(view, "transitionAlpha", 0.0f, 1.0f).apply { 104 interpolator = Interpolators.DECELERATE_QUINT 105 startDelay = ALPHA_ENTER_DELAY 106 duration = ALPHA_ENTER_DURATION 107 } 108 109 val yAnimator = ObjectAnimator.ofFloat(view, "translationY", 0.0f).apply { 110 interpolator = Interpolators.DECELERATE_QUINT 111 startDelay = Y_TRANSLATION_ENTER_DURATION 112 duration = Y_TRANSLATION_ENTER_DURATION 113 } 114 115 return AnimatorSet().apply { 116 playTogether(alphaAnimator, yAnimator) 117 } 118 } 119 120 /** 121 * Properly handle animations originating from dialogs. Activity transitions require 122 * transitioning between two activities, so expose this method for dialogs to animate 123 * on exit. 124 */ 125 @JvmStatic 126 fun exitAnimation(view: View, onEnd: Runnable? = null): Animator { 127 Log.d(ControlsUiController.TAG, "Exit animation for $view") 128 129 val alphaAnimator = ObjectAnimator.ofFloat(view, "transitionAlpha", 0.0f).apply { 130 interpolator = Interpolators.ACCELERATE 131 duration = ALPHA_EXIT_DURATION 132 } 133 134 view.translationY = 0.0f 135 val yAnimator = ObjectAnimator.ofFloat(view, "translationY", -translationY).apply { 136 interpolator = Interpolators.ACCELERATE 137 duration = Y_TRANSLATION_EXIT_DURATION 138 } 139 140 return AnimatorSet().apply { 141 playTogether(alphaAnimator, yAnimator) 142 onEnd?.let { 143 addListener(object : AnimatorListenerAdapter() { 144 override fun onAnimationEnd(animation: Animator) { 145 it.run() 146 } 147 }) 148 } 149 } 150 } 151 152 fun enterWindowTransition(@IdRes id: Int) = 153 WindowTransition({ view: View -> enterAnimation(view) }).apply { 154 addTarget(id) 155 } 156 157 fun exitWindowTransition(@IdRes id: Int) = 158 WindowTransition({ view: View -> exitAnimation(view) }).apply { 159 addTarget(id) 160 } 161 } 162 163 /** 164 * In order to animate, at least one property must be marked on each view that should move. 165 * Setting "item" is just a flag to indicate that it should move by the animator. 166 */ 167 class WindowTransition( 168 val animator: (view: View) -> Animator 169 ) : Transition() { 170 override fun captureStartValues(tv: TransitionValues) { 171 tv.values["item"] = 0.0f 172 } 173 174 override fun captureEndValues(tv: TransitionValues) { 175 tv.values["item"] = 1.0f 176 } 177 178 override fun createAnimator( 179 sceneRoot: ViewGroup, 180 startValues: TransitionValues?, 181 endValues: TransitionValues? 182 ): Animator? = animator(startValues!!.view) 183 } 184