summaryrefslogtreecommitdiffstats
path: root/core/java
diff options
context:
space:
mode:
authorGilles Debunne <debunne@google.com>2010-07-09 20:13:45 -0700
committerGilles Debunne <debunne@google.com>2010-07-12 18:45:40 -0700
commit05336274dd8e7ababfe5b253069653abbba20c3c (patch)
treea5229e6d1ab6151fb5d104b7aed58ab1b0c2bd6c /core/java
parent36febe1e041ca8d18712f2e75a255e4df6f9f545 (diff)
downloadframeworks_base-05336274dd8e7ababfe5b253069653abbba20c3c.zip
frameworks_base-05336274dd8e7ababfe5b253069653abbba20c3c.tar.gz
frameworks_base-05336274dd8e7ababfe5b253069653abbba20c3c.tar.bz2
Selection handlers in TextView
Long press on text view triggers selection mode. Two handles can be used to adjust selection. Change-Id: I45bb5fd62cae910570cff34920e45c4383160179
Diffstat (limited to 'core/java')
-rw-r--r--core/java/android/text/method/ArrowKeyMovementMethod.java44
-rw-r--r--core/java/android/widget/TextView.java605
2 files changed, 488 insertions, 161 deletions
diff --git a/core/java/android/text/method/ArrowKeyMovementMethod.java b/core/java/android/text/method/ArrowKeyMovementMethod.java
index 9df63a9..55ec655 100644
--- a/core/java/android/text/method/ArrowKeyMovementMethod.java
+++ b/core/java/android/text/method/ArrowKeyMovementMethod.java
@@ -138,35 +138,6 @@ public class ArrowKeyMovementMethod implements MovementMethod {
}
}
- private int getOffset(int x, int y, TextView widget){
- // Converts the absolute X,Y coordinates to the character offset for the
- // character whose position is closest to the specified
- // horizontal position.
- x -= widget.getTotalPaddingLeft();
- y -= widget.getTotalPaddingTop();
-
- // Clamp the position to inside of the view.
- if (x < 0) {
- x = 0;
- } else if (x >= (widget.getWidth()-widget.getTotalPaddingRight())) {
- x = widget.getWidth()-widget.getTotalPaddingRight() - 1;
- }
- if (y < 0) {
- y = 0;
- } else if (y >= (widget.getHeight()-widget.getTotalPaddingBottom())) {
- y = widget.getHeight()-widget.getTotalPaddingBottom() - 1;
- }
-
- x += widget.getScrollX();
- y += widget.getScrollY();
-
- Layout layout = widget.getLayout();
- int line = layout.getLineForVertical(y);
-
- int offset = layout.getOffsetForHorizontal(line, x);
- return offset;
- }
-
public boolean onKeyDown(TextView widget, Spannable buffer, int keyCode, KeyEvent event) {
if (executeDown(widget, buffer, keyCode)) {
MetaKeyKeyListener.adjustMetaAfterKeypress(buffer);
@@ -263,7 +234,7 @@ public class ArrowKeyMovementMethod implements MovementMethod {
MetaKeyKeyListener.META_SELECTING) != 0);
int x = (int) event.getX();
int y = (int) event.getY();
- int offset = getOffset(x, y, widget);
+ int offset = widget.getOffset(x, y);
if (cap) {
buffer.setSpan(LAST_TAP_DOWN, offset, offset,
@@ -320,7 +291,7 @@ public class ArrowKeyMovementMethod implements MovementMethod {
// Get the current touch position
int x = (int) event.getX();
int y = (int) event.getY();
- int offset = getOffset(x, y, widget);
+ int offset = widget.getOffset(x, y);
final OnePointFiveTapState[] tap = buffer.getSpans(0, buffer.length(),
OnePointFiveTapState.class);
@@ -366,7 +337,7 @@ public class ArrowKeyMovementMethod implements MovementMethod {
int x = (int) event.getX();
int y = (int) event.getY();
- int off = getOffset(x, y, widget);
+ int off = widget.getOffset(x, y);
// XXX should do the same adjust for x as we do for the line.
@@ -442,11 +413,10 @@ public class ArrowKeyMovementMethod implements MovementMethod {
widget.cancelLongPress();
// Offset the current touch position (from controller to cursor)
- final int x = (int) event.getX() + mCursorController.getOffsetX();
- final int y = (int) event.getY() + mCursorController.getOffsetY();
- int offset = getOffset(x, y, widget);
- Selection.setSelection(buffer, offset);
- mCursorController.updatePosition();
+ final float x = event.getX() + mCursorController.getOffsetX();
+ final float y = event.getY() + mCursorController.getOffsetY();
+ int offset = widget.getOffset((int) x, (int) y);
+ mCursorController.updatePosition(offset);
return true;
case MotionEvent.ACTION_UP:
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index f591483..a5cb151 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -1133,6 +1133,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
setText(mText);
fixFocusableAndClickableSettings();
+ prepareCursorController();
}
private void fixFocusableAndClickableSettings() {
@@ -2375,8 +2376,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
int end = 0;
if (mText != null) {
- start = Selection.getSelectionStart(mText);
- end = Selection.getSelectionEnd(mText);
+ start = getSelectionStart();
+ end = getSelectionEnd();
if (start >= 0 || end >= 0) {
// Or save state if there is a selection
save = true;
@@ -2700,6 +2701,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
if (needEditableForNotification) {
sendAfterTextChanged((Editable) text);
}
+
+ // Depends on canSelectText, which depends on text
+ prepareCursorController();
}
/**
@@ -3617,7 +3621,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
private void invalidateCursor() {
- int where = Selection.getSelectionEnd(mText);
+ int where = getSelectionEnd();
invalidateCursor(where, where, where);
}
@@ -3693,7 +3697,18 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
boolean changed = false;
if (mMovement != null) {
- int curs = Selection.getSelectionEnd(mText);
+ /* This code also provides auto-scrolling when a cursor is moved using a
+ * CursorController (insertion point or selection limits).
+ * For selection, ensure start or end is visible depending on controller's state.
+ */
+ int curs = getSelectionEnd();
+ if (mSelectionModifierCursorController != null) {
+ SelectionModifierCursorController selectionController =
+ (SelectionModifierCursorController) mSelectionModifierCursorController;
+ if (selectionController.isSelectionStartDragged()) {
+ curs = getSelectionStart();
+ }
+ }
/*
* TODO: This should really only keep the end in view if
@@ -3986,8 +4001,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
// XXX This is not strictly true -- a program could set the
// selection manually if it really wanted to.
if (mMovement != null && (isFocused() || isPressed())) {
- selStart = Selection.getSelectionStart(mText);
- selEnd = Selection.getSelectionEnd(mText);
+ selStart = getSelectionStart();
+ selEnd = getSelectionEnd();
if (mCursorVisible && selStart >= 0 && isEnabled()) {
if (mHighlightPath == null)
@@ -4097,6 +4112,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
if (mInsertionPointCursorController != null) {
mInsertionPointCursorController.draw(canvas);
}
+ if (mSelectionModifierCursorController != null) {
+ mSelectionModifierCursorController.draw(canvas);
+ }
}
@Override
@@ -4511,8 +4529,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
outAttrs.hintText = mHint;
if (mText instanceof Editable) {
InputConnection ic = new EditableInputConnection(this);
- outAttrs.initialSelStart = Selection.getSelectionStart(mText);
- outAttrs.initialSelEnd = Selection.getSelectionEnd(mText);
+ outAttrs.initialSelStart = getSelectionStart();
+ outAttrs.initialSelEnd = getSelectionEnd();
outAttrs.initialCapsMode = ic.getCursorCapsMode(mInputType);
return ic;
}
@@ -4597,8 +4615,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
outText.flags |= ExtractedText.FLAG_SINGLE_LINE;
}
outText.startOffset = 0;
- outText.selectionStart = Selection.getSelectionStart(content);
- outText.selectionEnd = Selection.getSelectionEnd(content);
+ outText.selectionStart = getSelectionStart();
+ outText.selectionEnd = getSelectionEnd();
return true;
}
return false;
@@ -4777,7 +4795,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
void updateAfterEdit() {
invalidate();
- int curs = Selection.getSelectionStart(mText);
+ int curs = getSelectionStart();
if (curs >= 0 || (mGravity & Gravity.VERTICAL_GRAVITY_MASK) ==
Gravity.BOTTOM) {
@@ -4921,7 +4939,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
w, alignment, mSpacingMult, mSpacingAdd,
boring, mIncludePad);
}
- // Log.e("aaa", "Boring: " + mTransformed);
mSavedLayout = (BoringLayout) mLayout;
} else if (shouldEllipsize && boring.width <= w) {
@@ -5677,8 +5694,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
if (!(mText instanceof Spannable)) {
return false;
}
- int start = Selection.getSelectionStart(mText);
- int end = Selection.getSelectionEnd(mText);
+ int start = getSelectionStart();
+ int end = getSelectionEnd();
if (start != end) {
return false;
}
@@ -6522,6 +6539,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
// Don't leave us in the middle of a batch edit.
onEndBatchEdit();
+
+ if (mInsertionPointCursorController != null) {
+ mInsertionPointCursorController.hide();
+ }
+ if (mSelectionModifierCursorController != null) {
+ mSelectionModifierCursorController.hide();
+ }
}
startStopMarquee(focused);
@@ -6651,12 +6675,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
boolean handled = false;
- int oldSelStart = Selection.getSelectionStart(mText);
- int oldSelEnd = Selection.getSelectionEnd(mText);
+ int oldSelStart = getSelectionStart();
+ int oldSelEnd = getSelectionEnd();
if (mInsertionPointCursorController != null) {
mInsertionPointCursorController.onTouchEvent(event);
}
+ if (mSelectionModifierCursorController != null) {
+ mSelectionModifierCursorController.onTouchEvent(event);
+ }
if (mMovement != null) {
handled |= mMovement.onTouchEvent(this, (Spannable) mText, event);
@@ -6667,8 +6694,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
InputMethodManager imm = (InputMethodManager)
getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
- final int newSelStart = Selection.getSelectionStart(mText);
- final int newSelEnd = Selection.getSelectionEnd(mText);
+ final int newSelStart = getSelectionStart();
+ final int newSelEnd = getSelectionEnd();
CommitSelectionReceiver csr = null;
if (newSelStart != oldSelStart || newSelEnd != oldSelEnd) {
@@ -6689,9 +6716,37 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
private void prepareCursorController() {
+ boolean atLeastOneController = false;
+
// TODO Add an extra android:cursorController flag to disable the controller?
- mInsertionPointCursorController =
- mCursorVisible ? new InsertionPointCursorController() : null;
+ if (mCursorVisible) {
+ atLeastOneController = true;
+ if (mInsertionPointCursorController == null) {
+ mInsertionPointCursorController = new InsertionPointCursorController();
+ }
+ } else {
+ mInsertionPointCursorController = null;
+ }
+
+ if (canSelectText()) {
+ atLeastOneController = true;
+ if (mSelectionModifierCursorController == null) {
+ mSelectionModifierCursorController = new SelectionModifierCursorController();
+ }
+ } else {
+ mSelectionModifierCursorController = null;
+ }
+
+ if (atLeastOneController) {
+ if (sCursorControllerTempRect == null) {
+ sCursorControllerTempRect = new Rect();
+ }
+ Resources res = mContext.getResources();
+ mCursorControllerVerticalOffset = res.getDimensionPixelOffset(
+ com.android.internal.R.dimen.cursor_controller_vertical_offset);
+ } else {
+ sCursorControllerTempRect = null;
+ }
}
/**
@@ -6751,8 +6806,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
TextView tv = mView.get();
if (tv != null && tv.isFocused()) {
- int st = Selection.getSelectionStart(tv.mText);
- int en = Selection.getSelectionEnd(tv.mText);
+ int st = tv.getSelectionStart();
+ int en = tv.getSelectionEnd();
if (st == en && st >= 0 && en >= 0) {
if (tv.mLayout != null) {
@@ -6944,6 +6999,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
private boolean canSelectText() {
+ // prepareCursorController() relies on this method.
+ // If you change this condition, make sure prepareCursorController is called anywhere
+ // the value of this condition might be changed.
if (mText instanceof Spannable && mText.length() != 0 &&
mMovement != null && mMovement.canSelectArbitrarily()) {
return true;
@@ -6992,10 +7050,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
/**
- * Returns a word to add to the dictionary from the context menu,
- * or null if there is no cursor or no word at the cursor.
+ * Returns the offsets delimiting the 'word' located at position offset.
+ *
+ * @param offset An offset in the text.
+ * @return The offsets for the start and end of the word located at <code>offset</code>.
+ * The two ints offsets are packed in a long, with the starting offset shifted by 32 bits.
+ * Returns a negative value if no valid word was found.
*/
- private String getWordForDictionary() {
+ private long getWordLimitsAt(int offset) {
/*
* Quick return if the input type is one where adding words
* to the dictionary doesn't make any sense.
@@ -7004,7 +7066,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
if (klass == InputType.TYPE_CLASS_NUMBER ||
klass == InputType.TYPE_CLASS_PHONE ||
klass == InputType.TYPE_CLASS_DATETIME) {
- return null;
+ return -1;
}
int variation = mInputType & InputType.TYPE_MASK_VARIATION;
@@ -7013,13 +7075,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
variation == InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD ||
variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS ||
variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
- return null;
+ return -1;
}
- int end = getSelectionEnd();
+ int end = offset;
if (end < 0) {
- return null;
+ return -1;
}
int start = end;
@@ -7053,6 +7115,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
}
+ if (start == end) {
+ return -1;
+ }
+
+ if (end - start > 48) {
+ return -1;
+ }
+
boolean hasLetter = false;
for (int i = start; i < end; i++) {
if (Character.isLetter(mTransformed.charAt(i))) {
@@ -7060,19 +7130,28 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
break;
}
}
+
if (!hasLetter) {
- return null;
+ return -1;
}
- if (start == end) {
- return null;
- }
+ // Two ints packed in a long
+ return (((long) start) << 32) | end;
+ }
- if (end - start > 48) {
+ /**
+ * Returns a word to add to the dictionary from the context menu,
+ * or null if there is no cursor or no word at the cursor.
+ */
+ private String getWordForDictionary() {
+ long wordLimits = getWordLimitsAt(getSelectionEnd());
+ if (wordLimits < 0) {
return null;
+ } else {
+ int start = (int) (wordLimits >>> 32);
+ int end = (int) (wordLimits & 0x00000000FFFFFFFFL);
+ return TextUtils.substring(mTransformed, start, end);
}
-
- return TextUtils.substring(mTransformed, start, end);
}
@Override
@@ -7372,6 +7451,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
@Override
public boolean performLongClick() {
+ // TODO This behavior should be moved to View
+ // TODO handle legacy code that added items to context menu
+ if (canSelectText()) {
+ if (startSelectionMode()) {
+ mEatTouchRelease = true;
+ return true;
+ }
+ }
+
if (super.performLongClick()) {
mEatTouchRelease = true;
return true;
@@ -7380,6 +7468,83 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
return false;
}
+ private boolean startSelectionMode() {
+ if (mSelectionModifierCursorController != null) {
+ int offset = ((SelectionModifierCursorController) mSelectionModifierCursorController).
+ getTouchOffset();
+
+ int selectionStart, selectionEnd;
+
+ if (hasSelection()) {
+ selectionStart = getSelectionStart();
+ selectionEnd = getSelectionEnd();
+ if (selectionStart > selectionEnd) {
+ int tmp = selectionStart;
+ selectionStart = selectionEnd;
+ selectionEnd = tmp;
+ }
+ if ((offset >= selectionStart) && (offset <= selectionEnd)) {
+ // Long press in the current selection.
+ // Should initiate a drag. Return false, to rely on context menu for now.
+ return false;
+ }
+ }
+
+ long wordLimits = getWordLimitsAt(offset);
+ if (wordLimits >= 0) {
+ selectionStart = (int) (wordLimits >>> 32);
+ selectionEnd = (int) (wordLimits & 0x00000000FFFFFFFFL);
+ } else {
+ selectionStart = Math.max(offset - 5, 0);
+ selectionEnd = Math.min(offset + 5, mText.length());
+ }
+
+ Selection.setSelection((Spannable) mText, selectionStart, selectionEnd);
+
+ // Has to be done AFTER selection has been changed to correctly position controllers.
+ mSelectionModifierCursorController.show();
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Get the offset character closest to the specified absolute position.
+ *
+ * @param x The horizontal absolute position of a point on screen
+ * @param y The vertical absolute position of a point on screen
+ * @return the character offset for the character whose position is closest to the specified
+ * position.
+ *
+ * @hide
+ */
+ public int getOffset(int x, int y) {
+ x -= getTotalPaddingLeft();
+ y -= getTotalPaddingTop();
+
+ // Clamp the position to inside of the view.
+ if (x < 0) {
+ x = 0;
+ } else if (x >= (getWidth() - getTotalPaddingRight())) {
+ x = getWidth()-getTotalPaddingRight() - 1;
+ }
+ if (y < 0) {
+ y = 0;
+ } else if (y >= (getHeight() - getTotalPaddingBottom())) {
+ y = getHeight()-getTotalPaddingBottom() - 1;
+ }
+
+ x += getScrollX();
+ y += getScrollY();
+
+ Layout layout = getLayout();
+ final int line = layout.getLineForVertical(y);
+ final int offset = layout.getOffsetForHorizontal(line, x);
+ return offset;
+ }
+
/**
* A CursorController instance can be used to control a cursor in the text.
*
@@ -7387,6 +7552,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
* and send them to this object instead of the cursor.
*/
public interface CursorController {
+ /* Cursor fade-out animation duration, in milliseconds. */
+ static final int FADE_OUT_DURATION = 400;
+
/**
* Makes the cursor controller visible on screen. Will be drawn by {@link #draw(Canvas)}.
* See also {@link #hide()}.
@@ -7402,19 +7570,19 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
/**
* Update the controller's position.
*/
- public void updatePosition();
+ public void updatePosition(int offset);
/**
* The controller and the cursor's positions can be link by a fixed offset,
* computed when the controller is touched, and then maintained as it moves
* @return Horizontal offset between the controller and the cursor.
*/
- public int getOffsetX();
+ public float getOffsetX();
/**
* @return Vertical offset between the controller and the cursor.
*/
- public int getOffsetY();
+ public float getOffsetY();
/**
* This method is called by {@link #onTouchEvent(MotionEvent)} and gives the controller
@@ -7434,12 +7602,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
class InsertionPointCursorController implements CursorController {
private static final int DELAY_BEFORE_FADE_OUT = 2100;
- private static final int FADE_OUT_DURATION = 400;
// Whether or not the cursor control is currently visible
private boolean mIsVisible = false;
- // Current cursor control bounds, in content coordinates
- private final Rect mBounds = new Rect();
// Starting time of the fade timer
private long mFadeOutTimerStart;
// The cursor controller image
@@ -7447,7 +7612,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
// Used to detect a tap (vs drag) on the controller
private long mOnDownTimerStart;
// Offset between finger hot point on cursor controller and actual cursor
- private int mOffsetX, mOffsetY;
+ private float mOffsetX, mOffsetY;
InsertionPointCursorController() {
Resources res = mContext.getResources();
@@ -7455,10 +7620,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
public void show() {
- updatePosition();
+ updateDrawablePosition();
// Has to be done after updatePosition, so that previous position invalidate
// in only done if necessary.
mIsVisible = true;
+ if (mSelectionModifierCursorController != null) {
+ mSelectionModifierCursorController.hide();
+ }
}
public void hide() {
@@ -7467,7 +7635,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
// Start fading out, only if not already in progress
if (time - mFadeOutTimerStart < DELAY_BEFORE_FADE_OUT) {
mFadeOutTimerStart = time - DELAY_BEFORE_FADE_OUT;
- postInvalidate(mBounds.left, mBounds.top, mBounds.right, mBounds.bottom);
+ postInvalidate(mDrawable);
}
}
}
@@ -7476,15 +7644,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
if (mIsVisible) {
int time = (int) (System.currentTimeMillis() - mFadeOutTimerStart);
if (time <= DELAY_BEFORE_FADE_OUT) {
- postInvalidateDelayed(DELAY_BEFORE_FADE_OUT - time,
- mBounds.left, mBounds.top, mBounds.right, mBounds.bottom);
+ postInvalidateDelayed(DELAY_BEFORE_FADE_OUT - time, mDrawable);
} else {
time -= DELAY_BEFORE_FADE_OUT;
if (time <= FADE_OUT_DURATION) {
- int alpha = 255 * (FADE_OUT_DURATION - time) / FADE_OUT_DURATION;
+ final int alpha = 255 * (FADE_OUT_DURATION - time) / FADE_OUT_DURATION;
mDrawable.setAlpha(alpha);
- postInvalidateDelayed(30,
- mBounds.left, mBounds.top, mBounds.right, mBounds.bottom);
+ postInvalidateDelayed(30, mDrawable);
} else {
mDrawable.setAlpha(0);
mIsVisible = false;
@@ -7494,113 +7660,299 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
}
- public void updatePosition() {
+ public void updatePosition(int offset) {
+ Selection.setSelection((Spannable) mText, offset);
+ updateDrawablePosition();
+ }
+
+ private void updateDrawablePosition() {
if (mIsVisible) {
// Clear previous cursor controller before bounds are updated
- postInvalidate(mBounds.left, mBounds.top, mBounds.right, mBounds.bottom);
+ postInvalidate(mDrawable);
}
- final int offset = Selection.getSelectionStart(mText);
+ final int offset = getSelectionStart();
if (offset < 0) {
// Should never happen, safety check.
- Log.w(LOG_TAG, "Update cursor controller position called with no cursor", null);
+ Log.w(LOG_TAG, "Update cursor controller position called with no cursor");
mIsVisible = false;
return;
}
- final int cursorControllerDrawableWidth = mDrawable.getIntrinsicWidth();
- final int cursorControllerDrawableHeight = mDrawable.getIntrinsicHeight();
- final int line = mLayout.getLineForOffset(offset);
+ positionDrawableUnderCursor(offset, mDrawable);
+
+ mFadeOutTimerStart = System.currentTimeMillis();
+ mDrawable.setAlpha(255);
+ }
+
+ public void onTouchEvent(MotionEvent event) {
+ if (isFocused() && isTextEditable() && mIsVisible) {
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN : {
+ final float x = event.getX();
+ final float y = event.getY();
+
+ if (fingerIsOnDrawable(x, y, mDrawable)) {
+ show();
+
+ if (mMovement instanceof ArrowKeyMovementMethod) {
+ ((ArrowKeyMovementMethod)mMovement).setCursorController(this);
+ }
+
+ if (mParent != null) {
+ // Prevent possible scrollView parent from scrolling, so that
+ // we can use auto-scrolling.
+ mParent.requestDisallowInterceptTouchEvent(true);
+
+ final Rect bounds = mDrawable.getBounds();
+ mOffsetX = (bounds.left + bounds.right) / 2.0f - x;
+ mOffsetY = bounds.top - mCursorControllerVerticalOffset - y;
+
+ mOnDownTimerStart = System.currentTimeMillis();
+ }
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_UP : {
+ int time = (int) (System.currentTimeMillis() - mOnDownTimerStart);
+
+ if (time <= ViewConfiguration.getTapTimeout()) {
+ // A tap on the controller is not grabbed, move the cursor instead
+ int offset = getOffset((int) event.getX(), (int) event.getY());
+ Selection.setSelection((Spannable) mText, offset);
+
+ // Modified by cancelLongPress and prevents the cursor from changing
+ mScrolled = false;
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ public float getOffsetX() {
+ return mOffsetX;
+ }
+
+ public float getOffsetY() {
+ return mOffsetY;
+ }
+ }
+
+ class SelectionModifierCursorController implements CursorController {
+ // Whether or not the selection controls are currently visible
+ private boolean mIsVisible = false;
+ // Whether that start or the end of selection controller is dragged
+ private boolean mStartIsDragged = false;
+ // Starting time of the fade timer
+ private long mFadeOutTimerStart;
+ // The cursor controller images
+ private final Drawable mStartDrawable, mEndDrawable;
+ // Offset between finger hot point on active cursor controller and actual cursor
+ private float mOffsetX, mOffsetY;
+ // The offset of that last touch down event. Remembered to start selection there.
+ private int mTouchOffset;
+
+ SelectionModifierCursorController() {
+ Resources res = mContext.getResources();
+ mStartDrawable = res.getDrawable(com.android.internal.R.drawable.selection_start_handle);
+ mEndDrawable = res.getDrawable(com.android.internal.R.drawable.selection_end_handle);
+ }
- mBounds.left = (int) (mLayout.getPrimaryHorizontal(offset) - 0.5 -
- cursorControllerDrawableWidth / 2.0);
- mBounds.top = mLayout.getLineTop(line + 1);
+ public void show() {
+ updateDrawablesPositions();
+ // Has to be done after updatePosition, so that previous position invalidate
+ // in only done if necessary.
+ mIsVisible = true;
+ mFadeOutTimerStart = -1;
+ if (mInsertionPointCursorController != null) {
+ mInsertionPointCursorController.hide();
+ }
+ }
- // Move cursor controller a little bit up when editing the last line of text
- // (or a single line) so that it is visible and easier to grab.
- if (line == mLayout.getLineCount() - 1) {
- mBounds.top -= Math.max(0,
- cursorControllerDrawableHeight / 2 - getExtendedPaddingBottom());
+ public void hide() {
+ if (mIsVisible && (mFadeOutTimerStart < 0)) {
+ mFadeOutTimerStart = System.currentTimeMillis();
+ postInvalidate(mStartDrawable);
+ postInvalidate(mEndDrawable);
}
+ }
- mBounds.right = mBounds.left + cursorControllerDrawableWidth;
- mBounds.bottom = mBounds.top + cursorControllerDrawableHeight;
+ public void draw(Canvas canvas) {
+ if (mIsVisible) {
+ if (mFadeOutTimerStart >= 0) {
+ int time = (int) (System.currentTimeMillis() - mFadeOutTimerStart);
+ if (time <= FADE_OUT_DURATION) {
+ final int alpha = 255 * (FADE_OUT_DURATION - time) / FADE_OUT_DURATION;
+ mStartDrawable.setAlpha(alpha);
+ mEndDrawable.setAlpha(alpha);
+ postInvalidateDelayed(30, mStartDrawable);
+ postInvalidateDelayed(30, mEndDrawable);
+ } else {
+ mStartDrawable.setAlpha(0);
+ mEndDrawable.setAlpha(0);
+ mIsVisible = false;
+ }
+ }
+ mStartDrawable.draw(canvas);
+ mEndDrawable.draw(canvas);
+ }
+ }
- convertFromViewportToContentCoordinates(mBounds);
- mDrawable.setBounds(mBounds);
+ public void updatePosition(int offset) {
+ int selectionStart = getSelectionStart();
+ int selectionEnd = getSelectionEnd();
- mFadeOutTimerStart = System.currentTimeMillis();
- mDrawable.setAlpha(255);
+ // Handle the case where start and end are swapped, making sure start <= end
+ if (mStartIsDragged) {
+ if (offset <= selectionEnd) {
+ selectionStart = offset;
+ } else {
+ selectionStart = selectionEnd;
+ selectionEnd = offset;
+ mStartIsDragged = false;
+ }
+ } else {
+ if (offset >= selectionStart) {
+ selectionEnd = offset;
+ } else {
+ selectionEnd = selectionStart;
+ selectionStart = offset;
+ mStartIsDragged = true;
+ }
+ }
- postInvalidate(mBounds.left, mBounds.top, mBounds.right, mBounds.bottom);
+ Selection.setSelection((Spannable) mText, selectionStart, selectionEnd);
+ updateDrawablesPositions();
}
- public void onTouchEvent(MotionEvent event) {
- if (isFocused() && isTextEditable()) {
- if (event.getActionMasked() == MotionEvent.ACTION_DOWN && mIsVisible) {
- final int x = (int) event.getX();
- final int y = (int) event.getY();
-
- // Simulate a 'fat finger' to ease grabbing of the controller.
- // Expand according to controller image size instead of using density.
- // Assume controller imager has a sensible size, proportionnal to density.
- final int cursorControllerDrawableWidth = mDrawable.getIntrinsicWidth();
- final int cursorControllerDrawableHeight = mDrawable.getIntrinsicHeight();
- final Rect fingerRect = new Rect(
- x - cursorControllerDrawableWidth / 2,
- y - cursorControllerDrawableHeight,
- x + cursorControllerDrawableWidth / 2,
- y);
-
- if (Rect.intersects(mBounds, fingerRect)) {
- show();
-
- if (mMovement instanceof ArrowKeyMovementMethod) {
- ((ArrowKeyMovementMethod)mMovement).setCursorController(this);
- }
+ private void updateDrawablesPositions() {
+ if (mIsVisible) {
+ // Clear previous cursor controller before bounds are updated
+ postInvalidate(mStartDrawable);
+ postInvalidate(mEndDrawable);
+ }
+
+ final int selectionStart = getSelectionStart();
+ final int selectionEnd = getSelectionEnd();
+
+ if ((selectionStart < 0) || (selectionEnd < 0)) {
+ // Should never happen, safety check.
+ Log.w(LOG_TAG, "Update selection controller position called with no cursor");
+ mIsVisible = false;
+ return;
+ }
+
+ positionDrawableUnderCursor(selectionStart, mStartDrawable);
+ positionDrawableUnderCursor(selectionEnd, mEndDrawable);
- if (mParent != null) {
- // Prevent possible scrollView parent from scrolling, so that
- // we can use auto-scrolling.
- mParent.requestDisallowInterceptTouchEvent(true);
+ mStartDrawable.setAlpha(255);
+ mEndDrawable.setAlpha(255);
+ }
- Resources res = mContext.getResources();
- final int verticalOffset = res.getDimensionPixelOffset(
- com.android.internal.R.dimen.cursor_controller_vertical_offset);
+ public void onTouchEvent(MotionEvent event) {
+ if (isFocused() && isTextEditable() &&
+ (event.getActionMasked() == MotionEvent.ACTION_DOWN)) {
+ final int x = (int) event.getX();
+ final int y = (int) event.getY();
+
+ // Remember finger down position, to be able to start selection on that point
+ mTouchOffset = getOffset(x, y);
+
+ if (mIsVisible) {
+ if (mMovement instanceof ArrowKeyMovementMethod) {
+ boolean isOnStart = fingerIsOnDrawable(x, y, mStartDrawable);
+ boolean isOnEnd = fingerIsOnDrawable(x, y, mEndDrawable);
+ if (isOnStart || isOnEnd) {
+ if (mParent != null) {
+ // Prevent possible scrollView parent from scrolling, so that
+ // we can use auto-scrolling.
+ mParent.requestDisallowInterceptTouchEvent(true);
+ }
- mOffsetX = (mBounds.left + mBounds.right) / 2 - x;
- mOffsetY = mBounds.top - verticalOffset - y;
+ // Start handle will be dragged in case BOTH controller are under finger
+ mStartIsDragged = isOnStart;
+ final Rect bounds =
+ (mStartIsDragged ? mStartDrawable : mEndDrawable).getBounds();
+ mOffsetX = (bounds.left + bounds.right) / 2.0f - x;
+ mOffsetY = bounds.top - mCursorControllerVerticalOffset - y;
- mOnDownTimerStart = System.currentTimeMillis();
+ ((ArrowKeyMovementMethod)mMovement).setCursorController(this);
}
}
- } else if (event.getActionMasked() == MotionEvent.ACTION_UP) {
- int time = (int) (System.currentTimeMillis() - mOnDownTimerStart);
-
- if (mIsVisible && (time <= ViewConfiguration.getTapTimeout())) {
- // A tap on the controller is not grabbed, move the cursor instead
- final int x = (int) event.getX();
- final int y = (int) event.getY();
-
- Layout layout = getLayout();
- int line = layout.getLineForVertical(y);
- int offset = layout.getOffsetForHorizontal(line, x);
- Selection.setSelection((Spannable) mText, offset);
- // Modified by cancelLongPress and prevents the cursor from changing
- mScrolled = false;
- }
}
}
}
- public int getOffsetX() {
+ public int getTouchOffset() {
+ return mTouchOffset;
+ }
+
+ public float getOffsetX() {
return mOffsetX;
}
- public int getOffsetY() {
+ public float getOffsetY() {
return mOffsetY;
}
+
+ /**
+ * @return true iff this controller is currently used to move the selection start.
+ */
+ public boolean isSelectionStartDragged() {
+ return mIsVisible && mStartIsDragged;
+ }
+ }
+
+ // Helper methods used by CursorController implementations
+
+ private void positionDrawableUnderCursor(final int offset, Drawable drawable) {
+ final int drawableWidth = drawable.getIntrinsicWidth();
+ final int drawableHeight = drawable.getIntrinsicHeight();
+ final int line = mLayout.getLineForOffset(offset);
+
+ final Rect bounds = sCursorControllerTempRect;
+ bounds.left = (int) (mLayout.getPrimaryHorizontal(offset) - 0.5 - drawableWidth / 2.0);
+ bounds.top = mLayout.getLineTop(line + 1);
+
+ // Move cursor controller a little bit up when editing the last line of text
+ // (or a single line) so that it is visible and easier to grab.
+ if (line == mLayout.getLineCount() - 1) {
+ bounds.top -= Math.max(0, drawableHeight / 2 - getExtendedPaddingBottom());
+ }
+
+ bounds.right = bounds.left + drawableWidth;
+ bounds.bottom = bounds.top + drawableHeight;
+
+ convertFromViewportToContentCoordinates(bounds);
+ drawable.setBounds(bounds);
+ postInvalidate(bounds.left, bounds.top, bounds.right, bounds.bottom);
+ }
+
+ private boolean fingerIsOnDrawable(float x, float y, Drawable drawable) {
+ // Simulate a 'fat finger' to ease grabbing of the controller.
+ // Expands according to controller image size instead of using density.
+ // Assumes controller imager has a sensible size, proportionnal to density.
+ final int drawableWidth = drawable.getIntrinsicWidth();
+ final int drawableHeight = drawable.getIntrinsicHeight();
+ final Rect fingerRect = sCursorControllerTempRect;
+ fingerRect.set((int) (x - drawableWidth / 2.0),
+ (int) (y - drawableHeight),
+ (int) (x + drawableWidth / 2.0),
+ (int) y);
+ return Rect.intersects(drawable.getBounds(), fingerRect);
+ }
+
+ private void postInvalidate(Drawable drawable) {
+ final Rect bounds = drawable.getBounds();
+ postInvalidate(bounds.left, bounds.top, bounds.right, bounds.bottom);
+ }
+
+ private void postInvalidateDelayed(long delay, Drawable drawable) {
+ final Rect bounds = drawable.getBounds();
+ postInvalidateDelayed(delay, bounds.left, bounds.top, bounds.right, bounds.bottom);
}
@ViewDebug.ExportedProperty
@@ -7624,15 +7976,20 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
private final TextPaint mTextPaint;
private boolean mUserSetTextScaleX;
private final Paint mHighlightPaint;
- private int mHighlightColor = 0xFFBBDDFF;
+ private int mHighlightColor = 0xD077A14B;
private Layout mLayout;
private long mShowCursor;
private Blink mBlink;
private boolean mCursorVisible = true;
- // Cursor Controller. Null when disabled.
+ // Cursor Controllers. Null when disabled.
private CursorController mInsertionPointCursorController;
+ private CursorController mSelectionModifierCursorController;
+ // Stored once and for all.
+ private int mCursorControllerVerticalOffset;
+ // Created once and shared by different CursorController helper methods.
+ private static Rect sCursorControllerTempRect;
private boolean mSelectAllOnFocus = false;