Track difference between touch and word in x coordinate.

After a handle is snapped to the word, the difference was
tracked in offset. It can break grapheme clusters, ignores
character width, and makes it difficult to select text near
line breaks.

Bug: 21005599
Change-Id: I42402a377670c7e3c6d6e6583744d085ae52bba2
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index 814882a..30f373a 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -3855,8 +3855,8 @@
     private class SelectionStartHandleView extends HandleView {
         // Indicates whether the cursor is making adjustments within a word.
         private boolean mInWord = false;
-        // Offset to track difference between touch and word boundary.
-        protected int mTouchWordOffset;
+        // Difference between touch position and word boundary position.
+        private float mTouchWordDelta;
 
         public SelectionStartHandleView(Drawable drawableLtr, Drawable drawableRtl) {
             super(drawableLtr, drawableRtl);
@@ -3908,18 +3908,36 @@
                         offset = mPreviousOffset;
                     }
                 }
-                mTouchWordOffset = Math.max(trueOffset - offset, 0);
-                positionCursor = true;
-            } else if (offset - mTouchWordOffset > mPreviousOffset || currLine > mPrevLine) {
-                // User is shrinking the selection.
-                if (currLine > mPrevLine) {
-                    // We're on a different line, so we'll snap to word boundaries.
-                    offset = start;
-                    mTouchWordOffset = Math.max(trueOffset - offset, 0);
+                final Layout layout = mTextView.getLayout();
+                if (layout != null && offset < trueOffset) {
+                    final float adjustedX = layout.getPrimaryHorizontal(offset);
+                    mTouchWordDelta =
+                            mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX;
                 } else {
-                    offset -= mTouchWordOffset;
+                    mTouchWordDelta = 0.0f;
                 }
                 positionCursor = true;
+            } else {
+                final int adjustedOffset =
+                        mTextView.getOffsetAtCoordinate(currLine, x - mTouchWordDelta);
+                if (adjustedOffset > mPreviousOffset || currLine > mPrevLine) {
+                    // User is shrinking the selection.
+                    if (currLine > mPrevLine) {
+                        // We're on a different line, so we'll snap to word boundaries.
+                        offset = start;
+                        final Layout layout = mTextView.getLayout();
+                        if (layout != null && offset < trueOffset) {
+                            final float adjustedX = layout.getPrimaryHorizontal(offset);
+                            mTouchWordDelta =
+                                    mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX;
+                        } else {
+                            mTouchWordDelta = 0.0f;
+                        }
+                    } else {
+                        offset = adjustedOffset;
+                    }
+                    positionCursor = true;
+                }
             }
 
             // Handles can not cross and selection is at least one character.
@@ -3934,7 +3952,7 @@
                     } else {
                         offset = alteredOffset;
                     }
-                    mTouchWordOffset = 0;
+                    mTouchWordDelta = 0.0f;
                 }
                 mInWord = !getWordIteratorWithText().isBoundary(offset);
                 positionAtCursorOffset(offset, false);
@@ -3946,7 +3964,7 @@
             boolean superResult = super.onTouchEvent(event);
             if (event.getActionMasked() == MotionEvent.ACTION_UP) {
                 // Reset the touch word offset when the user has lifted their finger.
-                mTouchWordOffset = 0;
+                mTouchWordDelta = 0.0f;
             }
             return superResult;
         }
@@ -3955,8 +3973,8 @@
     private class SelectionEndHandleView extends HandleView {
         // Indicates whether the cursor is making adjustments within a word.
         private boolean mInWord = false;
-        // Offset to track difference between touch and word boundary.
-        protected int mTouchWordOffset;
+        // Difference between touch position and word boundary position.
+        private float mTouchWordDelta;
 
         public SelectionEndHandleView(Drawable drawableLtr, Drawable drawableRtl) {
             super(drawableLtr, drawableRtl);
@@ -4008,18 +4026,36 @@
                         offset = mPreviousOffset;
                     }
                 }
-                mTouchWordOffset = Math.max(offset - trueOffset, 0);
-                positionCursor = true;
-            } else if (offset + mTouchWordOffset < mPreviousOffset || currLine < mPrevLine) {
-                // User is shrinking the selection.
-                if (currLine < mPrevLine) {
-                    // We're on a different line, so we'll snap to word boundaries.
-                    offset = end;
-                    mTouchWordOffset = Math.max(offset - trueOffset, 0);
+                final Layout layout = mTextView.getLayout();
+                if (layout != null && offset > trueOffset) {
+                    final float adjustedX = layout.getPrimaryHorizontal(offset);
+                    mTouchWordDelta =
+                            adjustedX - mTextView.convertToLocalHorizontalCoordinate(x);
                 } else {
-                    offset += mTouchWordOffset;
+                    mTouchWordDelta = 0.0f;
                 }
                 positionCursor = true;
+            } else {
+                final int adjustedOffset =
+                        mTextView.getOffsetAtCoordinate(currLine, x + mTouchWordDelta);
+                if (adjustedOffset < mPreviousOffset || currLine < mPrevLine) {
+                    // User is shrinking the selection.
+                    if (currLine < mPrevLine) {
+                        // We're on a different line, so we'll snap to word boundaries.
+                        offset = end;
+                        final Layout layout = mTextView.getLayout();
+                        if (layout != null && offset > trueOffset) {
+                            final float adjustedX = layout.getPrimaryHorizontal(offset);
+                            mTouchWordDelta =
+                                    adjustedX - mTextView.convertToLocalHorizontalCoordinate(x);
+                        } else {
+                            mTouchWordDelta = 0.0f;
+                        }
+                    } else {
+                        offset = adjustedOffset;
+                    }
+                    positionCursor = true;
+                }
             }
 
             if (positionCursor) {
@@ -4034,7 +4070,7 @@
                     } else {
                         offset = Math.min(alteredOffset, length);
                     }
-                    mTouchWordOffset = 0;
+                    mTouchWordDelta = 0.0f;
                 }
                 mInWord = !getWordIteratorWithText().isBoundary(offset);
                 positionAtCursorOffset(offset, false);
@@ -4046,7 +4082,7 @@
             boolean superResult = super.onTouchEvent(event);
             if (event.getActionMasked() == MotionEvent.ACTION_UP) {
                 // Reset the touch word offset when the user has lifted their finger.
-                mTouchWordOffset = 0;
+                mTouchWordDelta = 0.0f;
             }
             return superResult;
         }