[Magnifier-33] Add animation on line jump

This CL adds a simple motion animation when the magnifier jumps between
lines.

Bug: 74381647
Test: manual testing
Test: atest FrameworksCoreTests:android.widget.TextViewActivityTest
Test: atest CtsWidgetTestCases:android.widget.cts.TextViewTest
Change-Id: I27caba47b18e694f93739866d6fd69569cb89184
(cherry picked from commit 8175edc7555febe2fd9792a13abb346577b02c95)
Merged-In: I27caba47b18e694f93739866d6fd69569cb89184
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index b1410f1..92285c7 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -17,6 +17,7 @@
 package android.widget;
 
 import android.R;
+import android.animation.ValueAnimator;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -100,6 +101,7 @@
 import android.view.ViewTreeObserver;
 import android.view.WindowManager;
 import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.animation.LinearInterpolator;
 import android.view.inputmethod.CorrectionInfo;
 import android.view.inputmethod.CursorAnchorInfo;
 import android.view.inputmethod.EditorInfo;
@@ -201,11 +203,11 @@
 
     private final boolean mHapticTextHandleEnabled;
 
-    private final Magnifier mMagnifier;
+    private final MagnifierMotionAnimator mMagnifierAnimator;
     private final Runnable mUpdateMagnifierRunnable = new Runnable() {
         @Override
         public void run() {
-            mMagnifier.update();
+            mMagnifierAnimator.update();
         }
     };
     // Update the magnifier contents whenever anything in the view hierarchy is updated.
@@ -216,7 +218,7 @@
             new ViewTreeObserver.OnDrawListener() {
         @Override
         public void onDraw() {
-            if (mMagnifier != null) {
+            if (mMagnifierAnimator != null) {
                 // Posting the method will ensure that updating the magnifier contents will
                 // happen right after the rendering of the current frame.
                 mTextView.post(mUpdateMagnifierRunnable);
@@ -372,7 +374,9 @@
         mHapticTextHandleEnabled = mTextView.getContext().getResources().getBoolean(
                 com.android.internal.R.bool.config_enableHapticTextHandle);
 
-        mMagnifier = FLAG_USE_MAGNIFIER ? new Magnifier(mTextView) : null;
+        if (FLAG_USE_MAGNIFIER) {
+            mMagnifierAnimator = new MagnifierMotionAnimator(new Magnifier(mTextView));
+        }
     }
 
     ParcelableParcel saveInstanceState() {
@@ -4310,6 +4314,88 @@
         }
     }
 
+    private static class MagnifierMotionAnimator {
+        private static final long DURATION = 100 /* miliseconds */;
+
+        // The magnifier being animated.
+        private final Magnifier mMagnifier;
+        // A value animator used to animate the magnifier.
+        private final ValueAnimator mAnimator;
+
+        // Whether the magnifier is currently visible.
+        private boolean mMagnifierIsShowing;
+        // The coordinates of the magnifier when the currently running animation started.
+        private float mAnimationStartX;
+        private float mAnimationStartY;
+        // The coordinates of the magnifier in the latest animation frame.
+        private float mAnimationCurrentX;
+        private float mAnimationCurrentY;
+        // The latest coordinates the motion animator was asked to #show() the magnifier at.
+        private float mLastX;
+        private float mLastY;
+
+        private MagnifierMotionAnimator(final Magnifier magnifier) {
+            mMagnifier = magnifier;
+            // Prepare the animator used to run the motion animation.
+            mAnimator = ValueAnimator.ofFloat(0, 1);
+            mAnimator.setDuration(DURATION);
+            mAnimator.setInterpolator(new LinearInterpolator());
+            mAnimator.addUpdateListener((animation) -> {
+                // Interpolate to find the current position of the magnifier.
+                mAnimationCurrentX = mAnimationStartX
+                        + (mLastX - mAnimationStartX) * animation.getAnimatedFraction();
+                mAnimationCurrentY = mAnimationStartY
+                        + (mLastY - mAnimationStartY) * animation.getAnimatedFraction();
+                mMagnifier.show(mAnimationCurrentX, mAnimationCurrentY);
+            });
+        }
+
+        /**
+         * Shows the magnifier at a new position.
+         * If the y coordinate is different from the previous y coordinate
+         * (probably corresponding to a line jump in the text), a short
+         * animation is added to the jump.
+         */
+        private void show(final float x, final float y) {
+            final boolean startNewAnimation = mMagnifierIsShowing && y != mLastY;
+
+            if (startNewAnimation) {
+                if (mAnimator.isRunning()) {
+                    mAnimator.cancel();
+                    mAnimationStartX = mAnimationCurrentX;
+                    mAnimationStartY = mAnimationCurrentY;
+                } else {
+                    mAnimationStartX = mLastX;
+                    mAnimationStartY = mLastY;
+                }
+                mAnimator.start();
+            } else {
+                if (!mAnimator.isRunning()) {
+                    mMagnifier.show(x, y);
+                }
+            }
+            mLastX = x;
+            mLastY = y;
+            mMagnifierIsShowing = true;
+        }
+
+        /**
+         * Updates the content of the magnifier.
+         */
+        private void update() {
+            mMagnifier.update();
+        }
+
+        /**
+         * Dismisses the magnifier, or does nothing if it is already dismissed.
+         */
+        private void dismiss() {
+            mMagnifier.dismiss();
+            mAnimator.cancel();
+            mMagnifierIsShowing = false;
+        }
+    }
+
     @VisibleForTesting
     public abstract class HandleView extends View implements TextViewPositionListener {
         protected Drawable mDrawable;
@@ -4711,7 +4797,8 @@
             } else {
                 rightBound += mTextView.getLayout().getLineRight(lineNumber);
             }
-            final float contentWidth = Math.round(mMagnifier.getWidth() / mMagnifier.getZoom());
+            final float contentWidth = Math.round(mMagnifierAnimator.mMagnifier.getWidth()
+                    / mMagnifierAnimator.mMagnifier.getZoom());
             if (touchXInView < leftBound - contentWidth / 2
                     || touchXInView > rightBound + contentWidth / 2) {
                 // The touch is too far from the current line / selection, so hide the magnifier.
@@ -4728,7 +4815,7 @@
         }
 
         protected final void updateMagnifier(@NonNull final MotionEvent event) {
-            if (mMagnifier == null) {
+            if (mMagnifierAnimator == null) {
                 return;
             }
 
@@ -4739,15 +4826,15 @@
                 mRenderCursorRegardlessTiming = true;
                 mTextView.invalidateCursorPath();
                 suspendBlink();
-                mMagnifier.show(showPosInView.x, showPosInView.y);
+                mMagnifierAnimator.show(showPosInView.x, showPosInView.y);
             } else {
                 dismissMagnifier();
             }
         }
 
         protected final void dismissMagnifier() {
-            if (mMagnifier != null) {
-                mMagnifier.dismiss();
+            if (mMagnifierAnimator != null) {
+                mMagnifierAnimator.dismiss();
                 mRenderCursorRegardlessTiming = false;
                 resumeBlink();
             }