Implement smooth FAB animations for displacement due to IME visibility changes
For the FAB to remember where it was before being displaced by an IME,
it needs to avoid persisting its position (updating the internal percentage-position value)
This change adds a parameter to onPositionChanged(), as well as our spring animation functions,
that controls whether they persist the position,
allowing us to use them for handling IME displacement.
Bug: 281150010
Test: adb sync system_ext && adb shell stop && adb shell start
Change-Id: Ic259db5d2612737aada2dd0440efce661754677b
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 9d3200d..1bc49fb 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -476,6 +476,8 @@
"motion_tool_lib",
"androidx.core_core-animation-testing-nodeps",
"androidx.compose.ui_ui",
+ "flag-junit",
+ "platform-test-annotations",
],
}
diff --git a/packages/SystemUI/aconfig/accessibility.aconfig b/packages/SystemUI/aconfig/accessibility.aconfig
index bcf1535..08ecf09b 100644
--- a/packages/SystemUI/aconfig/accessibility.aconfig
+++ b/packages/SystemUI/aconfig/accessibility.aconfig
@@ -8,3 +8,10 @@
description: "Adjusts bounds to allow the floating menu to render on top of navigation bars."
bug: "283768342"
}
+
+flag {
+ name: "floating_menu_ime_displacement_animation"
+ namespace: "accessibility"
+ description: "Adds an animation for when the FAB is displaced by an IME becoming visible."
+ bug: "281150010"
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java
index 105de16c..cd8bef1 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java
@@ -38,6 +38,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
+import com.android.systemui.Flags;
import java.util.HashMap;
@@ -91,16 +92,48 @@
}
void moveToPosition(PointF position) {
- moveToPositionX(position.x);
- moveToPositionY(position.y);
+ moveToPosition(position, /* animateMovement = */ false);
+ }
+
+ /* Moves position without updating underlying percentage position. Can be animated. */
+ void moveToPosition(PointF position, boolean animateMovement) {
+ if (Flags.floatingMenuImeDisplacementAnimation()) {
+ moveToPositionX(position.x, animateMovement);
+ moveToPositionY(position.y, animateMovement);
+ } else {
+ moveToPositionX(position.x, /* animateMovement = */ false);
+ moveToPositionY(position.y, /* animateMovement = */ false);
+ }
}
void moveToPositionX(float positionX) {
- DynamicAnimation.TRANSLATION_X.setValue(mMenuView, positionX);
+ moveToPositionX(positionX, /* animateMovement = */ false);
}
- private void moveToPositionY(float positionY) {
- DynamicAnimation.TRANSLATION_Y.setValue(mMenuView, positionY);
+ void moveToPositionX(float positionX, boolean animateMovement) {
+ if (animateMovement && Flags.floatingMenuImeDisplacementAnimation()) {
+ springMenuWith(DynamicAnimation.TRANSLATION_X,
+ createSpringForce(),
+ /* velocity = */ 0,
+ positionX, /* writeToPosition = */ false);
+ } else {
+ DynamicAnimation.TRANSLATION_X.setValue(mMenuView, positionX);
+ }
+ }
+
+ void moveToPositionY(float positionY) {
+ moveToPositionY(positionY, /* animateMovement = */ false);
+ }
+
+ void moveToPositionY(float positionY, boolean animateMovement) {
+ if (animateMovement && Flags.floatingMenuImeDisplacementAnimation()) {
+ springMenuWith(DynamicAnimation.TRANSLATION_Y,
+ createSpringForce(),
+ /* velocity = */ 0,
+ positionY, /* writeToPosition = */ false);
+ } else {
+ DynamicAnimation.TRANSLATION_Y.setValue(mMenuView, positionY);
+ }
}
void moveToPositionYIfNeeded(float positionY) {
@@ -151,7 +184,7 @@
void moveAndPersistPosition(PointF position) {
moveToPosition(position);
mMenuView.onBoundsInParentChanged((int) position.x, (int) position.y);
- constrainPositionAndUpdate(position);
+ constrainPositionAndUpdate(position, /* writeToPosition = */ true);
}
void removeMenu() {
@@ -180,17 +213,13 @@
flingThenSpringMenuWith(DynamicAnimation.TRANSLATION_X,
startXVelocity,
FLING_FRICTION_SCALAR,
- new SpringForce()
- .setStiffness(SPRING_STIFFNESS)
- .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
+ createSpringForce(),
finalPositionX);
flingThenSpringMenuWith(DynamicAnimation.TRANSLATION_Y,
velocityY,
FLING_FRICTION_SCALAR,
- new SpringForce()
- .setStiffness(SPRING_STIFFNESS)
- .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO),
+ createSpringForce(),
/* finalPosition= */ null);
}
@@ -226,7 +255,8 @@
final float endPosition = finalPosition != null
? finalPosition
: Math.max(min, Math.min(max, endValue));
- springMenuWith(property, spring, endVelocity, endPosition);
+ springMenuWith(property, spring, endVelocity, endPosition,
+ /* writeToPosition = */ true);
});
cancelAnimation(property);
@@ -242,7 +272,7 @@
@VisibleForTesting
void springMenuWith(DynamicAnimation.ViewProperty property, SpringForce spring,
- float velocity, float finalPosition) {
+ float velocity, float finalPosition, boolean writeToPosition) {
final MenuPositionProperty menuPositionProperty = new MenuPositionProperty(property);
final SpringAnimation springAnimation =
new SpringAnimation(mMenuView, menuPositionProperty)
@@ -257,7 +287,7 @@
DynamicAnimation::isRunning);
if (!areAnimationsRunning) {
onSpringAnimationsEnd(new PointF(mMenuView.getTranslationX(),
- mMenuView.getTranslationY()));
+ mMenuView.getTranslationY()), writeToPosition);
}
})
.setStartVelocity(velocity);
@@ -281,7 +311,8 @@
if (currentXTranslation < draggableBounds.left
|| currentXTranslation > draggableBounds.right) {
constrainPositionAndUpdate(
- new PointF(mMenuView.getTranslationX(), mMenuView.getTranslationY()));
+ new PointF(mMenuView.getTranslationX(), mMenuView.getTranslationY()),
+ /* writeToPosition = */ true);
moveToEdgeAndHide();
return true;
}
@@ -298,15 +329,19 @@
return mMenuView.isMoveToTucked();
}
- void moveToEdgeAndHide() {
- mMenuView.updateMenuMoveToTucked(/* isMoveToTucked= */ true);
-
+ PointF getTuckedMenuPosition() {
final PointF position = mMenuView.getMenuPosition();
final float menuHalfWidth = mMenuView.getMenuWidth() / 2.0f;
final float endX = isOnLeftSide()
? position.x - menuHalfWidth
: position.x + menuHalfWidth;
- moveToPosition(new PointF(endX, position.y));
+ return new PointF(endX, position.y);
+ }
+
+ void moveToEdgeAndHide() {
+ mMenuView.updateMenuMoveToTucked(/* isMoveToTucked= */ true);
+ final PointF position = mMenuView.getMenuPosition();
+ moveToPosition(getTuckedMenuPosition());
// Keep the touch region let users could click extra space to pop up the menu view
// from the screen edge
@@ -335,6 +370,11 @@
mPositionAnimations.get(property).cancel();
}
+ @VisibleForTesting
+ DynamicAnimation getAnimation(DynamicAnimation.ViewProperty property) {
+ return mPositionAnimations.getOrDefault(property, null);
+ }
+
void onDraggingStart() {
mMenuView.onDraggingStart();
}
@@ -361,9 +401,9 @@
.start();
}
- private void onSpringAnimationsEnd(PointF position) {
+ private void onSpringAnimationsEnd(PointF position, boolean writeToPosition) {
mMenuView.onBoundsInParentChanged((int) position.x, (int) position.y);
- constrainPositionAndUpdate(position);
+ constrainPositionAndUpdate(position, writeToPosition);
fadeOutIfEnabled();
@@ -372,7 +412,7 @@
}
}
- private void constrainPositionAndUpdate(PointF position) {
+ private void constrainPositionAndUpdate(PointF position, boolean writeToPosition) {
final Rect draggableBounds = mMenuView.getMenuDraggableBoundsExcludeIme();
// Have the space gap margin between the top bound and the menu view, so actually the
// position y range needs to cut the margin.
@@ -384,7 +424,12 @@
final float percentageY = position.y < 0 || draggableBounds.height() == 0
? MIN_PERCENT
: Math.min(MAX_PERCENT, position.y / draggableBounds.height());
- mMenuView.persistPositionAndUpdateEdge(new Position(percentageX, percentageY));
+
+ if (Flags.floatingMenuImeDisplacementAnimation() && !writeToPosition) {
+ mMenuView.onEdgeChangedIfNeeded();
+ } else {
+ mMenuView.persistPositionAndUpdateEdge(new Position(percentageX, percentageY));
+ }
}
void updateOpacityWith(boolean isFadeEffectEnabled, float newOpacityValue) {
@@ -463,4 +508,11 @@
mProperty.setValue(menuView, value);
}
}
+
+ @VisibleForTesting
+ static SpringForce createSpringForce() {
+ return new SpringForce()
+ .setStiffness(SPRING_STIFFNESS)
+ .setDampingRatio(SPRING_AFTER_FLING_DAMPING_RATIO);
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java
index e1612b0..ea5a56c 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java
@@ -54,7 +54,6 @@
private final List<AccessibilityTarget> mTargetFeatures = new ArrayList<>();
private final AccessibilityTargetAdapter mAdapter;
private final MenuViewModel mMenuViewModel;
- private final MenuAnimationController mMenuAnimationController;
private final Rect mBoundsInParent = new Rect();
private final RecyclerView mTargetFeaturesView;
private final ViewTreeObserver.OnDrawListener mSystemGestureExcludeUpdater =
@@ -70,6 +69,7 @@
private boolean mIsMoveToTucked;
+ private final MenuAnimationController mMenuAnimationController;
private OnTargetFeaturesChangeListener mFeaturesChangeListener;
MenuView(Context context, MenuViewModel menuViewModel, MenuViewAppearance menuViewAppearance) {
@@ -197,8 +197,30 @@
}
void onPositionChanged() {
- final PointF position = mMenuViewAppearance.getMenuPosition();
- mMenuAnimationController.moveToPosition(position);
+ onPositionChanged(/* animateMovement = */ false);
+ }
+
+ void onPositionChanged(boolean animateMovement) {
+ final PointF position;
+ if (isMoveToTucked()) {
+ position = mMenuAnimationController.getTuckedMenuPosition();
+ } else {
+ position = getMenuPosition();
+ }
+
+ // We can skip animating if FAB is not visible
+ if (Flags.floatingMenuImeDisplacementAnimation()
+ && animateMovement && getVisibility() == VISIBLE) {
+ mMenuAnimationController.moveToPosition(position, /* animateMovement = */ true);
+ // onArrivalAtPosition() is called at the end of the animation.
+ } else {
+ mMenuAnimationController.moveToPosition(position);
+ onArrivalAtPosition(); // no animation, so we call this immediately.
+ }
+ }
+
+ void onArrivalAtPosition() {
+ final PointF position = getMenuPosition();
onBoundsInParentChanged((int) position.x, (int) position.y);
if (isMoveToTucked()) {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
index b6e8997..fbca022 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java
@@ -59,6 +59,7 @@
import com.android.internal.accessibility.dialog.AccessibilityTarget;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
+import com.android.systemui.Flags;
import com.android.systemui.res.R;
import com.android.systemui.util.settings.SecureSettings;
import com.android.wm.shell.bubbles.DismissViewUtils;
@@ -331,7 +332,7 @@
mMenuViewAppearance.onImeVisibilityChanged(windowInsets.isVisible(ime()), imeTop);
mMenuView.onEdgeChanged();
- mMenuView.onPositionChanged();
+ mMenuView.onPositionChanged(/* animateMovement = */ true);
mImeInsetsRect.set(imeInsetsRect);
}
@@ -362,6 +363,10 @@
mMenuAnimationController.startTuckedAnimationPreview();
}
+
+ if (Flags.floatingMenuImeDisplacementAnimation()) {
+ mMenuView.onArrivalAtPosition();
+ }
}
private CharSequence getMigrationMessage() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java
index ec6ec63..e832940 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java
@@ -234,10 +234,12 @@
mMenuAnimationController.springMenuWith(DynamicAnimation.TRANSLATION_X, new SpringForce()
.setStiffness(stiffness)
- .setDampingRatio(dampingRatio), velocity, finalPosition);
+ .setDampingRatio(dampingRatio), velocity, finalPosition,
+ /* writeToPosition = */ true);
mMenuAnimationController.springMenuWith(DynamicAnimation.TRANSLATION_Y, new SpringForce()
.setStiffness(stiffness)
- .setDampingRatio(dampingRatio), velocity, finalPosition);
+ .setDampingRatio(dampingRatio), velocity, finalPosition,
+ /* writeToPosition = */ true);
}
private void skipAnimationToEnd(DynamicAnimation animation) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
index aed795a..76094c1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
@@ -42,6 +42,10 @@
import android.graphics.Rect;
import android.os.Build;
import android.os.UserHandle;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.provider.Settings;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
@@ -52,8 +56,10 @@
import android.view.accessibility.AccessibilityManager;
import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.test.filters.SmallTest;
+import com.android.systemui.Flags;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.util.settings.SecureSettings;
@@ -98,6 +104,10 @@
@Rule
public MockitoRule mockito = MockitoJUnit.rule();
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule =
+ DeviceFlagsValueProvider.createCheckFlagsRule();
+
@Mock
private IAccessibilityFloatingMenu mFloatingMenu;
@@ -116,7 +126,8 @@
final Rect mDisplayBounds = new Rect();
mDisplayBounds.set(/* left= */ 0, /* top= */ 0, DISPLAY_WINDOW_WIDTH,
DISPLAY_WINDOW_HEIGHT);
- mWindowMetrics = spy(new WindowMetrics(mDisplayBounds, fakeDisplayInsets()));
+ mWindowMetrics = spy(
+ new WindowMetrics(mDisplayBounds, fakeDisplayInsets(), /* density = */ 0.0f));
doReturn(mWindowMetrics).when(mStubWindowManager).getCurrentWindowMetrics();
mMenuViewLayer = new MenuViewLayer(mContext, mStubWindowManager, mStubAccessibilityManager,
@@ -221,7 +232,8 @@
}
@Test
- public void showingImeInsetsChange_overlapOnIme_menuShownAboveIme() {
+ @RequiresFlagsDisabled(Flags.FLAG_FLOATING_MENU_IME_DISPLACEMENT_ANIMATION)
+ public void showingImeInsetsChange_overlapOnIme_menuShownAboveIme_old() {
mMenuAnimationController.moveAndPersistPosition(new PointF(0, IME_TOP + 100));
final PointF beforePosition = mMenuView.getMenuPosition();
@@ -233,15 +245,49 @@
}
@Test
- public void hidingImeInsetsChange_overlapOnIme_menuBackToOriginalPosition() {
- final float menuTop = IME_TOP + 200;
- mMenuAnimationController.moveAndPersistPosition(new PointF(0, menuTop));
+ @RequiresFlagsEnabled(Flags.FLAG_FLOATING_MENU_IME_DISPLACEMENT_ANIMATION)
+ public void showingImeInsetsChange_overlapOnIme_menuShownAboveIme() {
+ mMenuAnimationController.moveAndPersistPosition(new PointF(0, IME_TOP + 100));
+ final PointF beforePosition = mMenuView.getMenuPosition();
+
dispatchShowingImeInsets();
+ assertThat(isPositionAnimationRunning()).isTrue();
+ skipPositionAnimations();
+
+ final float menuBottom = mMenuView.getTranslationY() + mMenuView.getMenuHeight();
+
+ assertThat(mMenuView.getTranslationX()).isEqualTo(beforePosition.x);
+ assertThat(menuBottom).isLessThan(beforePosition.y);
+ }
+
+ @Test
+ @RequiresFlagsDisabled(Flags.FLAG_FLOATING_MENU_IME_DISPLACEMENT_ANIMATION)
+ public void hidingImeInsetsChange_overlapOnIme_menuBackToOriginalPosition_old() {
+ mMenuAnimationController.moveAndPersistPosition(new PointF(0, IME_TOP + 200));
+ final PointF beforePosition = mMenuView.getMenuPosition();
dispatchHidingImeInsets();
- assertThat(mMenuView.getTranslationX()).isEqualTo(0);
- assertThat(mMenuView.getTranslationY()).isEqualTo(menuTop);
+ assertThat(mMenuView.getTranslationX()).isEqualTo(beforePosition.x);
+ assertThat(mMenuView.getTranslationY()).isEqualTo(beforePosition.y);
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_FLOATING_MENU_IME_DISPLACEMENT_ANIMATION)
+ public void hidingImeInsetsChange_overlapOnIme_menuBackToOriginalPosition() {
+ mMenuAnimationController.moveAndPersistPosition(new PointF(0, IME_TOP + 200));
+ final PointF beforePosition = mMenuView.getMenuPosition();
+
+ dispatchShowingImeInsets();
+ assertThat(isPositionAnimationRunning()).isTrue();
+ skipPositionAnimations();
+
+ dispatchHidingImeInsets();
+ assertThat(isPositionAnimationRunning()).isTrue();
+ skipPositionAnimations();
+
+ assertThat(mMenuView.getTranslationX()).isEqualTo(beforePosition.x);
+ assertThat(mMenuView.getTranslationY()).isEqualTo(beforePosition.y);
}
private void setupEnabledAccessibilityServiceList() {
@@ -294,4 +340,21 @@
Insets.of(/* left= */ 0, /* top= */ 0, /* right= */ 0, bottom))
.build();
}
+
+ private boolean isPositionAnimationRunning() {
+ return !mMenuAnimationController.mPositionAnimations.values().stream().filter(
+ (animation) -> animation.isRunning()).findAny().isEmpty();
+ }
+
+ private void skipPositionAnimations() {
+ mMenuAnimationController.mPositionAnimations.values().stream().forEach(
+ (animation) -> {
+ final SpringAnimation springAnimation = ((SpringAnimation) animation);
+ // The doAnimationFrame function is used for skipping animation to the end.
+ springAnimation.doAnimationFrame(500);
+ springAnimation.skipToEnd();
+ springAnimation.doAnimationFrame(500);
+ });
+
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/utils/FlagUtils.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/utils/FlagUtils.java
index 2975549..c7bb0f5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/utils/FlagUtils.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/utils/FlagUtils.java
@@ -31,5 +31,6 @@
*/
public static void setFlagDefaults(SetFlagsRule setFlagsRule) {
setFlagDefault(setFlagsRule, Flags.FLAG_FLOATING_MENU_OVERLAPS_NAV_BARS_FLAG);
+ setFlagDefault(setFlagsRule, Flags.FLAG_FLOATING_MENU_IME_DISPLACEMENT_ANIMATION);
}
}