Suppress horizontal scrolling with trailing blanks

The existing behavior of EditText is that trailing blanks can cause a
line to exceed the layout width, causing the cursor to extend past the
line, which in turn causes horizontal scrolling. This patch clamps the
cursor to the layout width in the non-scrolling case, which makes the
spaces effectively invisible when they're at the end of the line, but at
least suppresses the scrolling.

The clamping only works reliably in left-to-right alignments, so this
patch checks for than and only enables the clamping in those cases.

Fix for bug 7699295.

Change-Id: I22bc4e6c9ded3d7716edfcf10dd2b5c31a5da9de
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
index 123acca..a6e8c70 100644
--- a/core/java/android/text/Layout.java
+++ b/core/java/android/text/Layout.java
@@ -792,8 +792,17 @@
      * the paragraph's primary direction.
      */
     public float getPrimaryHorizontal(int offset) {
+        return getPrimaryHorizontal(offset, false /* not clamped */);
+    }
+
+    /**
+     * Get the primary horizontal position for the specified text offset, but
+     * optionally clamp it so that it doesn't exceed the width of the layout.
+     * @hide
+     */
+    public float getPrimaryHorizontal(int offset, boolean clamped) {
         boolean trailing = primaryIsTrailingPrevious(offset);
-        return getHorizontal(offset, trailing);
+        return getHorizontal(offset, trailing, clamped);
     }
 
     /**
@@ -802,17 +811,26 @@
      * the direction other than the paragraph's primary direction.
      */
     public float getSecondaryHorizontal(int offset) {
-        boolean trailing = primaryIsTrailingPrevious(offset);
-        return getHorizontal(offset, !trailing);
+        return getSecondaryHorizontal(offset, false /* not clamped */);
     }
 
-    private float getHorizontal(int offset, boolean trailing) {
+    /**
+     * Get the secondary horizontal position for the specified text offset, but
+     * optionally clamp it so that it doesn't exceed the width of the layout.
+     * @hide
+     */
+    public float getSecondaryHorizontal(int offset, boolean clamped) {
+        boolean trailing = primaryIsTrailingPrevious(offset);
+        return getHorizontal(offset, !trailing, clamped);
+    }
+
+    private float getHorizontal(int offset, boolean trailing, boolean clamped) {
         int line = getLineForOffset(offset);
 
-        return getHorizontal(offset, trailing, line);
+        return getHorizontal(offset, trailing, line, clamped);
     }
 
-    private float getHorizontal(int offset, boolean trailing, int line) {
+    private float getHorizontal(int offset, boolean trailing, int line, boolean clamped) {
         int start = getLineStart(line);
         int end = getLineEnd(line);
         int dir = getParagraphDirection(line);
@@ -834,6 +852,9 @@
         float wid = tl.measure(offset - start, trailing, null);
         TextLine.recycle(tl);
 
+        if (clamped && wid > mWidth) {
+            wid = mWidth;
+        }
         int left = getParagraphLeft(line);
         int right = getParagraphRight(line);
 
@@ -1257,6 +1278,23 @@
     }
 
     /**
+     * Determine whether we should clamp cursor position. Currently it's
+     * only robust for left-aligned displays.
+     * @hide
+     */
+    public boolean shouldClampCursor(int line) {
+        // Only clamp cursor position in left-aligned displays.
+        switch (getParagraphAlignment(line)) {
+            case ALIGN_LEFT:
+                return true;
+            case ALIGN_NORMAL:
+                return getParagraphDirection(line) > 0;
+            default:
+                return false;
+        }
+
+    }
+    /**
      * Fills in the specified Path with a representation of a cursor
      * at the specified offset.  This will often be a vertical line
      * but can be multiple discontinuous lines in text with multiple
@@ -1270,8 +1308,9 @@
         int top = getLineTop(line);
         int bottom = getLineTop(line+1);
 
-        float h1 = getPrimaryHorizontal(point) - 0.5f;
-        float h2 = isLevelBoundary(point) ? getSecondaryHorizontal(point) - 0.5f : h1;
+        boolean clamped = shouldClampCursor(line);
+        float h1 = getPrimaryHorizontal(point, clamped) - 0.5f;
+        float h2 = isLevelBoundary(point) ? getSecondaryHorizontal(point, clamped) - 0.5f : h1;
 
         int caps = TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SHIFT_ON) |
                    TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SELECTING);
@@ -1357,8 +1396,8 @@
                 int en = Math.min(end, there);
 
                 if (st != en) {
-                    float h1 = getHorizontal(st, false, line);
-                    float h2 = getHorizontal(en, true, line);
+                    float h1 = getHorizontal(st, false, line, false /* not clamped */);
+                    float h2 = getHorizontal(en, true, line, false /* not clamped */);
 
                     float left = Math.min(h1, h2);
                     float right = Math.max(h1, h2);