Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 403730de authored by Peter Liang's avatar Peter Liang
Browse files

Refactor the design and improve the animations of Accessibility Floating Menu(11/n).

Goals:
Show the dock tooltip to indicate users how to use the feature of moving the menu to be tucked.

Bug: 227715451
Test: atest MenuAnimationControllerTest
Change-Id: Idd0d85f7d333701a7545e99a20164243e0ce12ec
parent cb655881
Loading
Loading
Loading
Loading
+59 −6
Original line number Diff line number Diff line
@@ -25,6 +25,9 @@ import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.OvershootInterpolator;
import android.view.animation.TranslateAnimation;

import androidx.dynamicanimation.animation.DynamicAnimation;
import androidx.dynamicanimation.animation.FlingAnimation;
@@ -33,6 +36,7 @@ import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;
import androidx.recyclerview.widget.RecyclerView;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;

import java.util.HashMap;
@@ -55,7 +59,11 @@ class MenuAnimationController {
    private static final float SPRING_AFTER_FLING_DAMPING_RATIO = 0.85f;
    private static final float SPRING_STIFFNESS = 700f;
    private static final float ESCAPE_VELOCITY = 750f;
    // Make tucked animation by using translation X relative to the view itself.
    private static final float ANIMATION_TO_X_VALUE = 0.5f;

    private static final int ANIMATION_START_OFFSET_MS = 600;
    private static final int ANIMATION_DURATION_MS = 600;
    private static final int FADE_OUT_DURATION_MS = 1000;
    private static final int FADE_EFFECT_DURATION_MS = 3000;

@@ -64,10 +72,12 @@ class MenuAnimationController {
    private final Handler mHandler;
    private boolean mIsFadeEffectEnabled;
    private DismissAnimationController.DismissCallback mDismissCallback;
    private Runnable mSpringAnimationsEndAction;

    // Cache the animations state of {@link DynamicAnimation.TRANSLATION_X} and {@link
    // DynamicAnimation.TRANSLATION_Y} to be well controlled by the touch handler
    private final HashMap<DynamicAnimation.ViewProperty, DynamicAnimation> mPositionAnimations =
    @VisibleForTesting
    final HashMap<DynamicAnimation.ViewProperty, DynamicAnimation> mPositionAnimations =
            new HashMap<>();

    MenuAnimationController(MenuView menuView) {
@@ -102,6 +112,13 @@ class MenuAnimationController {
        }
    }

    /**
     * Sets the action to be called when the all dynamic animations are completed.
     */
    void setSpringAnimationsEndAction(Runnable runnable) {
        mSpringAnimationsEndAction = runnable;
    }

    void setDismissCallback(
            DismissAnimationController.DismissCallback dismissCallback) {
        mDismissCallback = dismissCallback;
@@ -192,7 +209,7 @@ class MenuAnimationController {
                        ? bounds.right
                        : bounds.bottom;

        final FlingAnimation flingAnimation = new FlingAnimation(mMenuView, menuPositionProperty);
        final FlingAnimation flingAnimation = createFlingAnimation(mMenuView, menuPositionProperty);
        flingAnimation.setFriction(friction)
                .setStartVelocity(velocity)
                .setMinValue(Math.min(currentValue, min))
@@ -217,7 +234,14 @@ class MenuAnimationController {
        flingAnimation.start();
    }

    private void springMenuWith(DynamicAnimation.ViewProperty property, SpringForce spring,
    @VisibleForTesting
    FlingAnimation createFlingAnimation(MenuView menuView,
            MenuPositionProperty menuPositionProperty) {
        return new FlingAnimation(menuView, menuPositionProperty);
    }

    @VisibleForTesting
    void springMenuWith(DynamicAnimation.ViewProperty property, SpringForce spring,
            float velocity, float finalPosition) {
        final MenuPositionProperty menuPositionProperty = new MenuPositionProperty(property);
        final SpringAnimation springAnimation =
@@ -228,8 +252,13 @@ class MenuAnimationController {
                                return;
                            }

                            onSpringAnimationEnd(new PointF(mMenuView.getTranslationX(),
                            final boolean areAnimationsRunning =
                                    mPositionAnimations.values().stream().anyMatch(
                                            DynamicAnimation::isRunning);
                            if (!areAnimationsRunning) {
                                onSpringAnimationsEnd(new PointF(mMenuView.getTranslationX(),
                                        mMenuView.getTranslationY()));
                            }
                        })
                        .setStartVelocity(velocity);

@@ -332,11 +361,15 @@ class MenuAnimationController {
                .start();
    }

    private void onSpringAnimationEnd(PointF position) {
    private void onSpringAnimationsEnd(PointF position) {
        mMenuView.onBoundsInParentChanged((int) position.x, (int) position.y);
        constrainPositionAndUpdate(position);

        fadeOutIfEnabled();

        if (mSpringAnimationsEndAction != null) {
            mSpringAnimationsEndAction.run();
        }
    }

    private void constrainPositionAndUpdate(PointF position) {
@@ -387,6 +420,26 @@ class MenuAnimationController {
        mHandler.removeCallbacksAndMessages(/* token= */ null);
    }

    void startTuckedAnimationPreview() {
        fadeInNowIfEnabled();

        final float toXValue = isOnLeftSide()
                ? -ANIMATION_TO_X_VALUE
                : ANIMATION_TO_X_VALUE;
        final TranslateAnimation animation =
                new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0,
                        Animation.RELATIVE_TO_SELF, toXValue,
                        Animation.RELATIVE_TO_SELF, 0,
                        Animation.RELATIVE_TO_SELF, 0);
        animation.setDuration(ANIMATION_DURATION_MS);
        animation.setRepeatMode(Animation.REVERSE);
        animation.setInterpolator(new OvershootInterpolator());
        animation.setRepeatCount(Animation.INFINITE);
        animation.setStartOffset(ANIMATION_START_OFFSET_MS);

        mMenuView.startAnimation(animation);
    }

    private Handler createUiHandler() {
        return new Handler(requireNonNull(Looper.myLooper(), "looper must not be null"));
    }
+12 −0
Original line number Diff line number Diff line
@@ -56,6 +56,7 @@ class MenuInfoRepository {
    @FloatRange(from = 0.0, to = 1.0)
    private static final float DEFAULT_MENU_POSITION_Y_PERCENT = 0.77f;
    private static final boolean DEFAULT_MOVE_TO_TUCKED_VALUE = false;
    private static final boolean DEFAULT_HAS_SEEN_DOCK_TOOLTIP_VALUE = false;
    private static final int DEFAULT_MIGRATION_TOOLTIP_VALUE_PROMPT = MigrationPrompt.DISABLED;

    private final Context mContext;
@@ -114,6 +115,12 @@ class MenuInfoRepository {
                        DEFAULT_MOVE_TO_TUCKED_VALUE));
    }

    void loadDockTooltipVisibility(OnInfoReady<Boolean> callback) {
        callback.onReady(Prefs.getBoolean(mContext,
                Prefs.Key.HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP,
                DEFAULT_HAS_SEEN_DOCK_TOOLTIP_VALUE));
    }

    void loadMigrationTooltipVisibility(OnInfoReady<Boolean> callback) {
        callback.onReady(Settings.Secure.getIntForUser(mContext.getContentResolver(),
                ACCESSIBILITY_FLOATING_MENU_MIGRATION_TOOLTIP_PROMPT,
@@ -153,6 +160,11 @@ class MenuInfoRepository {
                percentagePosition.toString());
    }

    void updateDockTooltipVisibility(boolean hasSeen) {
        Prefs.putBoolean(mContext, Prefs.Key.HAS_SEEN_ACCESSIBILITY_FLOATING_MENU_DOCK_TOOLTIP,
                hasSeen);
    }

    void updateMigrationTooltipVisibility(boolean visible) {
        Settings.Secure.putIntForUser(mContext.getContentResolver(),
                ACCESSIBILITY_FLOATING_MENU_MIGRATION_TOOLTIP_PROMPT,
+29 −0
Original line number Diff line number Diff line
@@ -89,10 +89,13 @@ class MenuViewLayer extends FrameLayout implements
    private final IAccessibilityFloatingMenu mFloatingMenu;
    private final DismissAnimationController mDismissAnimationController;
    private final MenuViewModel mMenuViewModel;
    private final Observer<Boolean> mDockTooltipObserver =
            this::onDockTooltipVisibilityChanged;
    private final Observer<Boolean> mMigrationTooltipObserver =
            this::onMigrationTooltipVisibilityChanged;
    private final Rect mImeInsetsRect = new Rect();
    private boolean mIsMigrationTooltipShowing;
    private boolean mShouldShowDockTooltip;
    private Optional<MenuEduTooltipView> mEduTooltipView = Optional.empty();

    @IntDef({
@@ -111,10 +114,12 @@ class MenuViewLayer extends FrameLayout implements

    @StringDef({
            TooltipType.MIGRATION,
            TooltipType.DOCK,
    })
    @Retention(RetentionPolicy.SOURCE)
    @interface TooltipType {
        String MIGRATION = "migration";
        String DOCK = "dock";
    }

    @VisibleForTesting
@@ -154,6 +159,7 @@ class MenuViewLayer extends FrameLayout implements
        mMenuView = new MenuView(context, mMenuViewModel, mMenuViewAppearance);
        mMenuAnimationController = mMenuView.getMenuAnimationController();
        mMenuAnimationController.setDismissCallback(this::hideMenuAndShowMessage);
        mMenuAnimationController.setSpringAnimationsEndAction(this::onSpringAnimationsEndAction);
        mDismissView = new DismissView(context);
        mDismissAnimationController = new DismissAnimationController(mDismissView, mMenuView);
        mDismissAnimationController.setMagnetListener(new MagnetizedObject.MagnetListener() {
@@ -236,6 +242,7 @@ class MenuViewLayer extends FrameLayout implements
        setOnClickListener(this);
        setOnApplyWindowInsetsListener((view, insets) -> onWindowInsetsApplied(insets));
        getViewTreeObserver().addOnComputeInternalInsetsListener(this);
        mMenuViewModel.getDockTooltipVisibilityData().observeForever(mDockTooltipObserver);
        mMenuViewModel.getMigrationTooltipVisibilityData().observeForever(
                mMigrationTooltipObserver);
        mMessageView.setUndoListener(view -> undo());
@@ -250,6 +257,7 @@ class MenuViewLayer extends FrameLayout implements
        setOnClickListener(null);
        setOnApplyWindowInsetsListener(null);
        getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
        mMenuViewModel.getDockTooltipVisibilityData().removeObserver(mDockTooltipObserver);
        mMenuViewModel.getMigrationTooltipVisibilityData().removeObserver(
                mMigrationTooltipObserver);
        mHandler.removeCallbacksAndMessages(/* token= */ null);
@@ -306,6 +314,21 @@ class MenuViewLayer extends FrameLayout implements
        }
    }

    private void onDockTooltipVisibilityChanged(boolean hasSeenTooltip) {
        mShouldShowDockTooltip = !hasSeenTooltip;
    }

    private void onSpringAnimationsEndAction() {
        if (mShouldShowDockTooltip) {
            mEduTooltipView = Optional.of(new MenuEduTooltipView(mContext, mMenuViewAppearance));
            mEduTooltipView.ifPresent(view -> addTooltipView(view,
                    getContext().getText(R.string.accessibility_floating_button_docking_tooltip),
                    TooltipType.DOCK));

            mMenuAnimationController.startTuckedAnimationPreview();
        }
    }

    private CharSequence getMigrationMessage() {
        final Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_DETAILS_SETTINGS);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
@@ -341,6 +364,12 @@ class MenuViewLayer extends FrameLayout implements
            mIsMigrationTooltipShowing = false;
        }

        if (tooltipView.getTag().equals(TooltipType.DOCK)) {
            mMenuViewModel.updateDockTooltipVisibility(/* hasSeen= */ true);
            mMenuView.clearAnimation();
            mShouldShowDockTooltip = false;
        }

        removeView(tooltipView);

        mMenuListViewTouchHandler.setOnActionDownEndListener(null);
+10 −0
Original line number Diff line number Diff line
@@ -36,6 +36,7 @@ class MenuViewModel implements MenuInfoRepository.OnSettingsContentsChanged {
    private final MutableLiveData<MenuFadeEffectInfo> mFadeEffectInfoData =
            new MutableLiveData<>();
    private final MutableLiveData<Boolean> mMoveToTuckedData = new MutableLiveData<>();
    private final MutableLiveData<Boolean> mDockTooltipData = new MutableLiveData<>();
    private final MutableLiveData<Boolean> mMigrationTooltipData = new MutableLiveData<>();
    private final MutableLiveData<Position> mPercentagePositionData = new MutableLiveData<>();
    private final MenuInfoRepository mInfoRepository;
@@ -67,6 +68,10 @@ class MenuViewModel implements MenuInfoRepository.OnSettingsContentsChanged {
        mInfoRepository.updateMenuSavingPosition(percentagePosition);
    }

    void updateDockTooltipVisibility(boolean hasSeen) {
        mInfoRepository.updateDockTooltipVisibility(hasSeen);
    }

    void updateMigrationTooltipVisibility(boolean visible) {
        mInfoRepository.updateMigrationTooltipVisibility(visible);
    }
@@ -76,6 +81,11 @@ class MenuViewModel implements MenuInfoRepository.OnSettingsContentsChanged {
        return mMoveToTuckedData;
    }

    LiveData<Boolean> getDockTooltipVisibilityData() {
        mInfoRepository.loadDockTooltipVisibility(mDockTooltipData::setValue);
        return mDockTooltipData;
    }

    LiveData<Boolean> getMigrationTooltipVisibilityData() {
        mInfoRepository.loadMigrationTooltipVisibility(mMigrationTooltipData::setValue);
        return mMigrationTooltipData;
+125 −2
Original line number Diff line number Diff line
@@ -20,8 +20,10 @@ import static com.google.common.truth.Truth.assertThat;

import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;

import android.graphics.PointF;
import android.testing.AndroidTestingRunner;
@@ -30,6 +32,10 @@ import android.view.View;
import android.view.ViewPropertyAnimator;
import android.view.WindowManager;

import androidx.dynamicanimation.animation.DynamicAnimation;
import androidx.dynamicanimation.animation.FlingAnimation;
import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;
import androidx.test.filters.SmallTest;

import com.android.systemui.Prefs;
@@ -39,6 +45,9 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;

import java.util.Optional;

/** Tests for {@link MenuAnimationController}. */
@RunWith(AndroidTestingRunner.class)
@@ -47,9 +56,10 @@ import org.junit.runner.RunWith;
public class MenuAnimationControllerTest extends SysuiTestCase {

    private boolean mLastIsMoveToTucked;
    private ArgumentCaptor<DynamicAnimation.OnAnimationEndListener> mEndListenerCaptor;
    private ViewPropertyAnimator mViewPropertyAnimator;
    private MenuView mMenuView;
    private MenuAnimationController mMenuAnimationController;
    private TestMenuAnimationController mMenuAnimationController;

    @Before
    public void setUp() throws Exception {
@@ -62,15 +72,17 @@ public class MenuAnimationControllerTest extends SysuiTestCase {
        mViewPropertyAnimator = spy(mMenuView.animate());
        doReturn(mViewPropertyAnimator).when(mMenuView).animate();

        mMenuAnimationController = new MenuAnimationController(mMenuView);
        mMenuAnimationController = new TestMenuAnimationController(mMenuView);
        mLastIsMoveToTucked = Prefs.getBoolean(mContext,
                Prefs.Key.HAS_ACCESSIBILITY_FLOATING_MENU_TUCKED, /* defaultValue= */ false);
        mEndListenerCaptor = ArgumentCaptor.forClass(DynamicAnimation.OnAnimationEndListener.class);
    }

    @After
    public void tearDown() throws Exception {
        Prefs.putBoolean(mContext, Prefs.Key.HAS_ACCESSIBILITY_FLOATING_MENU_TUCKED,
                mLastIsMoveToTucked);
        mEndListenerCaptor.getAllValues().clear();
    }

    @Test
@@ -122,4 +134,115 @@ public class MenuAnimationControllerTest extends SysuiTestCase {

        assertThat(isMoveToTucked).isFalse();
    }

    @Test
    public void startTuckedAnimationPreview_hasAnimation() {
        mMenuView.clearAnimation();

        mMenuAnimationController.startTuckedAnimationPreview();

        assertThat(mMenuView.getAnimation()).isNotNull();
    }

    @Test
    public void startSpringAnimationsAndEndOneAnimation_notTriggerEndAction() {
        final Runnable onSpringAnimationsEndCallback = mock(Runnable.class);
        mMenuAnimationController.setSpringAnimationsEndAction(onSpringAnimationsEndCallback);

        setupAndRunSpringAnimations();
        final Optional<DynamicAnimation> anyAnimation =
                mMenuAnimationController.mPositionAnimations.values().stream().findAny();
        anyAnimation.ifPresent(this::skipAnimationToEnd);

        verifyZeroInteractions(onSpringAnimationsEndCallback);
    }

    @Test
    public void startAndEndSpringAnimations_triggerEndAction() {
        final Runnable onSpringAnimationsEndCallback = mock(Runnable.class);
        mMenuAnimationController.setSpringAnimationsEndAction(onSpringAnimationsEndCallback);

        setupAndRunSpringAnimations();
        mMenuAnimationController.mPositionAnimations.values().forEach(this::skipAnimationToEnd);

        verify(onSpringAnimationsEndCallback).run();
    }

    @Test
    public void flingThenSpringAnimationsAreEnded_triggerEndAction() {
        final Runnable onSpringAnimationsEndCallback = mock(Runnable.class);
        mMenuAnimationController.setSpringAnimationsEndAction(onSpringAnimationsEndCallback);

        mMenuAnimationController.flingMenuThenSpringToEdge(/* x= */ 0, /* velocityX= */
                100, /* velocityY= */ 100);
        mMenuAnimationController.mPositionAnimations.values()
                .forEach(animation -> verify((FlingAnimation) animation).addEndListener(
                        mEndListenerCaptor.capture()));
        mEndListenerCaptor.getAllValues()
                .forEach(listener -> listener.onAnimationEnd(mock(DynamicAnimation.class),
                        /* canceled */ false, /* endValue */ 0, /* endVelocity */ 0));
        mMenuAnimationController.mPositionAnimations.values().forEach(this::skipAnimationToEnd);

        verify(onSpringAnimationsEndCallback).run();
    }

    @Test
    public void existFlingIsRunningAndTheOtherAreEnd_notTriggerEndAction() {
        final Runnable onSpringAnimationsEndCallback = mock(Runnable.class);
        mMenuAnimationController.setSpringAnimationsEndAction(onSpringAnimationsEndCallback);

        mMenuAnimationController.flingMenuThenSpringToEdge(/* x= */ 0, /* velocityX= */
                200, /* velocityY= */ 200);
        mMenuAnimationController.mPositionAnimations.values()
                .forEach(animation -> verify((FlingAnimation) animation).addEndListener(
                        mEndListenerCaptor.capture()));
        final Optional<DynamicAnimation.OnAnimationEndListener> anyAnimation =
                mEndListenerCaptor.getAllValues().stream().findAny();
        anyAnimation.ifPresent(
                listener -> listener.onAnimationEnd(mock(DynamicAnimation.class), /* canceled */
                        false, /* endValue */ 0, /* endVelocity */ 0));
        mMenuAnimationController.mPositionAnimations.values()
                .stream()
                .filter(animation -> animation instanceof SpringAnimation)
                .forEach(this::skipAnimationToEnd);

        verifyZeroInteractions(onSpringAnimationsEndCallback);
    }

    private void setupAndRunSpringAnimations() {
        final float stiffness = 700f;
        final float dampingRatio = 0.85f;
        final float velocity = 100f;
        final float finalPosition = 300f;

        mMenuAnimationController.springMenuWith(DynamicAnimation.TRANSLATION_X, new SpringForce()
                .setStiffness(stiffness)
                .setDampingRatio(dampingRatio), velocity, finalPosition);
        mMenuAnimationController.springMenuWith(DynamicAnimation.TRANSLATION_Y, new SpringForce()
                .setStiffness(stiffness)
                .setDampingRatio(dampingRatio), velocity, finalPosition);
    }

    private void skipAnimationToEnd(DynamicAnimation animation) {
        final SpringAnimation springAnimation = ((SpringAnimation) animation);
        // The doAnimationFrame function is used for skipping animation to the end.
        springAnimation.doAnimationFrame(100);
        springAnimation.skipToEnd();
        springAnimation.doAnimationFrame(200);
    }

    /**
     * Wrapper class for testing.
     */
    private static class TestMenuAnimationController extends MenuAnimationController {
        TestMenuAnimationController(MenuView menuView) {
            super(menuView);
        }

        @Override
        FlingAnimation createFlingAnimation(MenuView menuView,
                MenuPositionProperty menuPositionProperty) {
            return spy(super.createFlingAnimation(menuView, menuPositionProperty));
        }
    }
}