Merge "[Magnifier - 14] Follow finger instead of cursor"
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index b5ac330..247c806 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -4627,7 +4627,7 @@
             return 0;
         }
 
-        protected final void showMagnifier() {
+        protected final void showMagnifier(@NonNull final MotionEvent event) {
             if (mMagnifier == null) {
                 return;
             }
@@ -4653,9 +4653,10 @@
 
             final Layout layout = mTextView.getLayout();
             final int lineNumber = layout.getLineForOffset(offset);
-            // Horizontally snap to character offset.
-            final float xPosInView = getHorizontal(mTextView.getLayout(), offset)
-                    + mTextView.getTotalPaddingLeft() - mTextView.getScrollX();
+            // Horizontally move the magnifier smoothly.
+            final int[] textViewLocationOnScreen = new int[2];
+            mTextView.getLocationOnScreen(textViewLocationOnScreen);
+            final float xPosInView = event.getRawX() - textViewLocationOnScreen[0];
             // Vertically snap to middle of current line.
             final float yPosInView = (mTextView.getLayout().getLineTop(lineNumber)
                     + mTextView.getLayout().getLineBottom(lineNumber)) / 2.0f
@@ -4850,11 +4851,11 @@
                 case MotionEvent.ACTION_DOWN:
                     mDownPositionX = ev.getRawX();
                     mDownPositionY = ev.getRawY();
-                    showMagnifier();
+                    showMagnifier(ev);
                     break;
 
                 case MotionEvent.ACTION_MOVE:
-                    showMagnifier();
+                    showMagnifier(ev);
                     break;
 
                 case MotionEvent.ACTION_UP:
@@ -5208,11 +5209,11 @@
                     // re-engages the handle.
                     mTouchWordDelta = 0.0f;
                     mPrevX = UNSET_X_VALUE;
-                    showMagnifier();
+                    showMagnifier(event);
                     break;
 
                 case MotionEvent.ACTION_MOVE:
-                    showMagnifier();
+                    showMagnifier(event);
                     break;
 
                 case MotionEvent.ACTION_UP:
diff --git a/core/java/android/widget/Magnifier.java b/core/java/android/widget/Magnifier.java
index 26dfcc2..310b170 100644
--- a/core/java/android/widget/Magnifier.java
+++ b/core/java/android/widget/Magnifier.java
@@ -32,6 +32,7 @@
 import android.view.Surface;
 import android.view.SurfaceView;
 import android.view.View;
+import android.view.ViewParent;
 
 import com.android.internal.util.Preconditions;
 
@@ -44,6 +45,8 @@
     private static final int NONEXISTENT_PREVIOUS_CONFIG_VALUE = -1;
     // The view to which this magnifier is attached.
     private final View mView;
+    // The coordinates of the view in the surface.
+    private final int[] mViewCoordinatesInSurface;
     // The window containing the magnifier.
     private final PopupWindow mWindow;
     // The center coordinates of the window containing the magnifier.
@@ -87,6 +90,8 @@
                 com.android.internal.R.dimen.magnifier_height);
         mZoomScale = context.getResources().getFloat(
                 com.android.internal.R.dimen.magnifier_zoom_scale);
+        // The view's surface coordinates will not be updated until the magnifier is first shown.
+        mViewCoordinatesInSurface = new int[2];
 
         mWindow = new PopupWindow(context);
         mWindow.setContentView(content);
@@ -120,9 +125,34 @@
         configureCoordinates(xPosInView, yPosInView);
 
         // Clamp startX value to avoid distorting the rendering of the magnifier content.
-        final int startX = Math.max(0, Math.min(
+        // For this, we compute:
+        // - zeroScrollXInSurface: this is the start x of mView, where this is not masked by a
+        //                         potential scrolling container. For example, if mView is a
+        //                         TextView contained in a HorizontalScrollView,
+        //                         mViewCoordinatesInSurface will reflect the surface position of
+        //                         the first text character, rather than the position of the first
+        //                         visible one. Therefore, we need to add back the amount of
+        //                         scrolling from the parent containers.
+        // - actualWidth: similarly, the width of a View will be larger than its actually visible
+        //                width when it is contained in a scrolling container. We need to use
+        //                the minimum width of a scrolling container which contains this view.
+        int zeroScrollXInSurface = mViewCoordinatesInSurface[0];
+        int actualWidth = mView.getWidth();
+        ViewParent viewParent = mView.getParent();
+        while (viewParent instanceof View) {
+            final View container = (View) viewParent;
+            if (container.canScrollHorizontally(-1 /* left scroll */)
+                    || container.canScrollHorizontally(1 /* right scroll */)) {
+                zeroScrollXInSurface += container.getScrollX();
+                actualWidth = Math.min(actualWidth, container.getWidth()
+                        - container.getPaddingLeft() - container.getPaddingRight());
+            }
+            viewParent = viewParent.getParent();
+        }
+
+        final int startX = Math.max(zeroScrollXInSurface, Math.min(
                 mCenterZoomCoords.x - mBitmap.getWidth() / 2,
-                mView.getWidth() - mBitmap.getWidth()));
+                zeroScrollXInSurface + actualWidth - mBitmap.getWidth()));
         final int startY = mCenterZoomCoords.y - mBitmap.getHeight() / 2;
 
         if (xPosInView != mPrevPosInView.x || yPosInView != mPrevPosInView.y) {
@@ -169,10 +199,9 @@
             posX = xPosInView;
             posY = yPosInView;
         } else {
-            final int[] coordinatesInSurface = new int[2];
-            mView.getLocationInSurface(coordinatesInSurface);
-            posX = xPosInView + coordinatesInSurface[0];
-            posY = yPosInView + coordinatesInSurface[1];
+            mView.getLocationInSurface(mViewCoordinatesInSurface);
+            posX = xPosInView + mViewCoordinatesInSurface[0];
+            posY = yPosInView + mViewCoordinatesInSurface[1];
         }
 
         mCenterZoomCoords.x = Math.round(posX);