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

Commit daf8ef3e authored by Tyler Freeman's avatar Tyler Freeman
Browse files

feat(magnification fling): add fling momentum velocity tracking to panning gesture

Make the fullscreen magnification nice and slippery, like a wet dog ice
skating with skates made of butter.

Unfortunately, we can't add tests to
FullScreenMagnificationControllerTest, because it uses mock animators
and custom Handler timing, but Scroller is wired directly to the real
ones which we can't fake/mock.

Fix: 319175022
Flag: ACONFIG services.accessibility.fullscreen_fling_gesture DISABLED
Test: atest com.android.server.accessibility.magnification.FullScreenMagnificationGestureHandlerTest
NO_IFTTT=just TODO comments

Change-Id: Ic37278895846ed46604bb506ecf41177b94b0d17
parent 85302d7d
Loading
Loading
Loading
Loading
+14 −0
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package android.view.accessibility;

import android.view.MagnificationSpec;

/**
 * A callback for magnification animation result.
 * @hide
@@ -31,4 +33,16 @@ public interface MagnificationAnimationCallback {
     *                change. Otherwise {@code false}
     */
    void onResult(boolean success);

    /**
     * Called when the animation is finished or interrupted during animating.
     *
     * @param success {@code true} if animating successfully with given spec or the spec did not
     *                change. Otherwise {@code false}
     * @param lastSpecSent the last spec that was sent to WindowManager for animation, in case you
     *                     need to update the local copy
     */
    default void onResult(boolean success, MagnificationSpec lastSpecSent) {
        onResult(success);
    }
}
 No newline at end of file
+7 −0
Original line number Diff line number Diff line
@@ -51,6 +51,13 @@ flag {
    bug: "300002193"
}

flag {
    name: "fullscreen_fling_gesture"
    namespace: "accessibility"
    description: "When true, adds a fling gesture animation for fullscreen magnification"
    bug: "319175022"
}

flag {
    name: "pinch_zoom_zero_min_span"
    namespace: "accessibility"
+224 −10
Original line number Diff line number Diff line
@@ -25,7 +25,9 @@ import static com.android.server.accessibility.AccessibilityManagerService.INVAL

import android.accessibilityservice.MagnificationConfig;
import android.animation.Animator;
import android.animation.TimeAnimator;
import android.animation.ValueAnimator;
import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.BroadcastReceiver;
@@ -51,6 +53,7 @@ import android.view.View;
import android.view.WindowManager;
import android.view.accessibility.MagnificationAnimationCallback;
import android.view.animation.DecelerateInterpolator;
import android.widget.Scroller;

import com.android.internal.R;
import com.android.internal.accessibility.common.MagnificationConstants;
@@ -60,6 +63,7 @@ import com.android.internal.util.function.pooled.PooledLambda;
import com.android.server.LocalServices;
import com.android.server.accessibility.AccessibilityManagerService;
import com.android.server.accessibility.AccessibilityTraceManager;
import com.android.server.accessibility.Flags;
import com.android.server.wm.WindowManagerInternal;

import java.util.ArrayList;
@@ -86,6 +90,7 @@ public class FullScreenMagnificationController implements
    private static final boolean DEBUG_SET_MAGNIFICATION_SPEC = false;

    private final Object mLock;
    private final Supplier<Scroller> mScrollerSupplier;

    private final ControllerContext mControllerCtx;

@@ -149,7 +154,8 @@ public class FullScreenMagnificationController implements

        DisplayMagnification(int displayId) {
            mDisplayId = displayId;
            mSpecAnimationBridge = new SpecAnimationBridge(mControllerCtx, mLock, mDisplayId);
            mSpecAnimationBridge =
                    new SpecAnimationBridge(mControllerCtx, mLock, mDisplayId, mScrollerSupplier);
        }

        /**
@@ -406,6 +412,41 @@ public class FullScreenMagnificationController implements
            }
        }

        void startFlingAnimation(
                float xPixelsPerSecond,
                float yPixelsPerSecond,
                MagnificationAnimationCallback animationCallback
        ) {
            if (DEBUG) {
                Slog.i(LOG_TAG,
                        "startFlingAnimation(spec = " + xPixelsPerSecond + ", animationCallback = "
                                + animationCallback + ")");
            }
            if (Thread.currentThread().getId() == mMainThreadId) {
                mSpecAnimationBridge.startFlingAnimation(
                        xPixelsPerSecond,
                        yPixelsPerSecond,
                        getMinOffsetXLocked(),
                        getMaxOffsetXLocked(),
                        getMinOffsetYLocked(),
                        getMaxOffsetYLocked(),
                        animationCallback);
            } else {
                final Message m =
                        PooledLambda.obtainMessage(
                                SpecAnimationBridge::startFlingAnimation,
                                mSpecAnimationBridge,
                                xPixelsPerSecond,
                                yPixelsPerSecond,
                                getMinOffsetXLocked(),
                                getMaxOffsetXLocked(),
                                getMinOffsetYLocked(),
                                getMaxOffsetYLocked(),
                                animationCallback);
                mControllerCtx.getHandler().sendMessage(m);
            }
        }

        /**
         * Get the ID of the last service that changed the magnification spec.
         *
@@ -759,6 +800,49 @@ public class FullScreenMagnificationController implements
            sendSpecToAnimation(mCurrentMagnificationSpec, null);
        }

        @GuardedBy("mLock")
        void startFling(float xPixelsPerSecond, float yPixelsPerSecond, int id) {
            if (!mRegistered) {
                return;
            }
            if (!isActivated()) {
                return;
            }

            if (id != INVALID_SERVICE_ID) {
                mIdOfLastServiceToMagnify = id;
            }

            startFlingAnimation(
                    xPixelsPerSecond,
                    yPixelsPerSecond,
                    new MagnificationAnimationCallback() {
                        @Override
                        public void onResult(boolean success) {
                            // never called
                        }

                        @Override
                        public void onResult(boolean success, MagnificationSpec lastSpecSent) {
                            if (DEBUG) {
                                Slog.i(
                                        LOG_TAG,
                                        "startFlingAnimation finished( "
                                                + success
                                                + " = "
                                                + lastSpecSent.offsetX
                                                + ", "
                                                + lastSpecSent.offsetY
                                                + ")");
                            }
                            synchronized (mLock) {
                                mCurrentMagnificationSpec.setTo(lastSpecSent);
                                onMagnificationChangedLocked();
                            }
                        }
                    });
        }

        boolean updateCurrentSpecWithOffsetsLocked(float nonNormOffsetX, float nonNormOffsetY) {
            if (DEBUG) {
                Slog.i(LOG_TAG,
@@ -838,7 +922,8 @@ public class FullScreenMagnificationController implements
                magnificationInfoChangedCallback,
                scaleProvider,
                /* thumbnailSupplier= */ null,
                backgroundExecutor);
                backgroundExecutor,
                () -> new Scroller(context));
    }

    /** Constructor for tests */
@@ -849,9 +934,11 @@ public class FullScreenMagnificationController implements
            @NonNull MagnificationInfoChangedCallback magnificationInfoChangedCallback,
            @NonNull MagnificationScaleProvider scaleProvider,
            Supplier<MagnificationThumbnail> thumbnailSupplier,
            @NonNull Executor backgroundExecutor) {
            @NonNull Executor backgroundExecutor,
            Supplier<Scroller> scrollerSupplier) {
        mControllerCtx = ctx;
        mLock = lock;
        mScrollerSupplier = scrollerSupplier;
        mMainThreadId = mControllerCtx.getContext().getMainLooper().getThread().getId();
        mScreenStateObserver = new ScreenStateObserver(mControllerCtx.getContext(), this);
        addInfoChangedCallback(magnificationInfoChangedCallback);
@@ -1436,6 +1523,26 @@ public class FullScreenMagnificationController implements
        }
    }

    /**
     * Call after a pan ends, if the velocity has passed the threshold, to start a fling animation.
     *
     * @param displayId The logical display id.
     * @param xPixelsPerSecond the velocity of the last pan gesture in the X direction, in current
     *     screen pixels per second.
     * @param yPixelsPerSecond the velocity of the last pan gesture in the Y direction, in current
     *     screen pixels per second.
     * @param id the ID of the service requesting the change
     */
    public void startFling(int displayId, float xPixelsPerSecond, float yPixelsPerSecond, int id) {
        synchronized (mLock) {
            final DisplayMagnification display = mDisplays.get(displayId);
            if (display == null) {
                return;
            }
            display.startFling(xPixelsPerSecond, yPixelsPerSecond, id);
        }
    }

    /**
     * Get the ID of the last service that changed the magnification spec.
     *
@@ -1698,7 +1805,14 @@ public class FullScreenMagnificationController implements
        @GuardedBy("mLock")
        private boolean mEnabled = false;

        private SpecAnimationBridge(ControllerContext ctx, Object lock, int displayId) {
        private final Scroller mScroller;
        private final TimeAnimator mScrollAnimator = new TimeAnimator();

        private SpecAnimationBridge(
                ControllerContext ctx,
                Object lock,
                int displayId,
                Supplier<Scroller> scrollerSupplier) {
            mControllerCtx = ctx;
            mLock = lock;
            mDisplayId = displayId;
@@ -1709,6 +1823,35 @@ public class FullScreenMagnificationController implements
            mValueAnimator.setFloatValues(0.0f, 1.0f);
            mValueAnimator.addUpdateListener(this);
            mValueAnimator.addListener(this);

            if (Flags.fullscreenFlingGesture()) {
                mScroller = scrollerSupplier.get();
                mScrollAnimator.addListener(this);
                mScrollAnimator.setTimeListener(
                        (animation, totalTime, deltaTime) -> {
                            synchronized (mLock) {
                                if (DEBUG) {
                                    Slog.v(
                                            LOG_TAG,
                                            "onScrollAnimationUpdate: "
                                                    + mEnabled + " : " + totalTime);
                                }

                                if (mEnabled) {
                                    if (!mScroller.computeScrollOffset()) {
                                        animation.end();
                                        return;
                                    }

                                    mEndMagnificationSpec.offsetX = mScroller.getCurrX();
                                    mEndMagnificationSpec.offsetY = mScroller.getCurrY();
                                    setMagnificationSpecLocked(mEndMagnificationSpec);
                                }
                            }
                        });
            } else {
                mScroller = null;
            }
        }

        /**
@@ -1735,16 +1878,20 @@ public class FullScreenMagnificationController implements
            }
        }

        void updateSentSpecMainThread(MagnificationSpec spec,
                MagnificationAnimationCallback animationCallback) {
            if (mValueAnimator.isRunning()) {
                mValueAnimator.cancel();
            }
        @MainThread
        void updateSentSpecMainThread(
                MagnificationSpec spec, MagnificationAnimationCallback animationCallback) {
            cancelAnimations();

            mAnimationCallback = animationCallback;
            // If the current and sent specs don't match, update the sent spec.
            synchronized (mLock) {
                final boolean changed = !mSentMagnificationSpec.equals(spec);
                if (DEBUG_SET_MAGNIFICATION_SPEC) {
                    Slog.d(
                            LOG_TAG,
                            "updateSentSpecMainThread: " + mEnabled + " : changed: " + changed);
                }
                if (changed) {
                    if (mAnimationCallback != null) {
                        animateMagnificationSpecLocked(spec);
@@ -1757,12 +1904,13 @@ public class FullScreenMagnificationController implements
            }
        }

        @MainThread
        private void sendEndCallbackMainThread(boolean success) {
            if (mAnimationCallback != null) {
                if (DEBUG) {
                    Slog.d(LOG_TAG, "sendEndCallbackMainThread: " + success);
                }
                mAnimationCallback.onResult(success);
                mAnimationCallback.onResult(success, mSentMagnificationSpec);
                mAnimationCallback = null;
            }
        }
@@ -1830,6 +1978,72 @@ public class FullScreenMagnificationController implements
        public void onAnimationRepeat(Animator animation) {

        }

        /**
         * Call after a pan ends, if the velocity has passed the threshold, to start a fling
         * animation.
         */
        @MainThread
        public void startFlingAnimation(
                float xPixelsPerSecond,
                float yPixelsPerSecond,
                float minX,
                float maxX,
                float minY,
                float maxY,
                MagnificationAnimationCallback animationCallback
        ) {
            if (!Flags.fullscreenFlingGesture()) {
                return;
            }
            cancelAnimations();

            mAnimationCallback = animationCallback;

            // We use this as a temp object to send updates every animation frame, so make sure it
            // matches the current spec before we start.
            mEndMagnificationSpec.setTo(mSentMagnificationSpec);

            if (DEBUG) {
                Slog.d(LOG_TAG, "startFlingAnimation: "
                        + "offsetX " + mSentMagnificationSpec.offsetX
                        + "offsetY " + mSentMagnificationSpec.offsetY
                        + "xPixelsPerSecond " + xPixelsPerSecond
                        + "yPixelsPerSecond " + yPixelsPerSecond
                        + "minX " + minX
                        + "maxX " + maxX
                        + "minY " + minY
                        + "maxY " + maxY
                );
            }

            mScroller.fling(
                    (int) mSentMagnificationSpec.offsetX,
                    (int) mSentMagnificationSpec.offsetY,
                    (int) xPixelsPerSecond,
                    (int) yPixelsPerSecond,
                    (int) minX,
                    (int) maxX,
                    (int) minY,
                    (int) maxY);

            mScrollAnimator.start();
        }

        @MainThread
        private void cancelAnimations() {
            if (mValueAnimator.isRunning()) {
                mValueAnimator.cancel();
            }

            if (!Flags.fullscreenFlingGesture()) {
                return;
            }
            if (mScrollAnimator.isRunning()) {
                mScrollAnimator.cancel();
            }
            mScroller.forceFinished(true);
        }
    }

    private static class ScreenStateObserver extends BroadcastReceiver {
+93 −8
Original line number Diff line number Diff line
@@ -62,6 +62,7 @@ import android.view.MotionEvent.PointerCoords;
import android.view.MotionEvent.PointerProperties;
import android.view.ScaleGestureDetector;
import android.view.ScaleGestureDetector.OnScaleGestureListener;
import android.view.VelocityTracker;
import android.view.ViewConfiguration;

import com.android.internal.R;
@@ -174,6 +175,10 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH

    private final boolean mIsWatch;

    @Nullable private VelocityTracker mVelocityTracker;
    private final int mMinimumVelocity;
    private final int mMaximumVelocity;

    public FullScreenMagnificationGestureHandler(@UiContext Context context,
            FullScreenMagnificationController fullScreenMagnificationController,
            AccessibilityTraceManager trace,
@@ -184,15 +189,25 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
            @NonNull WindowMagnificationPromptController promptController,
            int displayId,
            FullScreenMagnificationVibrationHelper fullScreenMagnificationVibrationHelper) {
        this(context, fullScreenMagnificationController, trace, callback,
                detectSingleFingerTripleTap, detectTwoFingerTripleTap,
                detectShortcutTrigger, promptController, displayId,
                fullScreenMagnificationVibrationHelper, /* magnificationLogger= */ null);
        this(
                context,
                fullScreenMagnificationController,
                trace,
                callback,
                detectSingleFingerTripleTap,
                detectTwoFingerTripleTap,
                detectShortcutTrigger,
                promptController,
                displayId,
                fullScreenMagnificationVibrationHelper,
                /* magnificationLogger= */ null,
                ViewConfiguration.get(context));
    }

    /** Constructor for tests. */
    @VisibleForTesting
    FullScreenMagnificationGestureHandler(@UiContext Context context,
    FullScreenMagnificationGestureHandler(
            @UiContext Context context,
            FullScreenMagnificationController fullScreenMagnificationController,
            AccessibilityTraceManager trace,
            Callback callback,
@@ -202,7 +217,8 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
            @NonNull WindowMagnificationPromptController promptController,
            int displayId,
            FullScreenMagnificationVibrationHelper fullScreenMagnificationVibrationHelper,
            MagnificationLogger magnificationLogger) {
            MagnificationLogger magnificationLogger,
            ViewConfiguration viewConfiguration) {
        super(displayId, detectSingleFingerTripleTap, detectTwoFingerTripleTap,
                detectShortcutTrigger, trace, callback);
        if (DEBUG_ALL) {
@@ -212,6 +228,15 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
                            + ", detectTwoFingerTripleTap = " + detectTwoFingerTripleTap
                            + ", detectShortcutTrigger = " + detectShortcutTrigger + ")");
        }

        if (Flags.fullscreenFlingGesture()) {
            mMinimumVelocity = viewConfiguration.getScaledMinimumFlingVelocity();
            mMaximumVelocity = viewConfiguration.getScaledMaximumFlingVelocity();
        } else {
            mMinimumVelocity = 0;
            mMaximumVelocity = 0;
        }

        mFullScreenMagnificationController = fullScreenMagnificationController;
        mMagnificationInfoChangedCallback =
                new FullScreenMagnificationController.MagnificationInfoChangedCallback() {
@@ -501,6 +526,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
                }
                persistScaleAndTransitionTo(mViewportDraggingState);
            } else if (action == ACTION_UP || action == ACTION_CANCEL) {
                onPanningFinished(event);
                // if feature flag is enabled, currently only true on watches
                if (mIsSinglePanningEnabled) {
                    mOverscrollHandler.setScaleAndCenterToEdgeIfNeeded();
@@ -578,6 +604,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
                Slog.i(mLogTag, "Panned content by scrollX: " + distanceX
                        + " scrollY: " + distanceY);
            }
            onPan(second);
            mFullScreenMagnificationController.offsetMagnifiedRegion(mDisplayId, distanceX,
                    distanceY, AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
            if (mIsSinglePanningEnabled) {
@@ -973,7 +1000,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
                                    && overscrollState(event, mFirstPointerDownLocation)
                                    == OVERSCROLL_VERTICAL_EDGE) {
                                transitionToDelegatingStateAndClear();
                            }
                            } // TODO(b/319537921): should there be an else here?
                            //Primary pointer is swiping, so transit to PanningScalingState
                            transitToPanningScalingStateAndClear();
                        } else if (mIsSinglePanningEnabled
@@ -982,7 +1009,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
                            if (overscrollState(event, mFirstPointerDownLocation)
                                    == OVERSCROLL_VERTICAL_EDGE) {
                                transitionToDelegatingStateAndClear();
                            }
                            } // TODO(b/319537921): should there be an else here?
                            transitToSinglePanningStateAndClear();
                        } else if (!mIsTwoFingerCountReached) {
                            // If it is a two-finger gesture, do not transition to the
@@ -1742,6 +1769,61 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
        }
    }

    /** Call during MOVE events for a panning gesture. */
    private void onPan(MotionEvent event) {
        if (!Flags.fullscreenFlingGesture()) {
            return;
        }

        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(event);
    }

    /**
     * Call during UP events for a panning gesture, so we can detect a fling and play a physics-
     * based fling animation.
     */
    private void onPanningFinished(MotionEvent event) {
        if (!Flags.fullscreenFlingGesture()) {
            return;
        }

        if (mVelocityTracker == null) {
            Log.e(mLogTag, "onPanningFinished: mVelocityTracker is null");
            return;
        }
        mVelocityTracker.addMovement(event);
        mVelocityTracker.computeCurrentVelocity(/* units= */ 1000, mMaximumVelocity);

        float xPixelsPerSecond = mVelocityTracker.getXVelocity();
        float yPixelsPerSecond = mVelocityTracker.getYVelocity();

        mVelocityTracker.recycle();
        mVelocityTracker = null;

        if (DEBUG_PANNING_SCALING) {
            Slog.v(
                    mLogTag,
                    "onPanningFinished: pixelsPerSecond: "
                            + xPixelsPerSecond
                            + ", "
                            + yPixelsPerSecond
                            + " mMinimumVelocity: "
                            + mMinimumVelocity);
        }

        if ((Math.abs(yPixelsPerSecond) > mMinimumVelocity)
                || (Math.abs(xPixelsPerSecond) > mMinimumVelocity)) {
            mFullScreenMagnificationController.startFling(
                    mDisplayId,
                    xPixelsPerSecond,
                    yPixelsPerSecond,
                    AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID);
        }
    }

    final class SinglePanningState extends SimpleOnGestureListener implements State {


@@ -1756,6 +1838,8 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
            int action = event.getActionMasked();
            switch (action) {
                case ACTION_UP:
                    onPanningFinished(event);
                    // fall-through!
                case ACTION_CANCEL:
                    mOverscrollHandler.setScaleAndCenterToEdgeIfNeeded();
                    mOverscrollHandler.clearEdgeState();
@@ -1770,6 +1854,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH
            if (mCurrentState != mSinglePanningState) {
                return true;
            }
            onPan(second);
            mFullScreenMagnificationController.offsetMagnifiedRegion(
                    mDisplayId,
                    distanceX,
+10 −7
Original line number Diff line number Diff line
@@ -57,6 +57,7 @@ import android.test.mock.MockContentResolver;
import android.view.DisplayInfo;
import android.view.MagnificationSpec;
import android.view.accessibility.MagnificationAnimationCallback;
import android.widget.Scroller;

import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
@@ -134,7 +135,8 @@ public class FullScreenMagnificationControllerTest {

    @Before
    public void setUp() {
        Looper looper = InstrumentationRegistry.getContext().getMainLooper();
        Context realContext = InstrumentationRegistry.getContext();
        Looper looper = realContext.getMainLooper();
        // Pretending ID of the Thread associated with looper as main thread ID in controller
        when(mMockContext.getMainLooper()).thenReturn(looper);
        when(mMockControllerCtx.getContext()).thenReturn(mMockContext);
@@ -168,7 +170,8 @@ public class FullScreenMagnificationControllerTest {
                        mRequestObserver,
                        mScaleProvider,
                        () -> mMockThumbnail,
                        ConcurrentUtils.DIRECT_EXECUTOR);
                        ConcurrentUtils.DIRECT_EXECUTOR,
                        () -> new Scroller(realContext));
    }

    @After
@@ -428,7 +431,7 @@ public class FullScreenMagnificationControllerTest {
        mTargetAnimationListener.onAnimationUpdate(mMockValueAnimator);
        mStateListener.onAnimationEnd(mMockValueAnimator);
        verify(mMockWindowManager).setMagnificationSpec(eq(displayId), argThat(closeTo(endSpec)));
        verify(mAnimationCallback).onResult(true);
        verify(mAnimationCallback).onResult(eq(true), any());
    }

    @Test
@@ -451,7 +454,7 @@ public class FullScreenMagnificationControllerTest {
        mMessageCapturingHandler.sendAllMessages();

        verify(mMockValueAnimator, never()).start();
        verify(mAnimationCallback).onResult(true);
        verify(mAnimationCallback).onResult(eq(true), any());
    }

    @Test
@@ -736,7 +739,7 @@ public class FullScreenMagnificationControllerTest {

        verify(mRequestObserver, never()).onFullScreenMagnificationChanged(eq(displayId),
                any(Region.class), any(MagnificationConfig.class));
        verify(mAnimationCallback).onResult(true);
        verify(mAnimationCallback).onResult(eq(true), any());
    }

    @Test
@@ -772,7 +775,7 @@ public class FullScreenMagnificationControllerTest {
        mMessageCapturingHandler.sendAllMessages();

        // Verify expected actions.
        verify(mAnimationCallback).onResult(false);
        verify(mAnimationCallback).onResult(eq(false), any());
        verify(mMockValueAnimator).start();
        verify(mMockValueAnimator).cancel();

@@ -782,7 +785,7 @@ public class FullScreenMagnificationControllerTest {
        mStateListener.onAnimationEnd(mMockValueAnimator);

        checkActivatedAndMagnifying(/* activated= */ false, /* magnifying= */ false, displayId);
        verify(lastAnimationCallback).onResult(true);
        verify(lastAnimationCallback).onResult(eq(true), any());
    }

    @Test
Loading