diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index 2fc12d30ffd89..53564f093b374 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -1015,17 +1015,24 @@ public abstract class Layout { * the paragraph's primary direction. */ public float getPrimaryHorizontal(int offset) { - return getPrimaryHorizontal(offset, false /* not clamped */); + return getPrimaryHorizontal(offset, false /* not clamped */, + true /* getNewLineStartPosOnLineBreak */); } /** * 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. + * + * @param offset the offset to get horizontal position + * @param clamped whether to clamp the position by using the width of this layout. + * @param getNewLineStartPosOnLineBreak whether to get the start position of new line when the + * offset is at automatic line break. * @hide */ - public float getPrimaryHorizontal(int offset, boolean clamped) { + public float getPrimaryHorizontal(int offset, boolean clamped, + boolean getNewLineStartPosOnLineBreak) { boolean trailing = primaryIsTrailingPrevious(offset); - return getHorizontal(offset, trailing, clamped); + return getHorizontal(offset, trailing, clamped, getNewLineStartPosOnLineBreak); } /** @@ -1034,26 +1041,37 @@ public abstract class Layout { * the direction other than the paragraph's primary direction. */ public float getSecondaryHorizontal(int offset) { - return getSecondaryHorizontal(offset, false /* not clamped */); + return getSecondaryHorizontal(offset, false /* not clamped */, + true /* getNewLineStartPosOnLineBreak */); } /** * 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. + * + * @param offset the offset to get horizontal position + * @param clamped whether to clamp the position by using the width of this layout. + * @param getNewLineStartPosOnLineBreak whether to get the start position of new line when the + * offset is at automatic line break. * @hide */ - public float getSecondaryHorizontal(int offset, boolean clamped) { + public float getSecondaryHorizontal(int offset, boolean clamped, + boolean getNewLineStartPosOnLineBreak) { boolean trailing = primaryIsTrailingPrevious(offset); - return getHorizontal(offset, !trailing, clamped); + return getHorizontal(offset, !trailing, clamped, getNewLineStartPosOnLineBreak); } - private float getHorizontal(int offset, boolean primary) { - return primary ? getPrimaryHorizontal(offset) : getSecondaryHorizontal(offset); + private float getHorizontal(int offset, boolean primary, + boolean getNewLineStartPosOnLineBreak) { + return primary ? getPrimaryHorizontal(offset, false /* not clamped */, + getNewLineStartPosOnLineBreak) + : getSecondaryHorizontal(offset, false /* not clamped */, + getNewLineStartPosOnLineBreak); } - private float getHorizontal(int offset, boolean trailing, boolean clamped) { - int line = getLineForOffset(offset); - + private float getHorizontal(int offset, boolean trailing, boolean clamped, + boolean getNewLineStartPosOnLineBreak) { + final int line = getLineForOffset(offset, getNewLineStartPosOnLineBreak); return getHorizontal(offset, trailing, line, clamped); } @@ -1267,6 +1285,10 @@ public abstract class Layout { * beyond the end of the text, you get the last line. */ public int getLineForOffset(int offset) { + return getLineForOffset(offset, true); + } + + private int getLineForOffset(int offset, boolean getNewLineOnLineBreak) { int high = getLineCount(), low = -1, guess; while (high - low > 1) { @@ -1278,10 +1300,15 @@ public abstract class Layout { low = guess; } - if (low < 0) + if (low < 0) { return 0; - else + } else { + if (!getNewLineOnLineBreak && low > 0 && getLineStart(low) == offset + && mText.charAt(offset - 1) != '\n') { + return low - 1; + } return low; + } } /** @@ -1315,14 +1342,14 @@ public abstract class Layout { false, null); final int max; - if (line == getLineCount() - 1) { - max = lineEndOffset; - } else { + if (line != getLineCount() - 1 && mText.charAt(lineEndOffset - 1) == '\n') { max = tl.getOffsetToLeftRightOf(lineEndOffset - lineStartOffset, !isRtlCharAt(lineEndOffset - 1)) + lineStartOffset; + } else { + max = lineEndOffset; } int best = lineStartOffset; - float bestdist = Math.abs(getHorizontal(best, primary) - horiz); + float bestdist = Math.abs(getHorizontal(best, primary, true) - horiz); for (int i = 0; i < dirs.mDirections.length; i += 2) { int here = lineStartOffset + dirs.mDirections[i]; @@ -1338,10 +1365,13 @@ public abstract class Layout { guess = (high + low) / 2; int adguess = getOffsetAtStartOf(guess); - if (getHorizontal(adguess, primary) * swap >= horiz * swap) + if (getHorizontal(adguess, primary, + adguess == lineStartOffset || adguess != lineEndOffset) * swap + >= horiz * swap) { high = guess; - else + } else { low = guess; + } } if (low < here + 1) @@ -1351,9 +1381,11 @@ public abstract class Layout { int aft = tl.getOffsetToLeftRightOf(low - lineStartOffset, isRtl) + lineStartOffset; low = tl.getOffsetToLeftRightOf(aft - lineStartOffset, !isRtl) + lineStartOffset; if (low >= here && low < there) { - float dist = Math.abs(getHorizontal(low, primary) - horiz); + float dist = Math.abs(getHorizontal(low, primary, + low == lineStartOffset || low != lineEndOffset) - horiz); if (aft < there) { - float other = Math.abs(getHorizontal(aft, primary) - horiz); + float other = Math.abs(getHorizontal(aft, primary, + aft == lineStartOffset || aft != lineEndOffset) - horiz); if (other < dist) { dist = other; @@ -1368,7 +1400,8 @@ public abstract class Layout { } } - float dist = Math.abs(getHorizontal(here, primary) - horiz); + float dist = Math.abs(getHorizontal(here, primary, + here == lineStartOffset || here != lineEndOffset) - horiz); if (dist < bestdist) { bestdist = dist; @@ -1376,10 +1409,10 @@ public abstract class Layout { } } - float dist = Math.abs(getHorizontal(max, primary) - horiz); + float dist = Math.abs(getHorizontal(max, primary, + max == lineStartOffset || max != lineEndOffset) - horiz); if (dist <= bestdist) { - bestdist = dist; best = max; } @@ -1573,8 +1606,9 @@ public abstract class Layout { int bottom = getLineTop(line+1); boolean clamped = shouldClampCursor(line); - float h1 = getPrimaryHorizontal(point, clamped) - 0.5f; - float h2 = isLevelBoundary(point) ? getSecondaryHorizontal(point, clamped) - 0.5f : h1; + float h1 = getPrimaryHorizontal(point, clamped, true) - 0.5f; + float h2 = isLevelBoundary(point) + ? getSecondaryHorizontal(point, clamped, true) - 0.5f : h1; int caps = TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SHIFT_ON) | TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SELECTING); @@ -1691,8 +1725,7 @@ public abstract class Layout { } int startline = getLineForOffset(start); - int endline = getLineForOffset(end); - + int endline = getLineForOffset(end, false); int top = getLineTop(startline); int bottom = getLineBottom(endline); diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index 95fafc4718164..2703d6e40eeee 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -1954,10 +1954,11 @@ public class Editor { } boolean clamped = layout.shouldClampCursor(line); - updateCursorPosition(0, top, middle, layout.getPrimaryHorizontal(offset, clamped)); + updateCursorPosition(0, top, middle, layout.getPrimaryHorizontal(offset, clamped, true)); if (mCursorCount == 2) { - updateCursorPosition(1, middle, bottom, layout.getSecondaryHorizontal(offset, clamped)); + updateCursorPosition(1, middle, bottom, + layout.getSecondaryHorizontal(offset, clamped, true)); } } @@ -4380,7 +4381,7 @@ public class Editor { updateSelection(offset); addPositionToTouchUpFilter(offset); } - final int line = layout.getLineForOffset(offset); + final int line = getLineForOffset(layout, offset); mPrevLine = line; mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX @@ -4407,6 +4408,15 @@ public class Editor { return (int) (getHorizontal(layout, offset) - 0.5f); } + /** + * @param layout Text layout. + * @param offset Character offset for the cursor. + * @return The line the cursor should be at. + */ + int getLineForOffset(Layout layout, int offset) { + return layout.getLineForOffset(offset); + } + @Override public void updatePosition(int parentPositionX, int parentPositionY, boolean parentPositionChanged, boolean parentScrolled) { @@ -4835,7 +4845,7 @@ public class Editor { || !isStartHandle() && initialOffset <= anotherHandleOffset) { // Handles have crossed, bound it to the first selected line and // adjust by word / char as normal. - currLine = layout.getLineForOffset(anotherHandleOffset); + currLine = getLineForOffset(layout, anotherHandleOffset, !isStartHandle()); initialOffset = getOffsetAtCoordinate(layout, currLine, x); } @@ -4907,14 +4917,18 @@ public class Editor { if (isExpanding) { // User is increasing the selection. int wordBoundary = isStartHandle() ? wordStart : wordEnd; - final boolean snapToWord = (!mInWord - || (isStartHandle() ? currLine < mPrevLine : currLine > mPrevLine)) - && atRtl == isAtRtlRun(layout, wordBoundary); + final boolean atLineBoundary = layout.getLineStart(currLine) == offset + || layout.getLineEnd(currLine) == offset; + final boolean atWordBoundary = getWordIteratorWithText().isBoundary(offset); + final boolean snapToWord = !(atLineBoundary && atWordBoundary) + && (!mInWord + || (isStartHandle() ? currLine < mPrevLine : currLine > mPrevLine)) + && atRtl == isAtRtlRun(layout, wordBoundary); if (snapToWord) { // Sometimes words can be broken across lines (Chinese, hyphenation). // We still snap to the word boundary but we only use the letters on the // current line to determine if the user is far enough into the word to snap. - if (layout.getLineForOffset(wordBoundary) != currLine) { + if (getLineForOffset(layout, wordBoundary) != currLine) { wordBoundary = isStartHandle() ? layout.getLineStart(currLine) : layout.getLineEnd(currLine); } @@ -5062,12 +5076,29 @@ public class Editor { } private float getHorizontal(@NonNull Layout layout, int offset, boolean startHandle) { - final int line = layout.getLineForOffset(offset); + final int line = getLineForOffset(layout, offset); final int offsetToCheck = startHandle ? offset : Math.max(offset - 1, 0); final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck); final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1; return (isRtlChar == isRtlParagraph) - ? layout.getPrimaryHorizontal(offset) : layout.getSecondaryHorizontal(offset); + ? layout.getPrimaryHorizontal(offset, false, startHandle) + : layout.getSecondaryHorizontal(offset, false, startHandle); + } + + @Override + public int getLineForOffset(@NonNull Layout layout, int offset) { + return getLineForOffset(layout, offset, isStartHandle()); + } + + private int getLineForOffset(@NonNull Layout layout, int offset, boolean startHandle) { + final int line = layout.getLineForOffset(offset); + if (!startHandle && line > 0 && layout.getLineStart(line) == offset + && mTextView.getText().charAt(offset - 1) != '\n') { + // If end handle is at a line break in a paragraph, the handle should be at the + // previous line. + return line - 1; + } + return line; } @Override diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 4b48501cb5030..54eb83206f4d1 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -7972,7 +7972,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // 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 x = (int) layout.getPrimaryHorizontal(offset, clamped, true); final int top = layout.getLineTop(line); final int bottom = layout.getLineTop(line + 1); diff --git a/core/tests/coretests/src/android/widget/TextViewActivityTest.java b/core/tests/coretests/src/android/widget/TextViewActivityTest.java index 71dd526458563..b276d16adae8f 100644 --- a/core/tests/coretests/src/android/widget/TextViewActivityTest.java +++ b/core/tests/coretests/src/android/widget/TextViewActivityTest.java @@ -26,6 +26,7 @@ import static android.widget.espresso.TextViewActions.dragHandle; import static android.widget.espresso.TextViewActions.Handle; import static android.widget.espresso.TextViewActions.longPressAndDragOnText; import static android.widget.espresso.TextViewActions.longPressOnTextAtIndex; +import static android.widget.espresso.TextViewAssertions.handleIsOnLine; import static android.widget.espresso.TextViewAssertions.hasInsertionPointerAtIndex; import static android.widget.espresso.TextViewAssertions.hasSelection; import static android.widget.espresso.FloatingToolbarEspressoUtils.assertFloatingToolbarIsDisplayed; @@ -464,6 +465,28 @@ public class TextViewActivityTest extends ActivityInstrumentationTestCase2 + *
+ * View constraints: + *