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