Merge "Make selection end handle stick to selection at line end."

This commit is contained in:
Keisuke Kuroyanagi
2017-01-13 06:39:18 +00:00
committed by Android (Google) Code Review
6 changed files with 228 additions and 65 deletions

View File

@@ -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);

View File

@@ -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

View File

@@ -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);

View File

@@ -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<TextV
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 {
// Arabic text.
final String text = "\u062A\u062B\u062C\n" + "\u062D\u062E\u062F\n"

View File

@@ -331,15 +331,37 @@ public final class TextViewActions {
*/
public static ViewAction dragHandle(TextView textView, Handle handleType, int endIndex,
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(
new DragAction(
DragAction.Drag.TAP,
new CurrentHandleCoordinates(textView),
new HandleCoordinates(textView, handleType, endIndex, primary),
new HandleCoordinates(textView, handleType, endIndex, primary,
getNewLineStartPosOnLineBreak),
Press.FINGER,
Editor.HandleView.class));
}
/**
* A provider of the x, y coordinates of the handle dragging point.
*/
@@ -402,13 +424,16 @@ public final class TextViewActions {
private final Handle mHandleType;
private final int mIndex;
private final boolean mPrimary;
private final boolean mGetNewLineStartPosOnLineBreak;
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;
mHandleType = handleType;
mIndex = index;
mPrimary = primary;
mGetNewLineStartPosOnLineBreak = getNewLineStartPosOnLineBreak;
mActionDescription = "Could not locate " + handleType.toString()
+ " handle that points text index: " + index
+ " (" + (primary ? "primary" : "secondary" ) + ")";
@@ -445,9 +470,10 @@ public final class TextViewActions {
final float currentX = handleView.getHorizontal(layout, currentOffset);
final float currentY = layout.getLineTop(currentLine);
final float[] currentCoordinates =
TextCoordinates.convertToScreenCoordinates(mTextView, currentX, currentY);
convertToScreenCoordinates(mTextView, currentX, currentY);
final float[] targetCoordinates =
(new TextCoordinates(mIndex, mPrimary)).calculateCoordinates(mTextView);
(new TextCoordinates(mIndex, mPrimary, mGetNewLineStartPosOnLineBreak))
.calculateCoordinates(mTextView);
final Rect bounds = new Rect();
view.getBoundsOnScreen(bounds);
final Rect visibleDisplayBounds = new Rect();
@@ -485,23 +511,27 @@ public final class TextViewActions {
private final int mIndex;
private final boolean mPrimary;
private final boolean mGetNewLineStartPosOnLineBreak;
private final String mActionDescription;
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;
mPrimary = primary;
mGetNewLineStartPosOnLineBreak = getNewLineStartPosOnLineBreak;
mActionDescription = "Could not locate text at index: " + mIndex
+ " (" + (primary ? "primary" : "secondary" ) + ")";
+ " (" + (primary ? "primary" : "secondary" )
+ ", mGetNewLineStartPosOnLineBreak: " + mGetNewLineStartPosOnLineBreak + ")";
}
@Override
public float[] calculateCoordinates(View view) {
try {
return locateTextAtIndex((TextView) view, mIndex, mPrimary);
return locateTextAtIndex((TextView) view, mIndex, mPrimary,
mGetNewLineStartPosOnLineBreak);
} catch (ClassCastException e) {
throw new PerformException.Builder()
.withActionDescription(mActionDescription)
@@ -520,30 +550,38 @@ public final class TextViewActions {
/**
* @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()) {
throw new StringIndexOutOfBoundsException(index);
}
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,
(primary ? layout.getPrimaryHorizontal(index)
: layout.getSecondaryHorizontal(index)),
(primary ? layout.getPrimaryHorizontal(index, false,
getNewLineStartPosOnLineBreak)
: layout.getSecondaryHorizontal(index, false,
getNewLineStartPosOnLineBreak)),
layout.getLineTop(line));
}
}
/**
* Convert TextView's local coordinates to on screen coordinates.
* @param textView the TextView
* @param x local horizontal coordinate
* @param y local vertical coordinate
* @return
*/
public static float[] convertToScreenCoordinates(TextView textView, float x, float y) {
final int[] xy = new int[2];
textView.getLocationOnScreen(xy);
return new float[]{ x + textView.getTotalPaddingLeft() - textView.getScrollX() + xy[0],
y + textView.getTotalPaddingTop() - textView.getScrollY() + xy[1] };
}
/**
* Convert TextView's local coordinates to on screen coordinates.
* @param textView the TextView
* @param x local horizontal coordinate
* @param y local vertical coordinate
* @return
*/
public static float[] convertToScreenCoordinates(TextView textView, float x, float y) {
final int[] xy = new int[2];
textView.getLocationOnScreen(xy);
return new float[]{ x + textView.getTotalPaddingLeft() - textView.getScrollX() + xy[0],
y + textView.getTotalPaddingTop() - textView.getScrollY() + xy[1] };
}
}

View File

@@ -28,11 +28,14 @@ import android.support.test.espresso.NoMatchingViewException;
import android.support.test.espresso.ViewAssertion;
import android.view.View;
import android.widget.EditText;
import android.widget.Editor;
import android.widget.TextView;
import junit.framework.AssertionFailedError;
import org.hamcrest.Matcher;
import com.android.ex.editstyledtext.EditStyledText.EditModeActions.TextViewAction;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -136,6 +139,14 @@ public final class TextViewAssertions {
return new CursorPositionAssertion(CursorPositionAssertion.RIGHT);
}
/**
* Returns a {@link ViewAssertion} that asserts that the TextView selection handle is on the
* specified line.
*/
public static ViewAssertion handleIsOnLine(TextView tv, int line) {
return new SelectionHandlePositionAssertion(tv, line);
}
/**
* A {@link ViewAssertion} to check the selected text in a {@link TextView}.
*/
@@ -216,4 +227,31 @@ public final class TextViewAssertions {
closeTo(0f, 1f));
}
}
/**
* {@link ViewAssertion} to check that TextView selection handle is on a given line.
*/
static class SelectionHandlePositionAssertion implements ViewAssertion {
private TextView mTextView;
private int mLine;
private SelectionHandlePositionAssertion(TextView tv, int line) {
mTextView = tv;
mLine = line;
}
@Override
public void check(View view, NoMatchingViewException exception) {
if (!(view instanceof Editor.HandleView)) {
throw new AssertionFailedError("View should be an instance of Editor.HandleView");
}
final Editor.HandleView handleView = (Editor.HandleView) view;
final Rect bounds = new Rect();
handleView.getBoundsOnScreen(bounds);
final float bottom = mTextView.getLayout().getLineBottom(mLine);
final float[] pos =
TextViewActions.convertToScreenCoordinates(mTextView, 0, bottom);
assertThat("Cursor should be on the line " + mLine, Double.valueOf(bounds.top),
closeTo(pos[1], 1f));
}
}
}