Loading core/java/android/text/Layout.java +61 −28 Original line number Original line Diff line number Diff line Loading @@ -1015,17 +1015,24 @@ public abstract class Layout { * the paragraph's primary direction. * the paragraph's primary direction. */ */ public float getPrimaryHorizontal(int offset) { 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 * 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. * 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 * @hide */ */ public float getPrimaryHorizontal(int offset, boolean clamped) { public float getPrimaryHorizontal(int offset, boolean clamped, boolean getNewLineStartPosOnLineBreak) { boolean trailing = primaryIsTrailingPrevious(offset); boolean trailing = primaryIsTrailingPrevious(offset); return getHorizontal(offset, trailing, clamped); return getHorizontal(offset, trailing, clamped, getNewLineStartPosOnLineBreak); } } /** /** Loading @@ -1034,26 +1041,37 @@ public abstract class Layout { * the direction other than the paragraph's primary direction. * the direction other than the paragraph's primary direction. */ */ public float getSecondaryHorizontal(int offset) { 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 * 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. * 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 * @hide */ */ public float getSecondaryHorizontal(int offset, boolean clamped) { public float getSecondaryHorizontal(int offset, boolean clamped, boolean getNewLineStartPosOnLineBreak) { boolean trailing = primaryIsTrailingPrevious(offset); boolean trailing = primaryIsTrailingPrevious(offset); return getHorizontal(offset, !trailing, clamped); return getHorizontal(offset, !trailing, clamped, getNewLineStartPosOnLineBreak); } } private float getHorizontal(int offset, boolean primary) { private float getHorizontal(int offset, boolean primary, return primary ? getPrimaryHorizontal(offset) : getSecondaryHorizontal(offset); 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) { private float getHorizontal(int offset, boolean trailing, boolean clamped, int line = getLineForOffset(offset); boolean getNewLineStartPosOnLineBreak) { final int line = getLineForOffset(offset, getNewLineStartPosOnLineBreak); return getHorizontal(offset, trailing, line, clamped); return getHorizontal(offset, trailing, line, clamped); } } Loading Loading @@ -1267,6 +1285,10 @@ public abstract class Layout { * beyond the end of the text, you get the last line. * beyond the end of the text, you get the last line. */ */ public int getLineForOffset(int offset) { public int getLineForOffset(int offset) { return getLineForOffset(offset, true); } private int getLineForOffset(int offset, boolean getNewLineOnLineBreak) { int high = getLineCount(), low = -1, guess; int high = getLineCount(), low = -1, guess; while (high - low > 1) { while (high - low > 1) { Loading @@ -1278,11 +1300,16 @@ public abstract class Layout { low = guess; low = guess; } } if (low < 0) if (low < 0) { return 0; return 0; else } else { if (!getNewLineOnLineBreak && low > 0 && getLineStart(low) == offset && mText.charAt(offset - 1) != '\n') { return low - 1; } return low; return low; } } } /** /** * Get the character offset on the specified line whose position is * Get the character offset on the specified line whose position is Loading Loading @@ -1315,14 +1342,14 @@ public abstract class Layout { false, null); false, null); final int max; final int max; if (line == getLineCount() - 1) { if (line != getLineCount() - 1 && mText.charAt(lineEndOffset - 1) == '\n') { max = lineEndOffset; } else { max = tl.getOffsetToLeftRightOf(lineEndOffset - lineStartOffset, max = tl.getOffsetToLeftRightOf(lineEndOffset - lineStartOffset, !isRtlCharAt(lineEndOffset - 1)) + lineStartOffset; !isRtlCharAt(lineEndOffset - 1)) + lineStartOffset; } else { max = lineEndOffset; } } int best = lineStartOffset; 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) { for (int i = 0; i < dirs.mDirections.length; i += 2) { int here = lineStartOffset + dirs.mDirections[i]; int here = lineStartOffset + dirs.mDirections[i]; Loading @@ -1338,11 +1365,14 @@ public abstract class Layout { guess = (high + low) / 2; guess = (high + low) / 2; int adguess = getOffsetAtStartOf(guess); int adguess = getOffsetAtStartOf(guess); if (getHorizontal(adguess, primary) * swap >= horiz * swap) if (getHorizontal(adguess, primary, adguess == lineStartOffset || adguess != lineEndOffset) * swap >= horiz * swap) { high = guess; high = guess; else } else { low = guess; low = guess; } } } if (low < here + 1) if (low < here + 1) low = here + 1; low = here + 1; Loading @@ -1351,9 +1381,11 @@ public abstract class Layout { int aft = tl.getOffsetToLeftRightOf(low - lineStartOffset, isRtl) + lineStartOffset; int aft = tl.getOffsetToLeftRightOf(low - lineStartOffset, isRtl) + lineStartOffset; low = tl.getOffsetToLeftRightOf(aft - lineStartOffset, !isRtl) + lineStartOffset; low = tl.getOffsetToLeftRightOf(aft - lineStartOffset, !isRtl) + lineStartOffset; if (low >= here && low < there) { 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) { 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) { if (other < dist) { dist = other; dist = other; Loading @@ -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) { if (dist < bestdist) { bestdist = dist; bestdist = dist; Loading @@ -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) { if (dist <= bestdist) { bestdist = dist; best = max; best = max; } } Loading Loading @@ -1573,8 +1606,9 @@ public abstract class Layout { int bottom = getLineTop(line+1); int bottom = getLineTop(line+1); boolean clamped = shouldClampCursor(line); boolean clamped = shouldClampCursor(line); float h1 = getPrimaryHorizontal(point, clamped) - 0.5f; float h1 = getPrimaryHorizontal(point, clamped, true) - 0.5f; float h2 = isLevelBoundary(point) ? getSecondaryHorizontal(point, clamped) - 0.5f : h1; float h2 = isLevelBoundary(point) ? getSecondaryHorizontal(point, clamped, true) - 0.5f : h1; int caps = TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SHIFT_ON) | int caps = TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SHIFT_ON) | TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SELECTING); TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SELECTING); Loading Loading @@ -1691,8 +1725,7 @@ public abstract class Layout { } } int startline = getLineForOffset(start); int startline = getLineForOffset(start); int endline = getLineForOffset(end); int endline = getLineForOffset(end, false); int top = getLineTop(startline); int top = getLineTop(startline); int bottom = getLineBottom(endline); int bottom = getLineBottom(endline); Loading core/java/android/widget/Editor.java +41 −10 Original line number Original line Diff line number Diff line Loading @@ -1954,10 +1954,11 @@ public class Editor { } } boolean clamped = layout.shouldClampCursor(line); 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) { if (mCursorCount == 2) { updateCursorPosition(1, middle, bottom, layout.getSecondaryHorizontal(offset, clamped)); updateCursorPosition(1, middle, bottom, layout.getSecondaryHorizontal(offset, clamped, true)); } } } } Loading Loading @@ -4380,7 +4381,7 @@ public class Editor { updateSelection(offset); updateSelection(offset); addPositionToTouchUpFilter(offset); addPositionToTouchUpFilter(offset); } } final int line = layout.getLineForOffset(offset); final int line = getLineForOffset(layout, offset); mPrevLine = line; mPrevLine = line; mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX Loading @@ -4407,6 +4408,15 @@ public class Editor { return (int) (getHorizontal(layout, offset) - 0.5f); 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 @Override public void updatePosition(int parentPositionX, int parentPositionY, public void updatePosition(int parentPositionX, int parentPositionY, boolean parentPositionChanged, boolean parentScrolled) { boolean parentPositionChanged, boolean parentScrolled) { Loading Loading @@ -4835,7 +4845,7 @@ public class Editor { || !isStartHandle() && initialOffset <= anotherHandleOffset) { || !isStartHandle() && initialOffset <= anotherHandleOffset) { // Handles have crossed, bound it to the first selected line and // Handles have crossed, bound it to the first selected line and // adjust by word / char as normal. // adjust by word / char as normal. currLine = layout.getLineForOffset(anotherHandleOffset); currLine = getLineForOffset(layout, anotherHandleOffset, !isStartHandle()); initialOffset = getOffsetAtCoordinate(layout, currLine, x); initialOffset = getOffsetAtCoordinate(layout, currLine, x); } } Loading Loading @@ -4907,14 +4917,18 @@ public class Editor { if (isExpanding) { if (isExpanding) { // User is increasing the selection. // User is increasing the selection. int wordBoundary = isStartHandle() ? wordStart : wordEnd; int wordBoundary = isStartHandle() ? wordStart : wordEnd; final boolean snapToWord = (!mInWord 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)) || (isStartHandle() ? currLine < mPrevLine : currLine > mPrevLine)) && atRtl == isAtRtlRun(layout, wordBoundary); && atRtl == isAtRtlRun(layout, wordBoundary); if (snapToWord) { if (snapToWord) { // Sometimes words can be broken across lines (Chinese, hyphenation). // Sometimes words can be broken across lines (Chinese, hyphenation). // We still snap to the word boundary but we only use the letters on the // 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. // 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() wordBoundary = isStartHandle() ? layout.getLineStart(currLine) : layout.getLineEnd(currLine); ? layout.getLineStart(currLine) : layout.getLineEnd(currLine); } } Loading Loading @@ -5062,12 +5076,29 @@ public class Editor { } } private float getHorizontal(@NonNull Layout layout, int offset, boolean startHandle) { 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 int offsetToCheck = startHandle ? offset : Math.max(offset - 1, 0); final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck); final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck); final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1; final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1; return (isRtlChar == isRtlParagraph) 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 @Override Loading core/java/android/widget/TextView.java +1 −1 Original line number Original line Diff line number Diff line Loading @@ -7972,7 +7972,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // right where it is most likely to be annoying. // right where it is most likely to be annoying. final boolean clamped = grav > 0; final boolean clamped = grav > 0; // FIXME: Is it okay to truncate this, or should we round? // 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 top = layout.getLineTop(line); final int bottom = layout.getLineTop(line + 1); final int bottom = layout.getLineTop(line + 1); Loading core/tests/coretests/src/android/widget/TextViewActivityTest.java +23 −0 Original line number Original line Diff line number Diff line Loading @@ -26,6 +26,7 @@ import static android.widget.espresso.TextViewActions.dragHandle; import static android.widget.espresso.TextViewActions.Handle; import static android.widget.espresso.TextViewActions.Handle; import static android.widget.espresso.TextViewActions.longPressAndDragOnText; import static android.widget.espresso.TextViewActions.longPressAndDragOnText; import static android.widget.espresso.TextViewActions.longPressOnTextAtIndex; 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.hasInsertionPointerAtIndex; import static android.widget.espresso.TextViewAssertions.hasSelection; import static android.widget.espresso.TextViewAssertions.hasSelection; import static android.widget.espresso.FloatingToolbarEspressoUtils.assertFloatingToolbarIsDisplayed; import static android.widget.espresso.FloatingToolbarEspressoUtils.assertFloatingToolbarIsDisplayed; Loading Loading @@ -464,6 +465,28 @@ public class TextViewActivityTest extends ActivityInstrumentationTestCase2<TextV onView(withId(R.id.textview)).check(hasSelection("abcd\nefg\nhijk\nlmn\nopqr")); onView(withId(R.id.textview)).check(hasSelection("abcd\nefg\nhijk\nlmn\nopqr")); } } public void testSelectionHandles_multiLine_japanese() throws Exception { onView(withId(R.id.textview)).perform(click()); final TextView textView = (TextView) getActivity().findViewById(R.id.textview); final StringBuilder builder = new StringBuilder(); for (int i = 0; i < 100; ++i) { builder.append("\u3042\u3044\u3046\u3048\u304A"); onView(withId(R.id.textview)).perform(replaceText(builder.toString())); final int lineEnd = textView.getLayout().getLineEnd(0); if (lineEnd < builder.length()) { break; } } onView(withId(R.id.textview)).perform(doubleClickOnTextAtIndex(3)); final int lineEnd = textView.getLayout().getLineEnd(0); onHandleView(com.android.internal.R.id.selection_end_handle) .perform(dragHandle(textView, Handle.SELECTION_END, lineEnd, true, false)); onHandleView(com.android.internal.R.id.selection_end_handle) .check(handleIsOnLine(textView, 0)); } public void testSelectionHandles_multiLine_rtl() throws Exception { public void testSelectionHandles_multiLine_rtl() throws Exception { // Arabic text. // Arabic text. final String text = "\u062A\u062B\u062C\n" + "\u062D\u062E\u062F\n" final String text = "\u062A\u062B\u062C\n" + "\u062D\u062E\u062F\n" Loading core/tests/coretests/src/android/widget/espresso/TextViewActions.java +64 −26 Original line number Original line Diff line number Diff line Loading @@ -331,15 +331,37 @@ public final class TextViewActions { */ */ public static ViewAction dragHandle(TextView textView, Handle handleType, int endIndex, public static ViewAction dragHandle(TextView textView, Handle handleType, int endIndex, boolean primary) { boolean primary) { return dragHandle(textView, handleType, endIndex, primary, true); } /** * Returns an action that tap then drags on the handle from the current position to endIndex on * the TextView.<br> * <br> * View constraints: * <ul> * <li>must be a TextView's drag-handle displayed on screen * <ul> * * @param textView TextView the handle is on * @param handleType Type of the handle * @param endIndex The index of the TextView's text to end the drag at * @param primary whether to use primary direction to get coordinate form index when endIndex is * at a direction boundary. * @param getNewLineStartPosOnLineBreak whether to use new line start coordinate on a line break * within a paragraph. */ public static ViewAction dragHandle(TextView textView, Handle handleType, int endIndex, boolean primary, boolean getNewLineStartPosOnLineBreak) { return actionWithAssertions( return actionWithAssertions( new DragAction( new DragAction( DragAction.Drag.TAP, DragAction.Drag.TAP, new CurrentHandleCoordinates(textView), new CurrentHandleCoordinates(textView), new HandleCoordinates(textView, handleType, endIndex, primary), new HandleCoordinates(textView, handleType, endIndex, primary, getNewLineStartPosOnLineBreak), Press.FINGER, Press.FINGER, Editor.HandleView.class)); Editor.HandleView.class)); } } /** /** * A provider of the x, y coordinates of the handle dragging point. * A provider of the x, y coordinates of the handle dragging point. */ */ Loading Loading @@ -402,13 +424,16 @@ public final class TextViewActions { private final Handle mHandleType; private final Handle mHandleType; private final int mIndex; private final int mIndex; private final boolean mPrimary; private final boolean mPrimary; private final boolean mGetNewLineStartPosOnLineBreak; private final String mActionDescription; private final String mActionDescription; public HandleCoordinates(TextView textView, Handle handleType, int index, boolean primary) { public HandleCoordinates(TextView textView, Handle handleType, int index, boolean primary, boolean getNewLineStartPosOnLineBreak) { mTextView = textView; mTextView = textView; mHandleType = handleType; mHandleType = handleType; mIndex = index; mIndex = index; mPrimary = primary; mPrimary = primary; mGetNewLineStartPosOnLineBreak = getNewLineStartPosOnLineBreak; mActionDescription = "Could not locate " + handleType.toString() mActionDescription = "Could not locate " + handleType.toString() + " handle that points text index: " + index + " handle that points text index: " + index + " (" + (primary ? "primary" : "secondary" ) + ")"; + " (" + (primary ? "primary" : "secondary" ) + ")"; Loading Loading @@ -445,9 +470,10 @@ public final class TextViewActions { final float currentX = handleView.getHorizontal(layout, currentOffset); final float currentX = handleView.getHorizontal(layout, currentOffset); final float currentY = layout.getLineTop(currentLine); final float currentY = layout.getLineTop(currentLine); final float[] currentCoordinates = final float[] currentCoordinates = TextCoordinates.convertToScreenCoordinates(mTextView, currentX, currentY); convertToScreenCoordinates(mTextView, currentX, currentY); final float[] targetCoordinates = final float[] targetCoordinates = (new TextCoordinates(mIndex, mPrimary)).calculateCoordinates(mTextView); (new TextCoordinates(mIndex, mPrimary, mGetNewLineStartPosOnLineBreak)) .calculateCoordinates(mTextView); final Rect bounds = new Rect(); final Rect bounds = new Rect(); view.getBoundsOnScreen(bounds); view.getBoundsOnScreen(bounds); final Rect visibleDisplayBounds = new Rect(); final Rect visibleDisplayBounds = new Rect(); Loading Loading @@ -485,23 +511,27 @@ public final class TextViewActions { private final int mIndex; private final int mIndex; private final boolean mPrimary; private final boolean mPrimary; private final boolean mGetNewLineStartPosOnLineBreak; private final String mActionDescription; private final String mActionDescription; public TextCoordinates(int index) { public TextCoordinates(int index) { this(index, true); this(index, true, true); } } public TextCoordinates(int index, boolean primary) { public TextCoordinates(int index, boolean primary, boolean getNewLineStartPosOnLineBreak) { mIndex = index; mIndex = index; mPrimary = primary; mPrimary = primary; mGetNewLineStartPosOnLineBreak = getNewLineStartPosOnLineBreak; mActionDescription = "Could not locate text at index: " + mIndex mActionDescription = "Could not locate text at index: " + mIndex + " (" + (primary ? "primary" : "secondary" ) + ")"; + " (" + (primary ? "primary" : "secondary" ) + ", mGetNewLineStartPosOnLineBreak: " + mGetNewLineStartPosOnLineBreak + ")"; } } @Override @Override public float[] calculateCoordinates(View view) { public float[] calculateCoordinates(View view) { try { try { return locateTextAtIndex((TextView) view, mIndex, mPrimary); return locateTextAtIndex((TextView) view, mIndex, mPrimary, mGetNewLineStartPosOnLineBreak); } catch (ClassCastException e) { } catch (ClassCastException e) { throw new PerformException.Builder() throw new PerformException.Builder() .withActionDescription(mActionDescription) .withActionDescription(mActionDescription) Loading @@ -520,17 +550,26 @@ public final class TextViewActions { /** /** * @throws StringIndexOutOfBoundsException * @throws StringIndexOutOfBoundsException */ */ private float[] locateTextAtIndex(TextView textView, int index, boolean primary) { private float[] locateTextAtIndex(TextView textView, int index, boolean primary, boolean getNewLineStartPosOnLineBreak) { if (index < 0 || index > textView.getText().length()) { if (index < 0 || index > textView.getText().length()) { throw new StringIndexOutOfBoundsException(index); throw new StringIndexOutOfBoundsException(index); } } final Layout layout = textView.getLayout(); final Layout layout = textView.getLayout(); final int line = layout.getLineForOffset(index); int line = layout.getLineForOffset(index); if (!getNewLineStartPosOnLineBreak && line > 0 && layout.getLineStart(line) == index && textView.getText().charAt(index - 1) != '\n') { line = line - 1; } return convertToScreenCoordinates(textView, return convertToScreenCoordinates(textView, (primary ? layout.getPrimaryHorizontal(index) (primary ? layout.getPrimaryHorizontal(index, false, : layout.getSecondaryHorizontal(index)), getNewLineStartPosOnLineBreak) : layout.getSecondaryHorizontal(index, false, getNewLineStartPosOnLineBreak)), layout.getLineTop(line)); layout.getLineTop(line)); } } } /** /** * Convert TextView's local coordinates to on screen coordinates. * Convert TextView's local coordinates to on screen coordinates. Loading @@ -546,4 +585,3 @@ public final class TextViewActions { y + textView.getTotalPaddingTop() - textView.getScrollY() + xy[1] }; y + textView.getTotalPaddingTop() - textView.getScrollY() + xy[1] }; } } } } } Loading
core/java/android/text/Layout.java +61 −28 Original line number Original line Diff line number Diff line Loading @@ -1015,17 +1015,24 @@ public abstract class Layout { * the paragraph's primary direction. * the paragraph's primary direction. */ */ public float getPrimaryHorizontal(int offset) { 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 * 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. * 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 * @hide */ */ public float getPrimaryHorizontal(int offset, boolean clamped) { public float getPrimaryHorizontal(int offset, boolean clamped, boolean getNewLineStartPosOnLineBreak) { boolean trailing = primaryIsTrailingPrevious(offset); boolean trailing = primaryIsTrailingPrevious(offset); return getHorizontal(offset, trailing, clamped); return getHorizontal(offset, trailing, clamped, getNewLineStartPosOnLineBreak); } } /** /** Loading @@ -1034,26 +1041,37 @@ public abstract class Layout { * the direction other than the paragraph's primary direction. * the direction other than the paragraph's primary direction. */ */ public float getSecondaryHorizontal(int offset) { 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 * 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. * 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 * @hide */ */ public float getSecondaryHorizontal(int offset, boolean clamped) { public float getSecondaryHorizontal(int offset, boolean clamped, boolean getNewLineStartPosOnLineBreak) { boolean trailing = primaryIsTrailingPrevious(offset); boolean trailing = primaryIsTrailingPrevious(offset); return getHorizontal(offset, !trailing, clamped); return getHorizontal(offset, !trailing, clamped, getNewLineStartPosOnLineBreak); } } private float getHorizontal(int offset, boolean primary) { private float getHorizontal(int offset, boolean primary, return primary ? getPrimaryHorizontal(offset) : getSecondaryHorizontal(offset); 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) { private float getHorizontal(int offset, boolean trailing, boolean clamped, int line = getLineForOffset(offset); boolean getNewLineStartPosOnLineBreak) { final int line = getLineForOffset(offset, getNewLineStartPosOnLineBreak); return getHorizontal(offset, trailing, line, clamped); return getHorizontal(offset, trailing, line, clamped); } } Loading Loading @@ -1267,6 +1285,10 @@ public abstract class Layout { * beyond the end of the text, you get the last line. * beyond the end of the text, you get the last line. */ */ public int getLineForOffset(int offset) { public int getLineForOffset(int offset) { return getLineForOffset(offset, true); } private int getLineForOffset(int offset, boolean getNewLineOnLineBreak) { int high = getLineCount(), low = -1, guess; int high = getLineCount(), low = -1, guess; while (high - low > 1) { while (high - low > 1) { Loading @@ -1278,11 +1300,16 @@ public abstract class Layout { low = guess; low = guess; } } if (low < 0) if (low < 0) { return 0; return 0; else } else { if (!getNewLineOnLineBreak && low > 0 && getLineStart(low) == offset && mText.charAt(offset - 1) != '\n') { return low - 1; } return low; return low; } } } /** /** * Get the character offset on the specified line whose position is * Get the character offset on the specified line whose position is Loading Loading @@ -1315,14 +1342,14 @@ public abstract class Layout { false, null); false, null); final int max; final int max; if (line == getLineCount() - 1) { if (line != getLineCount() - 1 && mText.charAt(lineEndOffset - 1) == '\n') { max = lineEndOffset; } else { max = tl.getOffsetToLeftRightOf(lineEndOffset - lineStartOffset, max = tl.getOffsetToLeftRightOf(lineEndOffset - lineStartOffset, !isRtlCharAt(lineEndOffset - 1)) + lineStartOffset; !isRtlCharAt(lineEndOffset - 1)) + lineStartOffset; } else { max = lineEndOffset; } } int best = lineStartOffset; 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) { for (int i = 0; i < dirs.mDirections.length; i += 2) { int here = lineStartOffset + dirs.mDirections[i]; int here = lineStartOffset + dirs.mDirections[i]; Loading @@ -1338,11 +1365,14 @@ public abstract class Layout { guess = (high + low) / 2; guess = (high + low) / 2; int adguess = getOffsetAtStartOf(guess); int adguess = getOffsetAtStartOf(guess); if (getHorizontal(adguess, primary) * swap >= horiz * swap) if (getHorizontal(adguess, primary, adguess == lineStartOffset || adguess != lineEndOffset) * swap >= horiz * swap) { high = guess; high = guess; else } else { low = guess; low = guess; } } } if (low < here + 1) if (low < here + 1) low = here + 1; low = here + 1; Loading @@ -1351,9 +1381,11 @@ public abstract class Layout { int aft = tl.getOffsetToLeftRightOf(low - lineStartOffset, isRtl) + lineStartOffset; int aft = tl.getOffsetToLeftRightOf(low - lineStartOffset, isRtl) + lineStartOffset; low = tl.getOffsetToLeftRightOf(aft - lineStartOffset, !isRtl) + lineStartOffset; low = tl.getOffsetToLeftRightOf(aft - lineStartOffset, !isRtl) + lineStartOffset; if (low >= here && low < there) { 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) { 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) { if (other < dist) { dist = other; dist = other; Loading @@ -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) { if (dist < bestdist) { bestdist = dist; bestdist = dist; Loading @@ -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) { if (dist <= bestdist) { bestdist = dist; best = max; best = max; } } Loading Loading @@ -1573,8 +1606,9 @@ public abstract class Layout { int bottom = getLineTop(line+1); int bottom = getLineTop(line+1); boolean clamped = shouldClampCursor(line); boolean clamped = shouldClampCursor(line); float h1 = getPrimaryHorizontal(point, clamped) - 0.5f; float h1 = getPrimaryHorizontal(point, clamped, true) - 0.5f; float h2 = isLevelBoundary(point) ? getSecondaryHorizontal(point, clamped) - 0.5f : h1; float h2 = isLevelBoundary(point) ? getSecondaryHorizontal(point, clamped, true) - 0.5f : h1; int caps = TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SHIFT_ON) | int caps = TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SHIFT_ON) | TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SELECTING); TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SELECTING); Loading Loading @@ -1691,8 +1725,7 @@ public abstract class Layout { } } int startline = getLineForOffset(start); int startline = getLineForOffset(start); int endline = getLineForOffset(end); int endline = getLineForOffset(end, false); int top = getLineTop(startline); int top = getLineTop(startline); int bottom = getLineBottom(endline); int bottom = getLineBottom(endline); Loading
core/java/android/widget/Editor.java +41 −10 Original line number Original line Diff line number Diff line Loading @@ -1954,10 +1954,11 @@ public class Editor { } } boolean clamped = layout.shouldClampCursor(line); 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) { if (mCursorCount == 2) { updateCursorPosition(1, middle, bottom, layout.getSecondaryHorizontal(offset, clamped)); updateCursorPosition(1, middle, bottom, layout.getSecondaryHorizontal(offset, clamped, true)); } } } } Loading Loading @@ -4380,7 +4381,7 @@ public class Editor { updateSelection(offset); updateSelection(offset); addPositionToTouchUpFilter(offset); addPositionToTouchUpFilter(offset); } } final int line = layout.getLineForOffset(offset); final int line = getLineForOffset(layout, offset); mPrevLine = line; mPrevLine = line; mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX Loading @@ -4407,6 +4408,15 @@ public class Editor { return (int) (getHorizontal(layout, offset) - 0.5f); 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 @Override public void updatePosition(int parentPositionX, int parentPositionY, public void updatePosition(int parentPositionX, int parentPositionY, boolean parentPositionChanged, boolean parentScrolled) { boolean parentPositionChanged, boolean parentScrolled) { Loading Loading @@ -4835,7 +4845,7 @@ public class Editor { || !isStartHandle() && initialOffset <= anotherHandleOffset) { || !isStartHandle() && initialOffset <= anotherHandleOffset) { // Handles have crossed, bound it to the first selected line and // Handles have crossed, bound it to the first selected line and // adjust by word / char as normal. // adjust by word / char as normal. currLine = layout.getLineForOffset(anotherHandleOffset); currLine = getLineForOffset(layout, anotherHandleOffset, !isStartHandle()); initialOffset = getOffsetAtCoordinate(layout, currLine, x); initialOffset = getOffsetAtCoordinate(layout, currLine, x); } } Loading Loading @@ -4907,14 +4917,18 @@ public class Editor { if (isExpanding) { if (isExpanding) { // User is increasing the selection. // User is increasing the selection. int wordBoundary = isStartHandle() ? wordStart : wordEnd; int wordBoundary = isStartHandle() ? wordStart : wordEnd; final boolean snapToWord = (!mInWord 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)) || (isStartHandle() ? currLine < mPrevLine : currLine > mPrevLine)) && atRtl == isAtRtlRun(layout, wordBoundary); && atRtl == isAtRtlRun(layout, wordBoundary); if (snapToWord) { if (snapToWord) { // Sometimes words can be broken across lines (Chinese, hyphenation). // Sometimes words can be broken across lines (Chinese, hyphenation). // We still snap to the word boundary but we only use the letters on the // 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. // 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() wordBoundary = isStartHandle() ? layout.getLineStart(currLine) : layout.getLineEnd(currLine); ? layout.getLineStart(currLine) : layout.getLineEnd(currLine); } } Loading Loading @@ -5062,12 +5076,29 @@ public class Editor { } } private float getHorizontal(@NonNull Layout layout, int offset, boolean startHandle) { 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 int offsetToCheck = startHandle ? offset : Math.max(offset - 1, 0); final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck); final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck); final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1; final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1; return (isRtlChar == isRtlParagraph) 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 @Override Loading
core/java/android/widget/TextView.java +1 −1 Original line number Original line Diff line number Diff line Loading @@ -7972,7 +7972,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // right where it is most likely to be annoying. // right where it is most likely to be annoying. final boolean clamped = grav > 0; final boolean clamped = grav > 0; // FIXME: Is it okay to truncate this, or should we round? // 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 top = layout.getLineTop(line); final int bottom = layout.getLineTop(line + 1); final int bottom = layout.getLineTop(line + 1); Loading
core/tests/coretests/src/android/widget/TextViewActivityTest.java +23 −0 Original line number Original line Diff line number Diff line Loading @@ -26,6 +26,7 @@ import static android.widget.espresso.TextViewActions.dragHandle; import static android.widget.espresso.TextViewActions.Handle; import static android.widget.espresso.TextViewActions.Handle; import static android.widget.espresso.TextViewActions.longPressAndDragOnText; import static android.widget.espresso.TextViewActions.longPressAndDragOnText; import static android.widget.espresso.TextViewActions.longPressOnTextAtIndex; 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.hasInsertionPointerAtIndex; import static android.widget.espresso.TextViewAssertions.hasSelection; import static android.widget.espresso.TextViewAssertions.hasSelection; import static android.widget.espresso.FloatingToolbarEspressoUtils.assertFloatingToolbarIsDisplayed; import static android.widget.espresso.FloatingToolbarEspressoUtils.assertFloatingToolbarIsDisplayed; Loading Loading @@ -464,6 +465,28 @@ public class TextViewActivityTest extends ActivityInstrumentationTestCase2<TextV onView(withId(R.id.textview)).check(hasSelection("abcd\nefg\nhijk\nlmn\nopqr")); onView(withId(R.id.textview)).check(hasSelection("abcd\nefg\nhijk\nlmn\nopqr")); } } public void testSelectionHandles_multiLine_japanese() throws Exception { onView(withId(R.id.textview)).perform(click()); final TextView textView = (TextView) getActivity().findViewById(R.id.textview); final StringBuilder builder = new StringBuilder(); for (int i = 0; i < 100; ++i) { builder.append("\u3042\u3044\u3046\u3048\u304A"); onView(withId(R.id.textview)).perform(replaceText(builder.toString())); final int lineEnd = textView.getLayout().getLineEnd(0); if (lineEnd < builder.length()) { break; } } onView(withId(R.id.textview)).perform(doubleClickOnTextAtIndex(3)); final int lineEnd = textView.getLayout().getLineEnd(0); onHandleView(com.android.internal.R.id.selection_end_handle) .perform(dragHandle(textView, Handle.SELECTION_END, lineEnd, true, false)); onHandleView(com.android.internal.R.id.selection_end_handle) .check(handleIsOnLine(textView, 0)); } public void testSelectionHandles_multiLine_rtl() throws Exception { public void testSelectionHandles_multiLine_rtl() throws Exception { // Arabic text. // Arabic text. final String text = "\u062A\u062B\u062C\n" + "\u062D\u062E\u062F\n" final String text = "\u062A\u062B\u062C\n" + "\u062D\u062E\u062F\n" Loading
core/tests/coretests/src/android/widget/espresso/TextViewActions.java +64 −26 Original line number Original line Diff line number Diff line Loading @@ -331,15 +331,37 @@ public final class TextViewActions { */ */ public static ViewAction dragHandle(TextView textView, Handle handleType, int endIndex, public static ViewAction dragHandle(TextView textView, Handle handleType, int endIndex, boolean primary) { boolean primary) { return dragHandle(textView, handleType, endIndex, primary, true); } /** * Returns an action that tap then drags on the handle from the current position to endIndex on * the TextView.<br> * <br> * View constraints: * <ul> * <li>must be a TextView's drag-handle displayed on screen * <ul> * * @param textView TextView the handle is on * @param handleType Type of the handle * @param endIndex The index of the TextView's text to end the drag at * @param primary whether to use primary direction to get coordinate form index when endIndex is * at a direction boundary. * @param getNewLineStartPosOnLineBreak whether to use new line start coordinate on a line break * within a paragraph. */ public static ViewAction dragHandle(TextView textView, Handle handleType, int endIndex, boolean primary, boolean getNewLineStartPosOnLineBreak) { return actionWithAssertions( return actionWithAssertions( new DragAction( new DragAction( DragAction.Drag.TAP, DragAction.Drag.TAP, new CurrentHandleCoordinates(textView), new CurrentHandleCoordinates(textView), new HandleCoordinates(textView, handleType, endIndex, primary), new HandleCoordinates(textView, handleType, endIndex, primary, getNewLineStartPosOnLineBreak), Press.FINGER, Press.FINGER, Editor.HandleView.class)); Editor.HandleView.class)); } } /** /** * A provider of the x, y coordinates of the handle dragging point. * A provider of the x, y coordinates of the handle dragging point. */ */ Loading Loading @@ -402,13 +424,16 @@ public final class TextViewActions { private final Handle mHandleType; private final Handle mHandleType; private final int mIndex; private final int mIndex; private final boolean mPrimary; private final boolean mPrimary; private final boolean mGetNewLineStartPosOnLineBreak; private final String mActionDescription; private final String mActionDescription; public HandleCoordinates(TextView textView, Handle handleType, int index, boolean primary) { public HandleCoordinates(TextView textView, Handle handleType, int index, boolean primary, boolean getNewLineStartPosOnLineBreak) { mTextView = textView; mTextView = textView; mHandleType = handleType; mHandleType = handleType; mIndex = index; mIndex = index; mPrimary = primary; mPrimary = primary; mGetNewLineStartPosOnLineBreak = getNewLineStartPosOnLineBreak; mActionDescription = "Could not locate " + handleType.toString() mActionDescription = "Could not locate " + handleType.toString() + " handle that points text index: " + index + " handle that points text index: " + index + " (" + (primary ? "primary" : "secondary" ) + ")"; + " (" + (primary ? "primary" : "secondary" ) + ")"; Loading Loading @@ -445,9 +470,10 @@ public final class TextViewActions { final float currentX = handleView.getHorizontal(layout, currentOffset); final float currentX = handleView.getHorizontal(layout, currentOffset); final float currentY = layout.getLineTop(currentLine); final float currentY = layout.getLineTop(currentLine); final float[] currentCoordinates = final float[] currentCoordinates = TextCoordinates.convertToScreenCoordinates(mTextView, currentX, currentY); convertToScreenCoordinates(mTextView, currentX, currentY); final float[] targetCoordinates = final float[] targetCoordinates = (new TextCoordinates(mIndex, mPrimary)).calculateCoordinates(mTextView); (new TextCoordinates(mIndex, mPrimary, mGetNewLineStartPosOnLineBreak)) .calculateCoordinates(mTextView); final Rect bounds = new Rect(); final Rect bounds = new Rect(); view.getBoundsOnScreen(bounds); view.getBoundsOnScreen(bounds); final Rect visibleDisplayBounds = new Rect(); final Rect visibleDisplayBounds = new Rect(); Loading Loading @@ -485,23 +511,27 @@ public final class TextViewActions { private final int mIndex; private final int mIndex; private final boolean mPrimary; private final boolean mPrimary; private final boolean mGetNewLineStartPosOnLineBreak; private final String mActionDescription; private final String mActionDescription; public TextCoordinates(int index) { public TextCoordinates(int index) { this(index, true); this(index, true, true); } } public TextCoordinates(int index, boolean primary) { public TextCoordinates(int index, boolean primary, boolean getNewLineStartPosOnLineBreak) { mIndex = index; mIndex = index; mPrimary = primary; mPrimary = primary; mGetNewLineStartPosOnLineBreak = getNewLineStartPosOnLineBreak; mActionDescription = "Could not locate text at index: " + mIndex mActionDescription = "Could not locate text at index: " + mIndex + " (" + (primary ? "primary" : "secondary" ) + ")"; + " (" + (primary ? "primary" : "secondary" ) + ", mGetNewLineStartPosOnLineBreak: " + mGetNewLineStartPosOnLineBreak + ")"; } } @Override @Override public float[] calculateCoordinates(View view) { public float[] calculateCoordinates(View view) { try { try { return locateTextAtIndex((TextView) view, mIndex, mPrimary); return locateTextAtIndex((TextView) view, mIndex, mPrimary, mGetNewLineStartPosOnLineBreak); } catch (ClassCastException e) { } catch (ClassCastException e) { throw new PerformException.Builder() throw new PerformException.Builder() .withActionDescription(mActionDescription) .withActionDescription(mActionDescription) Loading @@ -520,17 +550,26 @@ public final class TextViewActions { /** /** * @throws StringIndexOutOfBoundsException * @throws StringIndexOutOfBoundsException */ */ private float[] locateTextAtIndex(TextView textView, int index, boolean primary) { private float[] locateTextAtIndex(TextView textView, int index, boolean primary, boolean getNewLineStartPosOnLineBreak) { if (index < 0 || index > textView.getText().length()) { if (index < 0 || index > textView.getText().length()) { throw new StringIndexOutOfBoundsException(index); throw new StringIndexOutOfBoundsException(index); } } final Layout layout = textView.getLayout(); final Layout layout = textView.getLayout(); final int line = layout.getLineForOffset(index); int line = layout.getLineForOffset(index); if (!getNewLineStartPosOnLineBreak && line > 0 && layout.getLineStart(line) == index && textView.getText().charAt(index - 1) != '\n') { line = line - 1; } return convertToScreenCoordinates(textView, return convertToScreenCoordinates(textView, (primary ? layout.getPrimaryHorizontal(index) (primary ? layout.getPrimaryHorizontal(index, false, : layout.getSecondaryHorizontal(index)), getNewLineStartPosOnLineBreak) : layout.getSecondaryHorizontal(index, false, getNewLineStartPosOnLineBreak)), layout.getLineTop(line)); layout.getLineTop(line)); } } } /** /** * Convert TextView's local coordinates to on screen coordinates. * Convert TextView's local coordinates to on screen coordinates. Loading @@ -546,4 +585,3 @@ public final class TextViewActions { y + textView.getTotalPaddingTop() - textView.getScrollY() + xy[1] }; y + textView.getTotalPaddingTop() - textView.getScrollY() + xy[1] }; } } } } }