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