diff options
| author | 2025-02-18 10:07:38 -0800 | |
|---|---|---|
| committer | 2025-02-18 10:07:38 -0800 | |
| commit | e9fca3411bf577b1e37cccdfe2f9e84acd603717 (patch) | |
| tree | 79e1617b0583b1608f3a54f54a601414adcfad39 | |
| parent | ed3076898d3bd18beeaa4f6f25ae8d51f7b63a1d (diff) | |
| parent | 433cab73fd84fc768eee48c9fd17df8564bb5260 (diff) | |
Merge "Use physics for notification movement" into main
21 files changed, 775 insertions, 171 deletions
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 29b578ae6e48..f981545008fa 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -1915,6 +1915,13 @@ flag { } flag { + name: "physical_notification_movement" + namespace: "systemui" + description: "Make notifications use physics based animations for movement" + bug: "393581344" +} + +flag { name: "glanceable_hub_direct_edit_mode" namespace: "systemui" description: "Invokes edit mode directly from long press in glanceable hub" diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/DragDownHelperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/DragDownHelperTest.kt index 05d9495db091..a8aac39540fb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/DragDownHelperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/DragDownHelperTest.kt @@ -74,7 +74,7 @@ class DragDownHelperTest : SysuiTestCase() { dragDownHelper.cancelChildExpansion(expandableView, animationDuration = 0) - verify(expandableView, atLeast(1)).actualHeight = collapsedHeight + verify(expandableView, atLeast(1)).setFinalActualHeight(collapsedHeight) } @Test @@ -83,6 +83,6 @@ class DragDownHelperTest : SysuiTestCase() { dragDownHelper.cancelChildExpansion(expandableView, animationDuration = 0) - verify(expandableView, never()).actualHeight = anyInt() + verify(expandableView, never()).setFinalActualHeight(anyInt()) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/PulseExpansionHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/PulseExpansionHandlerTest.kt index cd66ef32180a..242da0bacea3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/PulseExpansionHandlerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/PulseExpansionHandlerTest.kt @@ -86,7 +86,7 @@ class PulseExpansionHandlerTest : SysuiTestCase() { pulseExpansionHandler.reset(expandableView, animationDuration = 0) - verify(expandableView, atLeast(1)).actualHeight = collapsedHeight + verify(expandableView, atLeast(1)).setFinalActualHeight(collapsedHeight) } @Test @@ -95,6 +95,6 @@ class PulseExpansionHandlerTest : SysuiTestCase() { pulseExpansionHandler.reset(expandableView, animationDuration = 0) - verify(expandableView, never()).actualHeight = anyInt() + verify(expandableView, never()).setFinalActualHeight(anyInt()) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/PhysicsPropertyAnimatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/PhysicsPropertyAnimatorTest.kt new file mode 100644 index 000000000000..56cd72e7725f --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/PhysicsPropertyAnimatorTest.kt @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2017 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.statusbar.notification + +import android.util.FloatProperty +import android.util.Property +import android.view.View +import androidx.dynamicanimation.animation.DynamicAnimation +import androidx.test.annotation.UiThreadTest +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.res.R +import com.android.systemui.statusbar.notification.stack.AnimationProperties +import com.android.systemui.statusbar.notification.stack.ViewState +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito +import org.mockito.kotlin.any + +@SmallTest +@RunWith(AndroidJUnit4::class) +@UiThreadTest +class PhysicsPropertyAnimatorTest : SysuiTestCase() { + private var view: View = View(context) + private val effectiveProperty = + object : FloatProperty<View>("TEST") { + private var _value: Float = 100f + + override fun setValue(view: View, value: Float) { + this._value = value + } + + override fun get(`object`: View): Float { + return _value + } + } + private val property: PhysicsProperty = + PhysicsProperty(R.id.scale_x_animator_tag, effectiveProperty) + private var finishListener: DynamicAnimation.OnAnimationEndListener? = null + private val animationProperties: AnimationProperties = AnimationProperties() + + @Before + fun setUp() { + finishListener = Mockito.mock(DynamicAnimation.OnAnimationEndListener::class.java) + } + + @Test + fun testAnimationStarted() { + PhysicsPropertyAnimator.setProperty( + view, + property, + 200f, + animationProperties, + true, /* animate */ + ) + Assert.assertTrue(PhysicsPropertyAnimator.isAnimating(view, property)) + } + + @Test + fun testNoAnimationStarted() { + PhysicsPropertyAnimator.setProperty(view, property, 200f, animationProperties, false) + Assert.assertFalse(PhysicsPropertyAnimator.isAnimating(view, property)) + } + + @Test + fun testEndValueUpdated() { + PhysicsPropertyAnimator.setProperty( + view, + property, + 200f, + animationProperties, + true, /* animate */ + ) + Assert.assertEquals( + (ViewState.getChildTag(view, property.tag) as PropertyData).finalValue, + 200f, + ) + } + + @Test + fun testOffset() { + effectiveProperty.setValue(view, 100f) + PhysicsPropertyAnimator.setProperty( + view, + property, + 200f, + animationProperties, + true, /* animate */ + ) + val propertyData = ViewState.getChildTag(view, property.tag) as PropertyData + Assert.assertEquals(propertyData.finalValue, 200f) + Assert.assertEquals(propertyData.offset, -100f) + } + + @Test + fun testValueIsSetUnAnimated() { + effectiveProperty.setValue(view, 100f) + PhysicsPropertyAnimator.setProperty( + view, + property, + 200f, + animationProperties, + false, /* animate */ + ) + Assert.assertEquals(200f, effectiveProperty[view]) + } + + @Test + fun testAnimationToRightValueUpdated() { + effectiveProperty.setValue(view, 100f) + PhysicsPropertyAnimator.setProperty( + view, + property, + 200f, + animationProperties, + true, /* animate */ + ) + PhysicsPropertyAnimator.setProperty( + view, + property, + 220f, + animationProperties, + false, /* animate */ + ) + Assert.assertTrue(PhysicsPropertyAnimator.isAnimating(view, property)) + Assert.assertEquals(120f, effectiveProperty[view]) + Assert.assertEquals( + (ViewState.getChildTag(view, property.tag) as PropertyData).finalValue, + 220f, + ) + } + + @Test + fun testAnimationToRightValueUpdateAnimated() { + effectiveProperty.setValue(view, 100f) + PhysicsPropertyAnimator.setProperty( + view, + property, + 200f, + animationProperties, + true, /* animate */ + ) + PhysicsPropertyAnimator.setProperty( + view, + property, + 220f, + animationProperties, + true, /* animate */ + ) + Assert.assertTrue(PhysicsPropertyAnimator.isAnimating(view, property)) + Assert.assertEquals(100f, effectiveProperty[view]) + val propertyData = ViewState.getChildTag(view, property.tag) as PropertyData + Assert.assertEquals(propertyData.finalValue, 220f) + Assert.assertEquals(propertyData.offset, -120f) + } + + @Test + fun testUsingDelay() { + effectiveProperty.setValue(view, 100f) + animationProperties.setDelay(200) + PhysicsPropertyAnimator.setProperty( + view, + property, + 200f, + animationProperties, + true, /* animate */ + ) + val propertyData = ViewState.getChildTag(view, property.tag) as PropertyData + Assert.assertNotNull(propertyData.delayRunnable) + Assert.assertFalse(propertyData.animator?.isRunning ?: true) + } + + @Test + fun testUsingListener() { + PhysicsPropertyAnimator.setProperty( + view, + property, + 200f, + animationProperties, + true, + finishListener, + ) + val propertyData = ViewState.getChildTag(view, property.tag) as PropertyData + propertyData.animator?.cancel() + Mockito.verify(finishListener!!).onAnimationEnd(any(), any(), any(), any()) + } + + @Test + fun testUsingListenerProperties() { + val finishListener2 = Mockito.mock(DynamicAnimation.OnAnimationEndListener::class.java) + val animationProperties: AnimationProperties = + object : AnimationProperties() { + override fun getAnimationEndListener( + property: Property<*, *>? + ): DynamicAnimation.OnAnimationEndListener { + return finishListener2 + } + } + PhysicsPropertyAnimator.setProperty(view, property, 200f, animationProperties, true) + val propertyData = ViewState.getChildTag(view, property.tag) as PropertyData + propertyData.animator?.cancel() + Mockito.verify(finishListener2).onAnimationEnd(any(), any(), any(), any()) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MediaContainerViewTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MediaContainerViewTest.kt index 3a77d822eb7e..52f903e20ab8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MediaContainerViewTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/MediaContainerViewTest.kt @@ -31,7 +31,7 @@ class MediaContainerViewTest : SysuiTestCase() { fun testUpdateClipping_updatesClipHeight() { assertTrue(mediaContainerView.clipHeight == 0) - mediaContainerView.actualHeight = 10 + mediaContainerView.setFinalActualHeight(10) mediaContainerView.updateClipping() assertTrue(mediaContainerView.clipHeight == 10) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt index e4dd29ad83b0..67415de86d9b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackStateAnimatorTest.kt @@ -81,7 +81,7 @@ class StackStateAnimatorTest : SysuiTestCase() { stackStateAnimator.startAnimationForEvents(arrayListOf(event), 0) - verify(view).setActualHeight(VIEW_HEIGHT, false) + verify(view).setFinalActualHeight(VIEW_HEIGHT) verify(view, description("should animate from the top")).translationY = expectedStartY verify(view) .performAddAnimation( @@ -104,7 +104,7 @@ class StackStateAnimatorTest : SysuiTestCase() { stackStateAnimator.startAnimationForEvents(arrayListOf(event), 0) - verify(view).setActualHeight(VIEW_HEIGHT, false) + verify(view).setFinalActualHeight(VIEW_HEIGHT) verify(view, description("should animate from the bottom")).translationY = expectedStartY verify(view) .performAddAnimation( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ViewStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ViewStateTest.kt index e493420b64a1..ef415c918f91 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ViewStateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ViewStateTest.kt @@ -16,21 +16,28 @@ package com.android.systemui.statusbar.notification.stack +import android.animation.ValueAnimator +import android.view.View +import androidx.test.annotation.UiThreadTest import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.log.assertDoesNotLogWtf import com.android.systemui.log.assertLogsWtf -import kotlin.math.log2 -import kotlin.math.sqrt +import com.android.systemui.statusbar.notification.PhysicsPropertyAnimator +import com.android.systemui.statusbar.notification.PhysicsPropertyAnimator.Companion.TAG_ANIMATOR_TRANSLATION_Y +import com.android.systemui.statusbar.notification.PhysicsPropertyAnimator.Companion.Y_TRANSLATION import org.junit.Assert import org.junit.Test import org.junit.runner.RunWith +import kotlin.math.log2 +import kotlin.math.sqrt @RunWith(AndroidJUnit4::class) @SmallTest +@UiThreadTest class ViewStateTest : SysuiTestCase() { - private val viewState = ViewState() + private val viewState = ViewState(true /* usePhysicsForMovement */) @Suppress("DIVISION_BY_ZERO") @Test @@ -64,4 +71,37 @@ class ViewStateTest : SysuiTestCase() { assertLogsWtf { viewState.scaleY = Float.POSITIVE_INFINITY * 0 } Assert.assertEquals(viewState.scaleY, 0.25f) } + + @Test + fun testUsingPhysics() { + val animatedView = View(context) + viewState.setUsePhysicsForMovement(true) + viewState.applyToView(animatedView) + viewState.yTranslation = 100f + val animationFilter = AnimationFilter().animateY() + val animationProperties = object : AnimationProperties() { + override fun getAnimationFilter(): AnimationFilter { + return animationFilter + } + } + viewState.animateTo(animatedView, animationProperties) + Assert.assertTrue(PhysicsPropertyAnimator.isAnimating(animatedView, Y_TRANSLATION)) + } + + @Test + fun testNotUsingPhysics() { + val animatedView = View(context) + viewState.setUsePhysicsForMovement(false) + viewState.applyToView(animatedView) + viewState.yTranslation = 100f + val animationFilter = AnimationFilter().animateY() + val animationProperties = object : AnimationProperties() { + override fun getAnimationFilter(): AnimationFilter { + return animationFilter + } + } + viewState.animateTo(animatedView, animationProperties) + val tag = animatedView.getTag(TAG_ANIMATOR_TRANSLATION_Y) + Assert.assertTrue(tag is ValueAnimator) + } } diff --git a/packages/SystemUI/src/com/android/systemui/ExpandHelper.java b/packages/SystemUI/src/com/android/systemui/ExpandHelper.java index 42896a419658..b2cb357fabc8 100644 --- a/packages/SystemUI/src/com/android/systemui/ExpandHelper.java +++ b/packages/SystemUI/src/com/android/systemui/ExpandHelper.java @@ -167,7 +167,7 @@ public class ExpandHelper implements Gefingerpoken { public void setHeight(float h) { if (DEBUG_SCALE) Log.v(TAG, "SetHeight: setting to " + h); - mView.setActualHeight((int) h); + mView.setFinalActualHeight((int) h); mCurrentHeight = h; } public float getHeight() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt index 10f61c66c838..5b5058fbc6c2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt @@ -798,6 +798,7 @@ class DragDownHelper( initialTouchY = y initialTouchX = x } + MotionEvent.ACTION_MOVE -> { val h = y - initialTouchY // Adjust the touch slop if another gesture may be being performed. @@ -852,6 +853,7 @@ class DragDownHelper( } return true } + MotionEvent.ACTION_UP -> if ( !falsingManager.isUnlockingDisabled && @@ -871,6 +873,7 @@ class DragDownHelper( stopDragging() return false } + MotionEvent.ACTION_CANCEL -> { stopDragging() return false @@ -910,7 +913,7 @@ class DragDownHelper( overshoot *= 1 - RUBBERBAND_FACTOR_STATIC rubberband -= overshoot } - child.actualHeight = (child.collapsedHeight + rubberband).toInt() + child.setFinalActualHeight((child.collapsedHeight + rubberband).toInt()) } @VisibleForTesting @@ -927,7 +930,7 @@ class DragDownHelper( anim.duration = animationDuration anim.addUpdateListener { animation: ValueAnimator -> // don't use reflection, because the `actualHeight` field may be obfuscated - child.actualHeight = animation.animatedValue as Int + child.setFinalActualHeight(animation.animatedValue as Int) } anim.addListener( object : AnimatorListenerAdapter() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt index 85b8bf9aec80..3be7682fe250 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt @@ -140,8 +140,8 @@ constructor( private fun canHandleMotionEvent(): Boolean { return wakeUpCoordinator.canShowPulsingHuns && - !shadeInteractor.isQsExpanded.value && - !bouncerShowing + !shadeInteractor.isQsExpanded.value && + !bouncerShowing } private fun startExpansion(event: MotionEvent): Boolean { @@ -194,7 +194,7 @@ constructor( override fun onTouchEvent(event: MotionEvent): Boolean { val finishExpanding = (event.action == MotionEvent.ACTION_CANCEL || event.action == MotionEvent.ACTION_UP) && - isExpanding + isExpanding val isDraggingNotificationOrCanBypass = mStartingChild?.showingPulsing() == true || bypassController.canBypass() @@ -218,8 +218,8 @@ constructor( velocityTracker!!.computeCurrentVelocity(/* units= */ 1000) val canExpand = moveDistance > 0 && - velocityTracker!!.getYVelocity() > -1000 && - statusBarStateController.state != StatusBarState.SHADE + velocityTracker!!.getYVelocity() > -1000 && + statusBarStateController.state != StatusBarState.SHADE if (!falsingManager.isUnlockingDisabled && !isFalseTouch && canExpand) { finishExpansion() } else { @@ -266,11 +266,11 @@ constructor( val child = mStartingChild!! val newHeight = Math.min((child.collapsedHeight + expansionHeight).toInt(), child.maxContentHeight) - child.actualHeight = newHeight + child.setFinalActualHeight(newHeight) } else { wakeUpCoordinator.setNotificationsVisibleForExpansion( height > - lockscreenShadeTransitionController.distanceUntilShowingPulsingNotifications, + lockscreenShadeTransitionController.distanceUntilShowingPulsingNotifications, /*animate= */ true, /*increaseSpeed= */ true, ) @@ -301,7 +301,7 @@ constructor( anim.duration = animationDuration anim.addUpdateListener { animation: ValueAnimator -> // don't use reflection, because the `actualHeight` field may be obfuscated - child.actualHeight = animation.animatedValue as Int + child.setFinalActualHeight(animation.animatedValue as Int) } anim.addListener( object : AnimatorListenerAdapter() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/PhysicsPropertyAnimator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/PhysicsPropertyAnimator.kt new file mode 100644 index 000000000000..74faf2576abd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/PhysicsPropertyAnimator.kt @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2017 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.statusbar.notification + +import android.util.Property +import android.view.View +import androidx.dynamicanimation.animation.DynamicAnimation +import androidx.dynamicanimation.animation.FloatPropertyCompat +import androidx.dynamicanimation.animation.SpringAnimation +import androidx.dynamicanimation.animation.SpringForce +import com.android.systemui.res.R +import com.android.systemui.statusbar.notification.PhysicsPropertyAnimator.Companion.createDefaultSpring +import com.android.systemui.statusbar.notification.stack.AnimationProperties + +/** + * A physically animatable property of a view. + * + * @param tag the view tag to safe this property in + * @param property the property to animate. + */ +data class PhysicsProperty(val tag: Int, val property: Property<View, Float>) { + val offsetProperty = + object : FloatPropertyCompat<View>(property.name) { + override fun getValue(view: View): Float { + return property.get(view) + } + + override fun setValue(view: View, offset: Float) { + val propertyData = view.getTag(tag) as PropertyData? ?: return + propertyData.offset = offset + property.set(view, propertyData.finalValue + offset) + } + } + + fun setFinalValue(view: View, finalValue: Float) { + val propertyData = obtainPropertyData(view, this) + val previousValue = propertyData.finalValue + if (previousValue != finalValue) { + propertyData.finalValue = finalValue + property.set(view, propertyData.finalValue + propertyData.offset) + } + } +} + +/** The propertyData associated with each animation running */ +data class PropertyData( + var finalValue: Float = 0f, + var offset: Float = 0f, + var animator: SpringAnimation? = null, + var delayRunnable: Runnable? = null, +) + +/** + * A utility that can run physics based animations in a simple way. It properly handles overlapping + * calls where sometimes a property can be set without animation, while also having instances where + * it's supposed to start animations. + * + * This overall helps making sure that physics based animations complete and don't constantly start + * new transitions which can lead to a feeling of lagging behind. + * + * Overall it is achieved by starting offset animations to an end value as soon as an animation is + * requested and updating the end value immediately when no animation is needed. With the offset + * always going to 0, this ensures that animations complete within a short time after an animation + * has been requested. + */ +class PhysicsPropertyAnimator { + companion object { + @JvmField val TAG_ANIMATOR_TRANSLATION_Y = R.id.translation_y_animator_tag + + @JvmField + val Y_TRANSLATION: PhysicsProperty = + PhysicsProperty(TAG_ANIMATOR_TRANSLATION_Y, View.TRANSLATION_Y) + + // Uses the standard spatial material spring by default + @JvmStatic + fun createDefaultSpring(): SpringForce { + return SpringForce() + .setStiffness(380f) // MEDIUM LOW STIFFNESS + .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY) // LOW BOUNCINESS + } + + @JvmStatic + @JvmOverloads + /** + * Set a property on a view, updating its value, even if it's already animating. The @param + * animated can be used to request an animation. If the view isn't animated, this utility + * will update the current animation if existent, such that the end value will point + * to @param newEndValue or apply it directly if there's no animation. + */ + fun setProperty( + view: View, + animatableProperty: PhysicsProperty, + newEndValue: Float, + properties: AnimationProperties? = null, + animated: Boolean = false, + endListener: DynamicAnimation.OnAnimationEndListener? = null, + ) { + if (animated) { + startAnimation(view, animatableProperty, newEndValue, properties, endListener) + } else { + animatableProperty.setFinalValue(view, newEndValue) + } + } + + fun isAnimating(view: View, property: PhysicsProperty): Boolean { + val (_, _, animator, _) = obtainPropertyData(view, property) + return animator?.isRunning ?: false + } + } +} + +private fun startAnimation( + view: View, + animatableProperty: PhysicsProperty, + newEndValue: Float, + properties: AnimationProperties?, + endListener: DynamicAnimation.OnAnimationEndListener?, +) { + val property = animatableProperty.property + val propertyData = obtainPropertyData(view, animatableProperty) + val previousEndValue = propertyData.finalValue + if (previousEndValue == newEndValue) { + return + } + propertyData.finalValue = newEndValue + var animator = propertyData.animator + if (animator == null) { + animator = SpringAnimation(view, animatableProperty.offsetProperty) + propertyData.animator = animator + animator.setSpring(createDefaultSpring()) + val listener = properties?.getAnimationEndListener(animatableProperty.property) + if (listener != null) { + animator.addEndListener(listener) + // We always notify things as started even if we have a delay + properties.getAnimationStartListener(animatableProperty.property)?.accept(animator) + } + // remove the tag when the animation is finished + animator.addEndListener { _, _, _, _ -> propertyData.animator = null } + } + // TODO(b/393581344): look at custom spring + endListener?.let { animator.addEndListener(it) } + val newOffset = previousEndValue - newEndValue + propertyData.offset + + // Immedialely set the new offset that compensates for the immediate end value change + propertyData.offset = newOffset + property.set(view, newEndValue + newOffset) + + // cancel previous starters still pending + view.removeCallbacks(propertyData.delayRunnable) + animator.setStartValue(newOffset) + val startRunnable = Runnable { + animator.animateToFinalPosition(0f) + propertyData.delayRunnable = null + } + if (properties != null && properties.delay > 0 && !animator.isRunning) { + propertyData.delayRunnable = startRunnable + view.postDelayed(propertyData.delayRunnable, properties.delay) + } else { + startRunnable.run() + } +} + +private fun obtainPropertyData(view: View, animatableProperty: PhysicsProperty): PropertyData { + var propertyData = view.getTag(animatableProperty.tag) as PropertyData? + if (propertyData == null) { + propertyData = + PropertyData(finalValue = animatableProperty.property.get(view), offset = 0f, null) + view.setTag(animatableProperty.tag, propertyData) + } + return propertyData +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index d383bee64530..b5858ec7e4e0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -2745,7 +2745,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView top = params.getTop(); } int actualHeight = params.getBottom() - top; - setActualHeight(actualHeight); + setFinalActualHeight(actualHeight); int notificationStackTop = params.getNotificationParentTop(); top -= notificationStackTop; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java index 2bc48746f847..da664f864f06 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.row; import static com.android.systemui.Flags.notificationColorUpdateLogger; +import static com.android.systemui.Flags.physicalNotificationMovement; import android.animation.AnimatorListenerAdapter; import android.content.Context; @@ -24,6 +25,7 @@ import android.content.res.Configuration; import android.graphics.Paint; import android.graphics.Rect; import android.util.AttributeSet; +import android.util.FloatProperty; import android.util.IndentingPrintWriter; import android.util.Log; import android.view.View; @@ -41,6 +43,7 @@ import com.android.app.animation.Interpolators; import com.android.systemui.Dumpable; import com.android.systemui.res.R; import com.android.systemui.statusbar.StatusBarIconView; +import com.android.systemui.statusbar.notification.PhysicsProperty; import com.android.systemui.statusbar.notification.Roundable; import com.android.systemui.statusbar.notification.RoundableState; import com.android.systemui.statusbar.notification.headsup.PinnedStatus; @@ -58,6 +61,20 @@ import java.util.List; * An abstract view for expandable views. */ public abstract class ExpandableView extends FrameLayout implements Dumpable, Roundable { + public static final int TAG_ANIMATOR_HEIGHT = R.id.height_animator_tag; + public static final PhysicsProperty HEIGHT_PROPERTY = new PhysicsProperty(TAG_ANIMATOR_HEIGHT, + new FloatProperty<>("ActualHeight") { + + @Override + public Float get(View view) { + return (float) ((ExpandableView) view).getActualHeight(); + } + + @Override + public void setValue(@NonNull View view, float value) { + ((ExpandableView) view).setActualHeight((int) value); + } + }); private static final String TAG = "ExpandableView"; /** whether the dump() for this class should include verbose details */ protected static final boolean DUMP_VERBOSE = Compile.IS_DEBUG @@ -84,7 +101,8 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable, Ro protected float mContentTransformationAmount; protected boolean mIsLastChild; protected int mContentShift; - @NonNull private final ExpandableViewState mViewState; + @NonNull + private final ExpandableViewState mViewState; private float mContentTranslation; protected boolean mLastInSection; protected boolean mFirstInSection; @@ -205,7 +223,7 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable, Ro MeasureSpec.EXACTLY); } child.measure(getChildMeasureSpec( - widthMeasureSpec, viewHorizontalPadding, layoutParams.width), + widthMeasureSpec, viewHorizontalPadding, layoutParams.width), childHeightSpec); int childHeight = child.getMeasuredHeight(); maxChildHeight = Math.max(maxChildHeight, childHeight); @@ -223,7 +241,7 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable, Ro // Now that we know our own height, measure the children that are MATCH_PARENT for (View child : mMatchParentViews) { child.measure(getChildMeasureSpec( - widthMeasureSpec, viewHorizontalPadding, child.getLayoutParams().width), + widthMeasureSpec, viewHorizontalPadding, child.getLayoutParams().width), exactlyOwnHeightSpec); } mMatchParentViews.clear(); @@ -269,12 +287,29 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable, Ro } /** + * Sets the final value of the actual height, which is to be applied immediately without + * animation. This may be different than the current value if we're animating away an offset. + */ + public void setFinalActualHeight(int childHeight) { + if (physicalNotificationMovement()) { + HEIGHT_PROPERTY.setFinalValue(this, childHeight); + } else { + setActualHeight(childHeight); + } + } + + /** + * Once the physical notification movement flag is enabled, don't use + * this directly as a public method since it may not update the property values and misbehave + * during animations. Use #setFinalActualHeight instead. + * * Sets the actual height of this notification. This is different than the laid out * {@link View#getHeight()}, as we want to avoid layouting during scrolling and expanding. * - * @param actualHeight The height of this notification. + * @param actualHeight The height of this notification. * @param notifyListeners Whether the listener should be informed about the change. */ + @Deprecated public void setActualHeight(int actualHeight, boolean notifyListeners) { if (mActualHeight != actualHeight) { mActualHeight = actualHeight; @@ -285,7 +320,7 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable, Ro } } - public void setActualHeight(int actualHeight) { + protected void setActualHeight(int actualHeight) { setActualHeight(actualHeight, true /* notifyListeners */); } @@ -748,7 +783,8 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable, Ro * * @return the ExpandableView's view state. */ - @NonNull public ExpandableViewState getViewState() { + @NonNull + public ExpandableViewState getViewState() { return mViewState; } @@ -840,9 +876,10 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable, Ro * Set how much this notification is transformed into the shelf. * * @param contentTransformationAmount A value from 0 to 1 indicating how much we are transformed - * to the content away - * @param isLastChild is this the last child in the list. If true, then the transformation is - * different since its content fades out. + * to the content away + * @param isLastChild is this the last child in the list. If true, then the + * transformation is + * different since its content fades out. */ public void setContentTransformationAmount(float contentTransformationAmount, boolean isLastChild) { @@ -971,8 +1008,9 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable, Ro public interface OnHeightChangedListener { /** - * @param view the view for which the height changed, or {@code null} if just the top - * padding or the padding between the elements changed + * @param view the view for which the height changed, or {@code null} if just the + * top + * padding or the padding between the elements changed * @param needsAnimation whether the view height needs to be animated */ void onHeightChanged(ExpandableView view, boolean needsAnimation); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AnimationProperties.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AnimationProperties.java index 00b9aa42ab26..3d60092cf29a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AnimationProperties.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AnimationProperties.java @@ -23,6 +23,8 @@ import android.util.Property; import android.view.View; import android.view.animation.Interpolator; +import androidx.dynamicanimation.animation.DynamicAnimation; + import java.util.function.Consumer; /** @@ -77,6 +79,34 @@ public class AnimationProperties { } /** + * @return a listener that will be added for a given property during its animation. Similar to + * the finish listener but used for Dynamic / SpringAnimations + */ + public DynamicAnimation.OnAnimationEndListener getAnimationEndListener(Property property) { + if (mAnimationEndAction == null && mAnimationCancelAction == null) { + return null; + } + Consumer<Property> cancelAction = mAnimationCancelAction; + Consumer<Property> endAction = mAnimationEndAction; + return (animation, canceled, value, velocity) -> { + if (canceled && cancelAction != null) { + cancelAction.accept(property); + } else if (!canceled && endAction != null) { + endAction.accept(property); + } + }; + } + + /** + * @return a listener that is invoked when a property animation starts, used for dynamic + * animations. For classical, interpolator based animations used the listeneradapter instead, + * this is only for Dynamic Animations + */ + public Consumer<DynamicAnimation> getAnimationStartListener(Property property) { + return null; + } + + /** * Add a callback for animation cancellation. */ public AnimationProperties setAnimationCancelAction(Consumer<Property> listener) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ExpandableViewState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ExpandableViewState.java index 69c9a4bf2dbb..8cf9dd365b60 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ExpandableViewState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ExpandableViewState.java @@ -16,23 +16,31 @@ package com.android.systemui.statusbar.notification.stack; +import static com.android.systemui.Flags.physicalNotificationMovement; +import static com.android.systemui.statusbar.notification.row.ExpandableView.HEIGHT_PROPERTY; +import static com.android.systemui.statusbar.notification.row.ExpandableView.TAG_ANIMATOR_HEIGHT; + import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.PropertyValuesHolder; import android.animation.ValueAnimator; +import android.util.FloatProperty; import android.view.View; +import androidx.annotation.NonNull; + import com.android.app.animation.Interpolators; import com.android.systemui.res.R; +import com.android.systemui.statusbar.notification.PhysicsProperty; +import com.android.systemui.statusbar.notification.PhysicsPropertyAnimator; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; /** -* A state of an expandable view -*/ + * A state of an expandable view + */ public class ExpandableViewState extends ViewState { - private static final int TAG_ANIMATOR_HEIGHT = R.id.height_animator_tag; private static final int TAG_ANIMATOR_TOP_INSET = R.id.top_inset_animator_tag; private static final int TAG_ANIMATOR_BOTTOM_INSET = R.id.bottom_inset_animator_tag; private static final int TAG_END_HEIGHT = R.id.height_animator_end_value_tag; @@ -149,7 +157,7 @@ public class ExpandableViewState extends ViewState { // apply height if (height != newHeight) { - expandableView.setActualHeight(newHeight, false /* notifyListeners */); + expandableView.setFinalActualHeight(newHeight); } // apply hiding sensitive @@ -186,8 +194,24 @@ public class ExpandableViewState extends ViewState { // start height animation if (this.height != expandableView.getActualHeight()) { - startHeightAnimation(expandableView, properties); - } else { + if (mUsePhysicsForMovement) { + boolean animateHeight = properties.getAnimationFilter().animateHeight; + if (animateHeight) { + expandableView.setActualHeightAnimating(true); + } + PhysicsPropertyAnimator.setProperty(child, HEIGHT_PROPERTY, this.height, properties, + animateHeight, + (animation, canceled, value, velocity) -> { + expandableView.setActualHeightAnimating(false); + if (!canceled && child instanceof ExpandableNotificationRow) { + ((ExpandableNotificationRow) child).setGroupExpansionChanging( + false /* isExpansionChanging */); + } + }); + } else { + startHeightAnimationInterpolator(expandableView, properties); + } + } else { abortAnimation(child, TAG_ANIMATOR_HEIGHT); } @@ -224,7 +248,8 @@ public class ExpandableViewState extends ViewState { } } - private void startHeightAnimation(final ExpandableView child, AnimationProperties properties) { + private void startHeightAnimationInterpolator(final ExpandableView child, + AnimationProperties properties) { Integer previousStartValue = getChildTag(child, TAG_START_HEIGHT); Integer previousEndValue = getChildTag(child, TAG_END_HEIGHT); int newEndValue = this.height; @@ -374,38 +399,16 @@ public class ExpandableViewState extends ViewState { } }); startAnimator(animator, listener); - child.setTag(clipTop ? TAG_ANIMATOR_TOP_INSET:TAG_ANIMATOR_BOTTOM_INSET, animator); - child.setTag(clipTop ? TAG_START_TOP_INSET: TAG_START_BOTTOM_INSET, + child.setTag(clipTop ? TAG_ANIMATOR_TOP_INSET : TAG_ANIMATOR_BOTTOM_INSET, animator); + child.setTag(clipTop ? TAG_START_TOP_INSET : TAG_START_BOTTOM_INSET, clipTop ? child.getClipTopAmount() : child.getClipBottomAmount()); - child.setTag(clipTop ? TAG_END_TOP_INSET: TAG_END_BOTTOM_INSET, newEndValue); - } - - /** - * Get the end value of the height animation running on a view or the actualHeight - * if no animation is running. - */ - public static int getFinalActualHeight(ExpandableView view) { - if (view == null) { - return 0; - } - ValueAnimator heightAnimator = getChildTag(view, TAG_ANIMATOR_HEIGHT); - if (heightAnimator == null) { - return view.getActualHeight(); - } else { - return getChildTag(view, TAG_END_HEIGHT); - } + child.setTag(clipTop ? TAG_END_TOP_INSET : TAG_END_BOTTOM_INSET, newEndValue); } @Override public void cancelAnimations(View view) { super.cancelAnimations(view); - Animator animator = getChildTag(view, TAG_ANIMATOR_HEIGHT); - if (animator != null) { - animator.cancel(); - } - animator = getChildTag(view, TAG_ANIMATOR_TOP_INSET); - if (animator != null) { - animator.cancel(); - } + abortAnimation(view, TAG_ANIMATOR_HEIGHT); + abortAnimation(view, TAG_ANIMATOR_TOP_INSET); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java index ee57d459e71c..1d185356626b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java @@ -1352,10 +1352,11 @@ public class NotificationChildrenContainer extends ViewGroup if (i < maxAllowedVisibleChildren) { float singleLineHeight = child.getShowingLayout().getMinHeight( false /* likeGroupExpanded */); - child.setActualHeight((int) NotificationUtils.interpolate(singleLineHeight, - childHeight, fraction), false); + childHeight = NotificationUtils.interpolate(singleLineHeight, + childHeight, fraction); + child.setFinalActualHeight((int) childHeight); } else { - child.setActualHeight((int) childHeight, false); + child.setFinalActualHeight((int) childHeight); } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index c694a19a46ae..3d60e03d7ca4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -26,6 +26,7 @@ import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_ import static com.android.internal.jank.InteractionJankMonitor.CUJ_SHADE_CLEAR_ALL; import static com.android.systemui.Flags.magneticNotificationSwipes; import static com.android.systemui.Flags.notificationOverExpansionClippingFix; +import static com.android.systemui.Flags.physicalNotificationMovement; import static com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt.BUCKET_SILENT; import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_SWIPE; import static com.android.systemui.statusbar.notification.stack.shared.model.AccessibilityScrollEvent.SCROLL_DOWN; @@ -109,6 +110,7 @@ import com.android.systemui.statusbar.notification.FakeShadowView; import com.android.systemui.statusbar.notification.LaunchAnimationParameters; import com.android.systemui.statusbar.notification.NotificationTransitionAnimatorController; import com.android.systemui.statusbar.notification.NotificationUtils; +import com.android.systemui.statusbar.notification.PhysicsPropertyAnimator; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; @@ -5761,7 +5763,12 @@ public class NotificationStackScrollLayout + view.getActualHeight() - mShelf.getIntrinsicHeight(); } } else if (!firstVisibleView) { - view.setTranslationY(wakeUplocation); + if (physicalNotificationMovement()) { + PhysicsPropertyAnimator.setProperty(view, PhysicsPropertyAnimator.Y_TRANSLATION, + wakeUplocation); + } else { + view.setTranslationY(wakeUplocation); + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java index c783250f2e0a..5e0d57ebb3fe 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackStateAnimator.java @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.notification.stack; +import static com.android.systemui.Flags.physicalNotificationMovement; +import static com.android.systemui.statusbar.notification.row.ExpandableView.HEIGHT_PROPERTY; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_APPEAR; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_CYCLING_IN; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_CYCLING_OUT; @@ -29,11 +31,14 @@ import android.content.Context; import android.util.Property; import android.view.View; +import androidx.dynamicanimation.animation.DynamicAnimation; + import com.android.app.animation.Interpolators; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.res.R; import com.android.systemui.shared.clocks.AnimatableClockView; import com.android.systemui.statusbar.NotificationShelf; +import com.android.systemui.statusbar.notification.PhysicsPropertyAnimator; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; import com.android.systemui.statusbar.notification.row.StackScrollerDecorView; @@ -41,6 +46,7 @@ import com.android.systemui.statusbar.notification.row.StackScrollerDecorView; import java.util.ArrayList; import java.util.HashSet; import java.util.Stack; +import java.util.function.Consumer; /** * An stack state animator which handles animations to new StackScrollStates @@ -68,8 +74,10 @@ public class StackStateAnimator { public static final int DELAY_EFFECT_MAX_INDEX_DIFFERENCE = 2; private static final int MAX_STAGGER_COUNT = 5; - @VisibleForTesting int mGoToFullShadeAppearingTranslation; - @VisibleForTesting float mHeadsUpAppearStartAboveScreen; + @VisibleForTesting + int mGoToFullShadeAppearingTranslation; + @VisibleForTesting + float mHeadsUpAppearStartAboveScreen; // Padding between the old and new heads up notifications for the hun cycling animation private float mHeadsUpCyclingPadding; private final ExpandableViewState mTmpState = new ExpandableViewState(); @@ -80,8 +88,9 @@ public class StackStateAnimator { private ArrayList<View> mNewAddChildren = new ArrayList<>(); private HashSet<View> mHeadsUpAppearChildren = new HashSet<>(); private HashSet<View> mHeadsUpDisappearChildren = new HashSet<>(); - private HashSet<Animator> mAnimatorSet = new HashSet<>(); + private HashSet<Object> mAnimatorSet = new HashSet<>(); private Stack<AnimatorListenerAdapter> mAnimationListenerPool = new Stack<>(); + private Stack<DynamicAnimation.OnAnimationEndListener> mAnimationEndPool = new Stack<>(); private AnimationFilter mAnimationFilter = new AnimationFilter(); private long mCurrentLength; private long mCurrentAdditionalDelay; @@ -99,6 +108,9 @@ public class StackStateAnimator { mHostLayout = hostLayout; initView(context); mAnimationProperties = new AnimationProperties() { + + private final Consumer<DynamicAnimation> mDynamicAnimationConsumer = mAnimatorSet::add; + @Override public AnimationFilter getAnimationFilter() { return mAnimationFilter; @@ -110,6 +122,17 @@ public class StackStateAnimator { } @Override + public DynamicAnimation.OnAnimationEndListener getAnimationEndListener( + Property property) { + return getGlobalAnimationEndListener(); + } + + @Override + public Consumer<DynamicAnimation> getAnimationStartListener(Property property) { + return mDynamicAnimationConsumer; + } + + @Override public boolean wasAdded(View view) { return mNewAddChildren.contains(view); } @@ -187,11 +210,11 @@ public class StackStateAnimator { adaptDurationWhenGoingToFullShade(child, viewState, wasAdded, animationStaggerCount); mAnimationProperties.delay = 0; if (wasAdded || mAnimationFilter.hasDelays - && (viewState.getYTranslation() != child.getTranslationY() - || viewState.getZTranslation() != child.getTranslationZ() - || viewState.getAlpha() != child.getAlpha() - || viewState.height != child.getActualHeight() - || viewState.clipTopAmount != child.getClipTopAmount())) { + && (viewState.getYTranslation() != child.getTranslationY() + || viewState.getZTranslation() != child.getTranslationZ() + || viewState.getAlpha() != child.getAlpha() + || viewState.height != child.getActualHeight() + || viewState.clipTopAmount != child.getClipTopAmount())) { mAnimationProperties.delay = mCurrentAdditionalDelay + calculateChildAnimationDelay(viewState, animationStaggerCount); } @@ -209,7 +232,13 @@ public class StackStateAnimator { mAnimationProperties.duration = ANIMATION_DURATION_APPEAR_DISAPPEAR + 50 + (long) (100 * longerDurationFactor); } - child.setTranslationY(viewState.getYTranslation() + startOffset); + float newTranslationY = viewState.getYTranslation() + startOffset; + if (physicalNotificationMovement()) { + PhysicsPropertyAnimator.setProperty(child, PhysicsPropertyAnimator.Y_TRANSLATION, + newTranslationY); + } else { + child.setTranslationY(newTranslationY); + } } } @@ -312,7 +341,7 @@ public class StackStateAnimator { /** * @return an adapter which ensures that onAnimationFinished is called once no animation is - * running anymore + * running anymore */ private AnimatorListenerAdapter getGlobalAnimationFinishedListener() { if (!mAnimationListenerPool.empty()) { @@ -345,6 +374,27 @@ public class StackStateAnimator { }; } + /** + * @return an adapter which ensures that onAnimationFinished is called once no animation is + * running anymore + */ + private DynamicAnimation.OnAnimationEndListener getGlobalAnimationEndListener() { + if (!mAnimationEndPool.empty()) { + return mAnimationEndPool.pop(); + } + return new DynamicAnimation.OnAnimationEndListener() { + @Override + public void onAnimationEnd(DynamicAnimation animation, boolean canceled, float value, + float velocity) { + mAnimatorSet.remove(animation); + if (mAnimatorSet.isEmpty() && !canceled) { + onAnimationFinished(); + } + mAnimationEndPool.push(this); + } + }; + } + private void onAnimationFinished() { mHostLayout.onChildAnimationFinished(); @@ -358,7 +408,7 @@ public class StackStateAnimator { * Process the animationEvents for a new animation. Here is the place to do something custom, * like to modify the ViewState or to create a custom animation for an event. * - * @param animationEvents the animation events for the animation to perform + * @param animationEvents the animation events for the animation to perform * @return true if any custom animation was created */ private boolean processAnimationEvents( @@ -428,7 +478,7 @@ public class StackStateAnimator { translationDirection = ((viewState.getYTranslation() - (ownPosition + actualHeight / 2.0f)) * 2 / actualHeight); - translationDirection = Math.max(Math.min(translationDirection, 1.0f),-1.0f); + translationDirection = Math.max(Math.min(translationDirection, 1.0f), -1.0f); } Runnable postAnimation; @@ -446,7 +496,7 @@ public class StackStateAnimator { changingView.removeFromTransientContainer(); }; } else { - startAnimation = ()-> { + startAnimation = () -> { changingView.setInRemovalAnimation(true); }; postAnimation = () -> { @@ -460,7 +510,7 @@ public class StackStateAnimator { ExpandableView.ClipSide.BOTTOM); needsCustomAnimation = true; } else if (event.animationType == - NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT) { + NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_REMOVE_SWIPED_OUT) { boolean isFullySwipedOut = mHostLayout.isFullySwipedOut(changingView); if (loggable) { mLogger.processAnimationEventsRemoveSwipeOut(key, isFullySwipedOut, isHeadsUp); @@ -699,8 +749,8 @@ public class StackStateAnimator { /** * @param headsUpFromBottom Whether we are showing the HUNs at the bottom of the screen - * @param oldHunHeight Height of the old HUN - * @param newHunHeight Height of the new HUN + * @param oldHunHeight Height of the old HUN + * @param newHunHeight Height of the new HUN * @return The y translation target value of the HUN cycling out animation */ private float getHeadsUpCyclingOutYTranslation( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ViewState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ViewState.java index b2ffa4aa8233..2ef6f362af34 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ViewState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ViewState.java @@ -16,6 +16,10 @@ package com.android.systemui.statusbar.notification.stack; +import static com.android.systemui.Flags.physicalNotificationMovement; +import static com.android.systemui.statusbar.notification.PhysicsPropertyAnimator.TAG_ANIMATOR_TRANSLATION_Y; +import static com.android.systemui.statusbar.notification.PhysicsPropertyAnimator.Y_TRANSLATION; + import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; @@ -26,14 +30,20 @@ import android.util.Property; import android.view.View; import android.view.animation.Interpolator; +import androidx.dynamicanimation.animation.DynamicAnimation; +import androidx.dynamicanimation.animation.SpringAnimation; + import com.android.app.animation.Interpolators; import com.android.systemui.Dumpable; import com.android.systemui.res.R; import com.android.systemui.statusbar.notification.AnimatableProperty; import com.android.systemui.statusbar.notification.NotificationFadeAware.FadeOptimizedNotification; +import com.android.systemui.statusbar.notification.PhysicsProperty; +import com.android.systemui.statusbar.notification.PhysicsPropertyAnimator; import com.android.systemui.statusbar.notification.PropertyAnimator; -import com.android.systemui.statusbar.notification.row.ExpandableView; +import com.android.systemui.statusbar.notification.PropertyData; import com.android.systemui.statusbar.notification.headsup.HeadsUpUtil; +import com.android.systemui.statusbar.notification.row.ExpandableView; import java.io.PrintWriter; import java.lang.reflect.Field; @@ -46,6 +56,14 @@ import java.lang.reflect.Modifier; */ public class ViewState implements Dumpable { + public ViewState() { + this(physicalNotificationMovement()); + } + + public ViewState(boolean usePhysicsForMovement) { + setUsePhysicsForMovement(usePhysicsForMovement); + } + /** * Some animation properties that can be used to update running animations but not creating * any new ones. @@ -59,7 +77,6 @@ public class ViewState implements Dumpable { } }; private static final int TAG_ANIMATOR_TRANSLATION_X = R.id.translation_x_animator_tag; - private static final int TAG_ANIMATOR_TRANSLATION_Y = R.id.translation_y_animator_tag; private static final int TAG_ANIMATOR_TRANSLATION_Z = R.id.translation_z_animator_tag; private static final int TAG_ANIMATOR_ALPHA = R.id.alpha_animator_tag; private static final int TAG_END_TRANSLATION_X = R.id.translation_x_animator_end_value_tag; @@ -72,8 +89,7 @@ public class ViewState implements Dumpable { private static final int TAG_START_ALPHA = R.id.alpha_animator_start_value_tag; private static final String LOG_TAG = "StackViewState"; - private static final AnimatableProperty SCALE_X_PROPERTY - = new AnimatableProperty() { + private static final AnimatableProperty SCALE_X_PROPERTY = new AnimatableProperty() { @Override public int getAnimationStartTag() { @@ -96,8 +112,7 @@ public class ViewState implements Dumpable { } }; - private static final AnimatableProperty SCALE_Y_PROPERTY - = new AnimatableProperty() { + private static final AnimatableProperty SCALE_Y_PROPERTY = new AnimatableProperty() { @Override public int getAnimationStartTag() { @@ -129,11 +144,16 @@ public class ViewState implements Dumpable { private float mZTranslation; private float mScaleX = 1.0f; private float mScaleY = 1.0f; + protected boolean mUsePhysicsForMovement = false; public float getAlpha() { return mAlpha; } + public void setUsePhysicsForMovement(boolean usePhysicsForMovement) { + this.mUsePhysicsForMovement = usePhysicsForMovement; + } + /** * @param alpha View transparency. */ @@ -230,6 +250,7 @@ public class ViewState implements Dumpable { hidden = viewState.hidden; mScaleX = viewState.mScaleX; mScaleY = viewState.mScaleY; + mUsePhysicsForMovement = viewState.mUsePhysicsForMovement; } public void initFrom(View view) { @@ -261,11 +282,15 @@ public class ViewState implements Dumpable { } // apply yTranslation - boolean animatingY = isAnimating(view, TAG_ANIMATOR_TRANSLATION_Y); - if (animatingY) { - updateAnimationY(view); - } else if (view.getTranslationY() != this.mYTranslation) { - view.setTranslationY(this.mYTranslation); + if (mUsePhysicsForMovement) { + PhysicsPropertyAnimator.setProperty(view, Y_TRANSLATION, this.mYTranslation); + } else { + boolean animatingY = isAnimating(view, TAG_ANIMATOR_TRANSLATION_Y); + if (animatingY) { + updateAnimationY(view); + } else if (view.getTranslationY() != this.mYTranslation) { + view.setTranslationY(this.mYTranslation); + } } // apply zTranslation @@ -293,8 +318,8 @@ public class ViewState implements Dumpable { } int oldVisibility = view.getVisibility(); - boolean becomesInvisible = this.mAlpha == 0.0f - || (this.hidden && (!isAnimating(view) || oldVisibility != View.VISIBLE)); + boolean becomesInvisible = this.mAlpha == 0.0f || (this.hidden && (!isAnimating(view) + || oldVisibility != View.VISIBLE)); boolean animatingAlpha = isAnimating(view, TAG_ANIMATOR_ALPHA); if (animatingAlpha) { updateAlphaAnimation(view); @@ -315,9 +340,8 @@ public class ViewState implements Dumpable { } else { boolean newLayerTypeIsHardware = becomesFaded && view.hasOverlappingRendering(); int layerType = view.getLayerType(); - int newLayerType = newLayerTypeIsHardware - ? View.LAYER_TYPE_HARDWARE - : View.LAYER_TYPE_NONE; + int newLayerType = + newLayerTypeIsHardware ? View.LAYER_TYPE_HARDWARE : View.LAYER_TYPE_NONE; if (layerType != newLayerType) { view.setLayerType(newLayerType, null); } @@ -360,11 +384,19 @@ public class ViewState implements Dumpable { } private static boolean isAnimating(View view, int tag) { - return getChildTag(view, tag) != null; + Object childTag = getChildTag(view, tag); + if (childTag instanceof PropertyData propertyData) { + return propertyData.getAnimator() != null; + } + return childTag != null; } public static boolean isAnimating(View view, AnimatableProperty property) { - return getChildTag(view, property.getAnimatorTag()) != null; + Object childTag = getChildTag(view, property.getAnimatorTag()); + if (childTag instanceof PropertyData propertyData) { + return propertyData.getAnimator() != null; + } + return childTag != null; } /** @@ -376,8 +408,7 @@ public class ViewState implements Dumpable { public void animateTo(View child, AnimationProperties animationProperties) { boolean wasVisible = child.getVisibility() == View.VISIBLE; final float alpha = this.mAlpha; - if (!wasVisible && (alpha != 0 || child.getAlpha() != 0) - && !this.gone && !this.hidden) { + if (!wasVisible && (alpha != 0 || child.getAlpha() != 0) && !this.gone && !this.hidden) { child.setVisibility(View.VISIBLE); } float childAlpha = child.getAlpha(); @@ -465,8 +496,8 @@ public class ViewState implements Dumpable { } } - ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.ALPHA, - child.getAlpha(), newEndValue); + ObjectAnimator animator = ObjectAnimator.ofFloat(child, View.ALPHA, child.getAlpha(), + newEndValue); animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); // Handle layer type child.setLayerType(View.LAYER_TYPE_HARDWARE, null); @@ -516,8 +547,7 @@ public class ViewState implements Dumpable { startZTranslationAnimation(view, NO_NEW_ANIMATIONS); } - private void updateAnimation(View view, AnimatableProperty property, - float endValue) { + private void updateAnimation(View view, AnimatableProperty property, float endValue) { PropertyAnimator.startAnimation(view, property, endValue, NO_NEW_ANIMATIONS); } @@ -615,8 +645,8 @@ public class ViewState implements Dumpable { child.getTranslationX(), newEndValue); Interpolator customInterpolator = properties.getCustomInterpolator(child, View.TRANSLATION_X); - Interpolator interpolator = customInterpolator != null ? customInterpolator - : Interpolators.FAST_OUT_SLOW_IN; + Interpolator interpolator = + customInterpolator != null ? customInterpolator : Interpolators.FAST_OUT_SLOW_IN; animator.setInterpolator(interpolator); long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator); animator.setDuration(newDuration); @@ -649,6 +679,24 @@ public class ViewState implements Dumpable { } private void startYTranslationAnimation(final View child, AnimationProperties properties) { + if (mUsePhysicsForMovement) { + // Y Translation does some extra calls when it ends, so lets add a listener + DynamicAnimation.OnAnimationEndListener endListener = + (animation, canceled, value, velocity) -> { + if (!canceled) { + HeadsUpUtil.setNeedsHeadsUpDisappearAnimationAfterClick(child, false); + onYTranslationAnimationFinished(child); + } + }; + PhysicsPropertyAnimator.setProperty(child, Y_TRANSLATION, this.mYTranslation, + properties, properties.getAnimationFilter().animateY, endListener); + } else { + startYTranslationInterpolatorAnimation(child, properties); + } + } + + private void startYTranslationInterpolatorAnimation(View child, + AnimationProperties properties) { Float previousStartValue = getChildTag(child, TAG_START_TRANSLATION_Y); Float previousEndValue = getChildTag(child, TAG_END_TRANSLATION_Y); float newEndValue = this.mYTranslation; @@ -681,8 +729,8 @@ public class ViewState implements Dumpable { child.getTranslationY(), newEndValue); Interpolator customInterpolator = properties.getCustomInterpolator(child, View.TRANSLATION_Y); - Interpolator interpolator = customInterpolator != null ? customInterpolator - : Interpolators.FAST_OUT_SLOW_IN; + Interpolator interpolator = + customInterpolator != null ? customInterpolator : Interpolators.FAST_OUT_SLOW_IN; animator.setInterpolator(interpolator); long newDuration = cancelAnimatorAndGetNewDuration(properties.duration, previousAnimator); animator.setDuration(newDuration); @@ -731,9 +779,19 @@ public class ViewState implements Dumpable { } protected void abortAnimation(View child, int animatorTag) { - Animator previousAnimator = getChildTag(child, animatorTag); - if (previousAnimator != null) { - previousAnimator.cancel(); + Object storedTag = getChildTag(child, animatorTag); + if (storedTag != null) { + if (storedTag instanceof Animator animator) { + animator.cancel(); + } else if (storedTag instanceof PropertyData propertyData) { + // Physics based animation! + Runnable delayRunnable = propertyData.getDelayRunnable(); + child.removeCallbacks(delayRunnable); + SpringAnimation animator = propertyData.getAnimator(); + if (animator != null) { + animator.cancel(); + } + } } } @@ -750,46 +808,15 @@ public class ViewState implements Dumpable { if (previousAnimator != null) { // We take either the desired length of the new animation or the remaining time of // the previous animator, whichever is longer. - newDuration = Math.max(previousAnimator.getDuration() - - previousAnimator.getCurrentPlayTime(), newDuration); + newDuration = Math.max( + previousAnimator.getDuration() - previousAnimator.getCurrentPlayTime(), + newDuration); previousAnimator.cancel(); } return newDuration; } /** - * Get the end value of the xTranslation animation running on a view or the xTranslation - * if no animation is running. - */ - public static float getFinalTranslationX(View view) { - if (view == null) { - return 0; - } - ValueAnimator xAnimator = getChildTag(view, TAG_ANIMATOR_TRANSLATION_X); - if (xAnimator == null) { - return view.getTranslationX(); - } else { - return getChildTag(view, TAG_END_TRANSLATION_X); - } - } - - /** - * Get the end value of the yTranslation animation running on a view or the yTranslation - * if no animation is running. - */ - public static float getFinalTranslationY(View view) { - if (view == null) { - return 0; - } - ValueAnimator yAnimator = getChildTag(view, TAG_ANIMATOR_TRANSLATION_Y); - if (yAnimator == null) { - return view.getTranslationY(); - } else { - return getChildTag(view, TAG_END_TRANSLATION_Y); - } - } - - /** * Get the end value of the zTranslation animation running on a view or the zTranslation * if no animation is running. */ @@ -806,26 +833,14 @@ public class ViewState implements Dumpable { } public static boolean isAnimatingY(View child) { - return getChildTag(child, TAG_ANIMATOR_TRANSLATION_Y) != null; + return isAnimating(child, TAG_ANIMATOR_TRANSLATION_Y); } public void cancelAnimations(View view) { - Animator animator = getChildTag(view, TAG_ANIMATOR_TRANSLATION_X); - if (animator != null) { - animator.cancel(); - } - animator = getChildTag(view, TAG_ANIMATOR_TRANSLATION_Y); - if (animator != null) { - animator.cancel(); - } - animator = getChildTag(view, TAG_ANIMATOR_TRANSLATION_Z); - if (animator != null) { - animator.cancel(); - } - animator = getChildTag(view, TAG_ANIMATOR_ALPHA); - if (animator != null) { - animator.cancel(); - } + abortAnimation(view, TAG_ANIMATOR_TRANSLATION_X); + abortAnimation(view, TAG_ANIMATOR_TRANSLATION_Y); + abortAnimation(view, TAG_ANIMATOR_TRANSLATION_Z); + abortAnimation(view, TAG_ANIMATOR_ALPHA); } @Override @@ -840,8 +855,8 @@ public class ViewState implements Dumpable { // Print field names paired with their values for (Field field : fields) { int modifiers = field.getModifiers(); - if (Modifier.isStatic(modifiers) || field.isSynthetic() - || Modifier.isTransient(modifiers)) { + if (Modifier.isStatic(modifiers) || field.isSynthetic() || Modifier.isTransient( + modifiers)) { continue; } if (!first) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java index 4825a10e901b..15d73d2deb7a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java @@ -735,6 +735,7 @@ public class NotificationIconContainer extends ViewGroup { private final Consumer<Property> mCannedAnimationEndListener; public IconState(View child) { + super(false /* usePhysicsForMovement */); mView = child; mCannedAnimationEndListener = (property) -> { // If we finished animating out of the shelf diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusIconContainer.java index 144939d1086f..38c0d281b320 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusIconContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusIconContainer.java @@ -443,6 +443,11 @@ public class StatusIconContainer extends AlphaOptimizedLinearLayout { } public static class StatusIconState extends ViewState { + + public StatusIconState() { + super(false /* usePhysicsForMovement */); + } + /// StatusBarIconView.STATE_* public int visibleState = STATE_ICON; public boolean justAdded = true; |