• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2017 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 package com.android.systemui.statusbar.notification
17 
18 import android.util.FloatProperty
19 import android.util.Property
20 import android.view.View
21 import com.android.internal.dynamicanimation.animation.DynamicAnimation
22 import com.android.internal.dynamicanimation.animation.SpringAnimation
23 import com.android.internal.dynamicanimation.animation.SpringForce
24 import com.android.systemui.res.R
25 import com.android.systemui.statusbar.notification.PhysicsPropertyAnimator.Companion.createDefaultSpring
26 import com.android.systemui.statusbar.notification.stack.AnimationProperties
27 import kotlin.math.sign
28 
29 /**
30  * A physically animatable property of a view.
31  *
32  * @param tag the view tag to safe this property in
33  * @param property the property to animate.
34  * @param avoidDoubleOvershoot should this property avoid double overshoot when animated
35  */
36 data class PhysicsProperty
37 @JvmOverloads constructor(
38     val tag: Int, val property: Property<View, Float>, val avoidDoubleOvershoot: Boolean = true
39 ) {
40     val offsetProperty =
41         object : FloatProperty<View>(property.name) {
42             override fun get(view: View): Float {
43                 return property.get(view)
44             }
45 
46             override fun setValue(view: View, offset: Float) {
47                 val propertyData = view.getTag(tag) as PropertyData? ?: return
48                 propertyData.offset = offset
49                 property.set(view, propertyData.finalValue + offset)
50             }
51         }
52 
53     fun setFinalValue(view: View, finalValue: Float) {
54         val propertyData = obtainPropertyData(view, this)
55         val previousValue = propertyData.finalValue
56         if (previousValue != finalValue) {
57             propertyData.finalValue = finalValue
58             property.set(view, propertyData.finalValue + propertyData.offset)
59         }
60     }
61 }
62 
63 /** The propertyData associated with each animation running */
64 data class PropertyData(
65     var finalValue: Float = 0f,
66     var offset: Float = 0f,
67     var animator: SpringAnimation? = null,
68     var delayRunnable: Runnable? = null,
69     var startOffset: Float = 0f,
70     var doubleOvershootAvoidingListener: DynamicAnimation.OnAnimationUpdateListener? = null
71 )
72 
73 /**
74  * A utility that can run physics based animations in a simple way. It properly handles overlapping
75  * calls where sometimes a property can be set without animation, while also having instances where
76  * it's supposed to start animations.
77  *
78  * This overall helps making sure that physics based animations complete and don't constantly start
79  * new transitions which can lead to a feeling of lagging behind.
80  *
81  * Overall it is achieved by starting offset animations to an end value as soon as an animation is
82  * requested and updating the end value immediately when no animation is needed. With the offset
83  * always going to 0, this ensures that animations complete within a short time after an animation
84  * has been requested.
85  */
86 class PhysicsPropertyAnimator {
87     companion object {
88         @JvmField val TAG_ANIMATOR_TRANSLATION_Y = R.id.translation_y_animator_tag
89 
90         @JvmField
91         val Y_TRANSLATION: PhysicsProperty =
92             PhysicsProperty(TAG_ANIMATOR_TRANSLATION_Y, View.TRANSLATION_Y)
93 
94         // Uses the standard spatial material spring by default
95         @JvmStatic
createDefaultSpringnull96         fun createDefaultSpring(): SpringForce {
97             return SpringForce()
98                 .setStiffness(380f)
99                 .setDampingRatio(0.68f);
100         }
101 
102         @JvmStatic
103         @JvmOverloads
104         /**
105          * Set a property on a view, updating its value, even if it's already animating. The @param
106          * animated can be used to request an animation. If the view isn't animated, this utility
107          * will update the current animation if existent, such that the end value will point
108          * to @param newEndValue or apply it directly if there's no animation.
109          */
setPropertynull110         fun setProperty(
111             view: View,
112             animatableProperty: PhysicsProperty,
113             newEndValue: Float,
114             properties: AnimationProperties? = null,
115             animated: Boolean = false,
116             endListener: DynamicAnimation.OnAnimationEndListener? = null,
117         ) {
118             if (animated) {
119                 startAnimation(view, animatableProperty, newEndValue, properties, endListener)
120             } else {
121                 animatableProperty.setFinalValue(view, newEndValue)
122             }
123         }
124 
isAnimatingnull125         fun isAnimating(view: View, property: PhysicsProperty): Boolean {
126             val (_, _, animator, _) = obtainPropertyData(view, property)
127             return animator?.isRunning ?: false
128         }
129     }
130 }
131 
startAnimationnull132 private fun startAnimation(
133     view: View,
134     animatableProperty: PhysicsProperty,
135     newEndValue: Float,
136     properties: AnimationProperties?,
137     endListener: DynamicAnimation.OnAnimationEndListener?,
138 ) {
139     val property = animatableProperty.property
140     val propertyData = obtainPropertyData(view, animatableProperty)
141     val previousEndValue = propertyData.finalValue
142     if (previousEndValue == newEndValue) {
143         return
144     }
145     propertyData.finalValue = newEndValue
146     var animator = propertyData.animator
147     if (animator == null) {
148         animator = SpringAnimation(view, animatableProperty.offsetProperty)
149         propertyData.animator = animator
150         val listener = properties?.getAnimationEndListener(animatableProperty.property)
151         if (listener != null) {
152             animator.addEndListener(listener)
153         }
154         // We always notify things as started even if we have a delay
155         properties?.getAnimationStartListener(animatableProperty.property)?.accept(animator)
156         // remove the tag when the animation is finished
157         animator.addEndListener { _, _, _, _ ->
158             propertyData.animator = null
159             propertyData.doubleOvershootAvoidingListener = null
160             // Let's make sure we never get stuck with an offset even when canceling
161             // We never actually cancel running animations but keep it around, so this only
162             // triggers if things really should end.
163             propertyData.offset = 0f
164         }
165     }
166     if (animatableProperty.avoidDoubleOvershoot
167         && propertyData.doubleOvershootAvoidingListener == null) {
168         propertyData.doubleOvershootAvoidingListener =
169             DynamicAnimation.OnAnimationUpdateListener { _, offset: Float, velocity: Float ->
170                 val isOscillatingBackwards = velocity.sign == propertyData.startOffset.sign
171                 val didAlreadyRemoveBounciness =
172                     animator.spring.dampingRatio == SpringForce.DAMPING_RATIO_NO_BOUNCY
173                 val isOvershooting = offset.sign != propertyData.startOffset.sign
174                 if (isOvershooting && isOscillatingBackwards && !didAlreadyRemoveBounciness) {
175                     // our offset is starting to decrease, let's remove all overshoot
176                     animator.spring.setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)
177                 } else if (!isOvershooting
178                     && (didAlreadyRemoveBounciness || isOscillatingBackwards)) {
179                     // we already did overshoot, let's skip to the end to avoid oscillations.
180                     // Usually we shouldn't hit this as setting the damping ratio avoid overshoots
181                     // but it may still happen if we see jank
182                     animator.skipToEnd();
183                 }
184             }
185         animator.addUpdateListener(propertyData.doubleOvershootAvoidingListener)
186     } else if (!animatableProperty.avoidDoubleOvershoot
187         && propertyData.doubleOvershootAvoidingListener != null) {
188         animator.removeUpdateListener(propertyData.doubleOvershootAvoidingListener)
189     }
190     // reset a new spring as it may have been modified
191     animator.setSpring(createDefaultSpring().setFinalPosition(0f))
192     // TODO(b/393581344): look at custom spring
193     endListener?.let { animator.addEndListener(it) }
194 
195     val startOffset = previousEndValue - newEndValue + propertyData.offset
196     // Immediately set the new offset that compensates for the immediate end value change
197     propertyData.offset = startOffset
198     propertyData.startOffset = startOffset
199     property.set(view, newEndValue + startOffset)
200 
201     // cancel previous starters still pending
202     view.removeCallbacks(propertyData.delayRunnable)
203     animator.setStartValue(startOffset)
204     val startRunnable = Runnable {
205         animator.animateToFinalPosition(0f)
206         propertyData.delayRunnable = null
207         // When setting a new spring on a running animation it doesn't properly set the finish
208         // conditions and will never actually end them only calling start explicitly does that,
209         // so let's start them again!
210         animator.start()
211     }
212     if (properties != null && properties.delay > 0 && !animator.isRunning) {
213         propertyData.delayRunnable = startRunnable
214         view.postDelayed(propertyData.delayRunnable, properties.delay)
215     } else {
216         startRunnable.run()
217     }
218 }
219 
obtainPropertyDatanull220 private fun obtainPropertyData(view: View, animatableProperty: PhysicsProperty): PropertyData {
221     var propertyData = view.getTag(animatableProperty.tag) as PropertyData?
222     if (propertyData == null) {
223         propertyData =
224             PropertyData(finalValue = animatableProperty.property.get(view), offset = 0f, null)
225         view.setTag(animatableProperty.tag, propertyData)
226     }
227     return propertyData
228 }
229