From afe8e9b6d033cb854afa3024d8198a32896a804a Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Wed, 19 Dec 2012 16:09:32 -0800 Subject: [PATCH] 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 --- core/java/android/text/Layout.java | 59 +++++++++++++++++++++----- core/java/android/widget/Editor.java | 14 +++--- core/java/android/widget/TextView.java | 33 ++++++++++---- 3 files changed, 82 insertions(+), 24 deletions(-) diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index 123accae4f6e3..a6e8c7032eaf4 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -792,8 +792,17 @@ public abstract class Layout { * 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 @@ public abstract class Layout { * 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 @@ public abstract class Layout { 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); @@ -1256,6 +1277,23 @@ public abstract class Layout { return offset; } + /** + * 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 @@ -1270,8 +1308,9 @@ public abstract class Layout { 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 @@ public abstract class Layout { 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); diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index ef95af76c623c..41d60339b5c7d 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -1468,20 +1468,24 @@ public class Editor { middle = (top + bottom) >> 1; } - updateCursorPosition(0, top, middle, getPrimaryHorizontal(layout, hintLayout, offset)); + boolean clamped = layout.shouldClampCursor(line); + updateCursorPosition(0, top, middle, + getPrimaryHorizontal(layout, hintLayout, offset, clamped)); if (mCursorCount == 2) { - updateCursorPosition(1, middle, bottom, layout.getSecondaryHorizontal(offset)); + updateCursorPosition(1, middle, bottom, + layout.getSecondaryHorizontal(offset, clamped)); } } - private float getPrimaryHorizontal(Layout layout, Layout hintLayout, int offset) { + private float getPrimaryHorizontal(Layout layout, Layout hintLayout, int offset, + boolean clamped) { if (TextUtils.isEmpty(layout.getText()) && hintLayout != null && !TextUtils.isEmpty(hintLayout.getText())) { - return hintLayout.getPrimaryHorizontal(offset); + return hintLayout.getPrimaryHorizontal(offset, clamped); } else { - return layout.getPrimaryHorizontal(offset); + return layout.getPrimaryHorizontal(offset, clamped); } } diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 0641df6f5f27e..d635de6f1edcf 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -6553,15 +6553,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener int line = layout.getLineForOffset(offset); - // FIXME: Is it okay to truncate this, or should we round? - final int x = (int)layout.getPrimaryHorizontal(offset); - final int top = layout.getLineTop(line); - final int bottom = layout.getLineTop(line + 1); - - int left = (int) FloatMath.floor(layout.getLineLeft(line)); - int right = (int) FloatMath.ceil(layout.getLineRight(line)); - int ht = layout.getHeight(); - int grav; switch (layout.getParagraphAlignment(line)) { @@ -6583,8 +6574,32 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener break; } + // We only want to clamp the cursor to fit within the layout width + // in left-to-right modes, because in a right to left alignment, + // we want to scroll to keep the line-right on the screen, as other + // lines are likely to have text flush with the right margin, which + // we want to keep visible. + // A better long-term solution would probably be to measure both + // the full line and a blank-trimmed version, and, for example, use + // the latter measurement for centering and right alignment, but for + // the time being we only implement the cursor clamping in left to + // right where it is most likely to be annoying. + final boolean clamped = grav > 0; + // FIXME: Is it okay to truncate this, or should we round? + final int x = (int)layout.getPrimaryHorizontal(offset, clamped); + final int top = layout.getLineTop(line); + final int bottom = layout.getLineTop(line + 1); + + int left = (int) FloatMath.floor(layout.getLineLeft(line)); + int right = (int) FloatMath.ceil(layout.getLineRight(line)); + int ht = layout.getHeight(); + int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(); int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom(); + if (!mHorizontallyScrolling && right - left > hspace && right > x) { + // If cursor has been clamped, make sure we don't scroll. + right = Math.max(x, left + hspace); + } int hslack = (bottom - top) / 2; int vslack = hslack;