Merge "Text selection changes - smart handles & drag accelerator"

This commit is contained in:
Mady Mellor
2015-03-05 15:15:25 +00:00
committed by Android (Google) Code Review
12 changed files with 369 additions and 33 deletions

View File

@@ -217,6 +217,12 @@ public class Editor {
WordIterator mWordIterator;
SpellChecker mSpellChecker;
// This word iterator is set with text and used to determine word boundaries
// when a user is selecting text.
private WordIterator mWordIteratorWithText;
// Indicate that the text in the word iterator needs to be updated.
private boolean mUpdateWordIteratorText;
private Rect mTempRect;
private TextView mTextView;
@@ -689,9 +695,52 @@ public class Editor {
return mTextView.getTransformationMethod() instanceof PasswordTransformationMethod;
}
private int getWordStart(int offset) {
// FIXME - For this and similar methods we're not doing anything to check if there's
// a LocaleSpan in the text, this may be something we should try handling or checking for.
int retOffset = getWordIteratorWithText().getBeginning(offset);
if (retOffset == BreakIterator.DONE) retOffset = offset;
return retOffset;
}
private int getWordEnd(int offset, boolean includePunctuation) {
int retOffset = getWordIteratorWithText().getEnd(offset);
if (retOffset == BreakIterator.DONE) {
retOffset = offset;
} else if (includePunctuation) {
retOffset = handlePunctuation(retOffset);
}
return retOffset;
}
private boolean isEndBoundary(int offset) {
int thisEnd = getWordEnd(offset, false);
return offset == thisEnd;
}
private boolean isStartBoundary(int offset) {
int thisStart = getWordStart(offset);
return thisStart == offset;
}
private int handlePunctuation(int offset) {
// FIXME - Check with UX how repeated ending punctuation should be handled.
// FIXME - Check with UX if / how we would handle non sentence ending characters.
// FIXME - Consider punctuation in different languages.
CharSequence text = mTextView.getText();
if (offset < text.length()) {
int c = Character.codePointAt(text, offset);
if (c == 0x002e /* period */|| c == 0x003f /* question mark */
|| c == 0x0021 /* exclamation mark */) {
offset = Character.offsetByCodePoints(text, offset, 1);
}
}
return offset;
}
/**
* Adjusts selection to the word under last touch offset.
* Return true if the operation was successfully performed.
* Adjusts selection to the word under last touch offset. Return true if the operation was
* successfully performed.
*/
private boolean selectCurrentWord() {
if (!canSelectText()) {
@@ -738,6 +787,8 @@ public class Editor {
selectionStart = ((Spanned) mTextView.getText()).getSpanStart(urlSpan);
selectionEnd = ((Spanned) mTextView.getText()).getSpanEnd(urlSpan);
} else {
// FIXME - We should check if there's a LocaleSpan in the text, this may be
// something we should try handling or checking for.
final WordIterator wordIterator = getWordIterator();
wordIterator.setCharSequence(mTextView.getText(), minOffset, maxOffset);
@@ -760,6 +811,7 @@ public class Editor {
void onLocaleChanged() {
// Will be re-created on demand in getWordIterator with the proper new locale
mWordIterator = null;
mWordIteratorWithText = null;
}
/**
@@ -772,6 +824,23 @@ public class Editor {
return mWordIterator;
}
private WordIterator getWordIteratorWithText() {
if (mWordIteratorWithText == null) {
mWordIteratorWithText = new WordIterator(mTextView.getTextServicesLocale());
mUpdateWordIteratorText = true;
}
if (mUpdateWordIteratorText) {
// FIXME - Shouldn't copy all of the text as only the area of the text relevant
// to the user's selection is needed. A possible solution would be to
// copy some number N of characters near the selection and then when the
// user approaches N then we'd do another copy of the next N characters.
CharSequence text = mTextView.getText();
mWordIteratorWithText.setCharSequence(text, 0, text.length());
mUpdateWordIteratorText = false;
}
return mWordIteratorWithText;
}
private long getCharRange(int offset) {
final int textLength = mTextView.getText().length();
if (offset + 1 < textLength) {
@@ -920,9 +989,8 @@ public class Editor {
mTextView.startDrag(data, getTextThumbnailBuilder(selectedText), localState, 0);
stopSelectionActionMode();
} else {
getSelectionController().hide();
selectCurrentWord();
getSelectionController().show();
stopSelectionActionMode();
startSelectionActionMode();
}
handled = true;
}
@@ -1058,6 +1126,9 @@ public class Editor {
void sendOnTextChanged(int start, int after) {
updateSpellCheckSpans(start, start + after, false);
// Flip flag to indicate the word iterator needs to have the text reset.
mUpdateWordIteratorText = true;
// Hide the controllers as soon as text is modified (typing, procedural...)
// We do not hide the span controllers, since they can be added when a new text is
// inserted into the text view (voice IME).
@@ -1613,6 +1684,9 @@ public class Editor {
}
}
if (selectionStarted) {
getSelectionController().enterDrag();
}
return selectionStarted;
}
@@ -2894,7 +2968,6 @@ public class Editor {
}
if (menu.hasVisibleItems() || mode.getCustomView() != null) {
getSelectionController().show();
mTextView.setHasTransientState(true);
return true;
} else {
@@ -3232,6 +3305,8 @@ public class Editor {
private Runnable mActionPopupShower;
// Minimum touch target size for handles
private int mMinSize;
// Indicates the line of text that the handle is on.
protected int mLine = -1;
public HandleView(Drawable drawableLtr, Drawable drawableRtl) {
super(mTextView.getContext());
@@ -3407,6 +3482,7 @@ public class Editor {
addPositionToTouchUpFilter(offset);
}
final int line = layout.getLineForOffset(offset);
mLine = line;
mPositionX = (int) (layout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX -
getHorizontalOffset() + getCursorOffset());
@@ -3456,6 +3532,30 @@ public class Editor {
}
}
public void showAtLocation(int offset) {
// TODO - investigate if there's a better way to show the handles
// after the drag accelerator has occured.
int[] tmpCords = new int[2];
mTextView.getLocationInWindow(tmpCords);
Layout layout = mTextView.getLayout();
int posX = tmpCords[0];
int posY = tmpCords[1];
final int line = layout.getLineForOffset(offset);
int startX = (int) (layout.getPrimaryHorizontal(offset) - 0.5f
- mHotspotX - getHorizontalOffset() + getCursorOffset());
int startY = layout.getLineBottom(line);
// Take TextView's padding and scroll into account.
startX += mTextView.viewportToContentHorizontalOffset();
startY += mTextView.viewportToContentVerticalOffset();
mContainer.showAtLocation(mTextView, Gravity.NO_GRAVITY,
startX + posX, startY + posY);
}
@Override
protected void onDraw(Canvas c) {
final int drawWidth = mDrawable.getIntrinsicWidth();
@@ -3694,6 +3794,12 @@ public class Editor {
}
private class SelectionStartHandleView extends HandleView {
// The previous offset this handle was at.
private int mPrevOffset;
// Indicates whether the cursor is making adjustments within a word.
private boolean mInWord = false;
// Offset to track difference between touch and word boundary.
protected int mTouchWordOffset;
public SelectionStartHandleView(Drawable drawableLtr, Drawable drawableRtl) {
super(drawableLtr, drawableRtl);
@@ -3701,11 +3807,7 @@ public class Editor {
@Override
protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
if (isRtlRun) {
return drawable.getIntrinsicWidth() / 4;
} else {
return (drawable.getIntrinsicWidth() * 3) / 4;
}
return isRtlRun ? 0 : drawable.getIntrinsicWidth();
}
@Override
@@ -3727,21 +3829,81 @@ public class Editor {
@Override
public void updatePosition(float x, float y) {
int offset = mTextView.getOffsetForPosition(x, y);
final int trueOffset = mTextView.getOffsetForPosition(x, y);
final int currLine = mTextView.getLineAtCoordinate(y);
int offset = trueOffset;
boolean positionCursor = false;
// Handles can not cross and selection is at least one character
final int selectionEnd = mTextView.getSelectionEnd();
if (offset >= selectionEnd) offset = Math.max(0, selectionEnd - 1);
int end = getWordEnd(offset, true);
int start = getWordStart(offset);
positionAtCursorOffset(offset, false);
if (offset < mPrevOffset) {
// User is increasing the selection.
if (!mInWord || currLine < mLine) {
// We're not in a word, or we're on a different line so we'll expand by
// word. First ensure the user has at least entered the next word.
int offsetToWord = Math.min((end - start) / 2, 2);
if (offset <= end - offsetToWord || currLine < mLine) {
offset = start;
} else {
offset = mPrevOffset;
}
}
mPrevOffset = offset;
mTouchWordOffset = trueOffset - offset;
mInWord = !isStartBoundary(offset);
positionCursor = true;
} else if (offset - mTouchWordOffset > mPrevOffset) {
// User is shrinking the selection.
if (currLine > mLine) {
// We're on a different line, so we'll snap to word boundaries.
offset = end;
}
offset -= mTouchWordOffset;
mPrevOffset = offset;
mInWord = !isEndBoundary(offset);
positionCursor = true;
}
// Handles can not cross and selection is at least one character.
if (positionCursor) {
final int selectionEnd = mTextView.getSelectionEnd();
if (offset >= selectionEnd) {
// We can't cross the handles so let's just constrain the Y value.
int alteredOffset = mTextView.getOffsetAtCoordinate(mLine, x);
if (alteredOffset >= selectionEnd) {
// Can't pass the other drag handle.
offset = Math.max(0, selectionEnd - 1);
} else {
offset = alteredOffset;
}
}
positionAtCursorOffset(offset, false);
}
}
public ActionPopupWindow getActionPopupWindow() {
return mActionPopupWindow;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean superResult = super.onTouchEvent(event);
if (event.getActionMasked() == MotionEvent.ACTION_UP) {
// Reset the touch word offset when the user has lifted their finger.
mTouchWordOffset = 0;
}
return superResult;
}
}
private class SelectionEndHandleView extends HandleView {
// The previous offset this handle was at.
private int mPrevOffset;
// Indicates whether the cursor is making adjustments within a word.
private boolean mInWord = false;
// Offset to track difference between touch and word boundary.
protected int mTouchWordOffset;
public SelectionEndHandleView(Drawable drawableLtr, Drawable drawableRtl) {
super(drawableLtr, drawableRtl);
@@ -3749,11 +3911,7 @@ public class Editor {
@Override
protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
if (isRtlRun) {
return (drawable.getIntrinsicWidth() * 3) / 4;
} else {
return drawable.getIntrinsicWidth() / 4;
}
return isRtlRun ? drawable.getIntrinsicWidth() : 0;
}
@Override
@@ -3775,20 +3933,72 @@ public class Editor {
@Override
public void updatePosition(float x, float y) {
int offset = mTextView.getOffsetForPosition(x, y);
final int trueOffset = mTextView.getOffsetForPosition(x, y);
final int currLine = mTextView.getLineAtCoordinate(y);
int offset = trueOffset;
boolean positionCursor = false;
// Handles can not cross and selection is at least one character
final int selectionStart = mTextView.getSelectionStart();
if (offset <= selectionStart) {
offset = Math.min(selectionStart + 1, mTextView.getText().length());
int end = getWordEnd(offset, true);
int start = getWordStart(offset);
if (offset > mPrevOffset) {
// User is increasing the selection.
if (!mInWord || currLine > mLine) {
// We're not in a word, or we're on a different line so we'll expand by
// word. First ensure the user has at least entered the next word.
int midPoint = Math.min((end - start) / 2, 2);
if (offset >= start + midPoint || currLine > mLine) {
offset = end;
} else {
offset = mPrevOffset;
}
}
mPrevOffset = offset;
mTouchWordOffset = offset - trueOffset;
mInWord = !isEndBoundary(offset);
positionCursor = true;
} else if (offset + mTouchWordOffset < mPrevOffset) {
// User is shrinking the selection.
if (currLine > mLine) {
// We're on a different line, so we'll snap to word boundaries.
offset = getWordStart(offset);
}
offset += mTouchWordOffset;
mPrevOffset = offset;
positionCursor = true;
mInWord = !isStartBoundary(offset);
}
positionAtCursorOffset(offset, false);
if (positionCursor) {
final int selectionStart = mTextView.getSelectionStart();
if (offset <= selectionStart) {
// We can't cross the handles so let's just constrain the Y value.
int alteredOffset = mTextView.getOffsetAtCoordinate(mLine, x);
int length = mTextView.getText().length();
if (alteredOffset <= selectionStart) {
// Can't pass the other drag handle.
offset = Math.min(selectionStart + 1, length);
} else {
offset = Math.min(alteredOffset, length);
}
}
positionAtCursorOffset(offset, false);
}
}
public void setActionPopupWindow(ActionPopupWindow actionPopupWindow) {
mActionPopupWindow = actionPopupWindow;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean superResult = super.onTouchEvent(event);
if (event.getActionMasked() == MotionEvent.ACTION_UP) {
// Reset the touch word offset when the user has lifted their finger.
mTouchWordOffset = 0;
}
return superResult;
}
}
/**
@@ -3871,6 +4081,11 @@ public class Editor {
private float mDownPositionX, mDownPositionY;
private boolean mGestureStayedInTapRegion;
// Where the user first starts the drag motion.
private int mStartOffset = -1;
// Indicates whether the user is selecting text and using the drag accelerator.
private boolean mDragAcceleratorActive;
SelectionModifierCursorController() {
resetTouchOffsets();
}
@@ -3920,6 +4135,22 @@ public class Editor {
if (mEndHandle != null) mEndHandle.hide();
}
public void enterDrag() {
// Just need to init the handles / hide insertion cursor.
show();
mDragAcceleratorActive = true;
// Start location of selection.
mStartOffset = mTextView.getOffsetForPosition(mLastDownPositionX,
mLastDownPositionY);
// Don't show the handles until user has lifted finger.
hide();
// This stops scrolling parents from intercepting the touch event, allowing
// the user to continue dragging across the screen to select text; TextView will
// scroll as necessary.
mTextView.getParent().requestDisallowInterceptTouchEvent(true);
}
public void onTouchEvent(MotionEvent event) {
// This is done even when the View does not have focus, so that long presses can start
// selection and tap can move cursor from this tap position.
@@ -3928,7 +4159,7 @@ public class Editor {
final float x = event.getX();
final float y = event.getY();
// Remember finger down position, to be able to start selection from there
// Remember finger down position, to be able to start selection from there.
mMinTouchOffset = mMaxTouchOffset = mTextView.getOffsetForPosition(x, y);
// Double tap detection
@@ -3967,23 +4198,112 @@ public class Editor {
break;
case MotionEvent.ACTION_MOVE:
final ViewConfiguration viewConfiguration = ViewConfiguration.get(
mTextView.getContext());
if (mGestureStayedInTapRegion) {
final float deltaX = event.getX() - mDownPositionX;
final float deltaY = event.getY() - mDownPositionY;
final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
final ViewConfiguration viewConfiguration = ViewConfiguration.get(
mTextView.getContext());
int doubleTapTouchSlop = viewConfiguration.getScaledDoubleTapTouchSlop();
if (distanceSquared > doubleTapTouchSlop * doubleTapTouchSlop) {
mGestureStayedInTapRegion = false;
}
}
if (mStartHandle != null && mStartHandle.isShowing()) {
// Don't do the drag if the handles are showing already.
break;
}
if (mStartOffset != -1) {
final int rawOffset = mTextView.getOffsetForPosition(event.getX(),
event.getY());
int offset = rawOffset;
// We don't start "dragging" until the user is past the initial word that
// gets selected on long press.
int firstWordStart = getWordStart(mStartOffset);
int firstWordEnd = getWordEnd(mStartOffset, false);
if (offset > firstWordEnd || offset < firstWordStart) {
// Basically the goal in the below code is to have the highlight be
// offset so that your finger isn't covering the end point.
int fingerOffset = viewConfiguration.getScaledTouchSlop();
float mx = event.getX();
float my = event.getY();
if (mx > fingerOffset) mx -= fingerOffset;
if (my > fingerOffset) my -= fingerOffset;
offset = mTextView.getOffsetForPosition(mx, my);
// Perform the check for closeness at edge of view, if we're very close
// don't adjust the offset to be in front of the finger - otherwise the
// user can't select words at the edge.
if (mTextView.getWidth() - fingerOffset > mx) {
// We're going by word, so we need to make sure that the offset
// that we get is within this, so we'll get the previous boundary.
final WordIterator wordIterator = getWordIteratorWithText();
final int precedingOffset = wordIterator.preceding(offset);
if (mStartOffset < offset) {
// Expanding with bottom handle, in this case the selection end
// is before the finger.
offset = Math.max(precedingOffset - 1, 0);
} else {
// Expand with the start handle, in this case the selection
// start is before the finger.
if (precedingOffset == WordIterator.DONE) {
offset = 0;
} else {
offset = wordIterator.preceding(precedingOffset);
}
}
}
if (offset == WordIterator.DONE)
offset = rawOffset;
// Need to adjust start offset based on direction of movement.
int newStart = mStartOffset < offset ? getWordStart(mStartOffset)
: getWordEnd(mStartOffset, true);
Selection.setSelection((Spannable) mTextView.getText(), newStart,
offset);
}
}
break;
case MotionEvent.ACTION_UP:
mPreviousTapUpTime = SystemClock.uptimeMillis();
if (mDragAcceleratorActive) {
// No longer dragging to select text, let the parent intercept events.
mTextView.getParent().requestDisallowInterceptTouchEvent(false);
show();
int startOffset = mTextView.getSelectionStart();
int endOffset = mTextView.getSelectionEnd();
// Since we don't let drag handles pass once they're visible, we need to
// make sure the start / end locations are correct because the user *can*
// switch directions during the initial drag.
if (endOffset < startOffset) {
int tmp = endOffset;
endOffset = startOffset;
startOffset = tmp;
// Also update the selection with the right offsets in this case.
Selection.setSelection((Spannable) mTextView.getText(),
startOffset, endOffset);
}
// Need to do this to display the handles.
mStartHandle.showAtLocation(startOffset);
mEndHandle.showAtLocation(endOffset);
// No longer the first dragging motion, reset.
mDragAcceleratorActive = false;
mStartOffset = -1;
}
break;
}
}
@@ -4010,6 +4330,8 @@ public class Editor {
public void resetTouchOffsets() {
mMinTouchOffset = mMaxTouchOffset = -1;
mStartOffset = -1;
mDragAcceleratorActive = false;
}
/**
@@ -4019,6 +4341,13 @@ public class Editor {
return mStartHandle != null && mStartHandle.isDragging();
}
/**
* @return true if the user is selecting text using the drag accelerator.
*/
public boolean isDragAcceleratorActive() {
return mDragAcceleratorActive;
}
public void onTouchModeChanged(boolean isInTouchMode) {
if (!isInTouchMode) {
hide();

View File

@@ -8098,7 +8098,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
public boolean onTouchEvent(MotionEvent event) {
final int action = event.getActionMasked();
if (mEditor != null) mEditor.onTouchEvent(event);
if (mEditor != null) {
mEditor.onTouchEvent(event);
if (mEditor.mSelectionModifierCursorController != null &&
mEditor.mSelectionModifierCursorController.isDragAcceleratorActive()) {
return true;
}
}
final boolean superResult = super.onTouchEvent(event);
@@ -9104,7 +9111,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
return getLayout().getLineForVertical((int) y);
}
private int getOffsetAtCoordinate(int line, float x) {
int getOffsetAtCoordinate(int line, float x) {
x = convertToLocalHorizontalCoordinate(x);
return getLayout().getOffsetForHorizontal(line, x);
}
@@ -9726,4 +9733,4 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
TextView.this.spanChange(buf, what, s, -1, e, -1);
}
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 478 B

After

Width:  |  Height:  |  Size: 368 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 486 B

After

Width:  |  Height:  |  Size: 349 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 346 B

After

Width:  |  Height:  |  Size: 267 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 347 B

After

Width:  |  Height:  |  Size: 241 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 751 B

After

Width:  |  Height:  |  Size: 467 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 752 B

After

Width:  |  Height:  |  Size: 466 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 684 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 630 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 889 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 858 B