summaryrefslogtreecommitdiffstats
path: root/core/java
diff options
context:
space:
mode:
authorGilles Debunne <debunne@google.com>2010-06-23 10:30:27 -0700
committerGilles Debunne <debunne@google.com>2010-07-09 16:20:21 -0700
commitcc3ec6cdb2b892eb29513e72d8b205acbe997b25 (patch)
treee57aa48666ef30f527898aed11ce4ce1b3110c0f /core/java
parenta6935ab00923edeb5291a24b2363262b7fa37e85 (diff)
downloadframeworks_base-cc3ec6cdb2b892eb29513e72d8b205acbe997b25.zip
frameworks_base-cc3ec6cdb2b892eb29513e72d8b205acbe997b25.tar.gz
frameworks_base-cc3ec6cdb2b892eb29513e72d8b205acbe997b25.tar.bz2
New cursor controller in TextViews.
Editable TextView now display a cursor controller under the insertion point so that it can be precisely moved. Change-Id: Ia2e6ddc57d249647ff6683e10e4226db3df27223
Diffstat (limited to 'core/java')
-rw-r--r--core/java/android/text/TextLine.java4
-rw-r--r--core/java/android/text/method/ArrowKeyMovementMethod.java76
-rw-r--r--core/java/android/text/method/Touch.java4
-rw-r--r--core/java/android/widget/TextView.java331
4 files changed, 364 insertions, 51 deletions
diff --git a/core/java/android/text/TextLine.java b/core/java/android/text/TextLine.java
index 33c9554..0e3522e 100644
--- a/core/java/android/text/TextLine.java
+++ b/core/java/android/text/TextLine.java
@@ -21,8 +21,8 @@ import com.android.internal.util.ArrayUtils;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
-import android.graphics.RectF;
import android.graphics.Paint.FontMetricsInt;
+import android.graphics.RectF;
import android.text.Layout.Directions;
import android.text.Layout.TabStops;
import android.text.style.CharacterStyle;
@@ -583,7 +583,7 @@ class TextLine {
/**
* Returns the next valid offset within this directional run, skipping
* conjuncts and zero-width characters. This should not be called to walk
- * off the end of the line, since the the returned values might not be valid
+ * off the end of the line, since the returned values might not be valid
* on neighboring lines. If the returned offset is less than zero or
* greater than the line length, the offset should be recomputed on the
* preceding or following line, respectively.
diff --git a/core/java/android/text/method/ArrowKeyMovementMethod.java b/core/java/android/text/method/ArrowKeyMovementMethod.java
index 9af42cc..9df63a9 100644
--- a/core/java/android/text/method/ArrowKeyMovementMethod.java
+++ b/core/java/android/text/method/ArrowKeyMovementMethod.java
@@ -16,23 +16,28 @@
package android.text.method;
-import android.util.Log;
+import android.text.Layout;
+import android.text.NoCopySpan;
+import android.text.Selection;
+import android.text.Spannable;
import android.view.KeyEvent;
-import android.graphics.Rect;
-import android.text.*;
-import android.widget.TextView;
+import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
-import android.view.MotionEvent;
+import android.widget.TextView;
+import android.widget.TextView.CursorController;
// XXX this doesn't extend MetaKeyKeyListener because the signatures
// don't match. Need to figure that out. Meanwhile the meta keys
// won't work in fields that don't take input.
-public class
-ArrowKeyMovementMethod
-implements MovementMethod
-{
+public class ArrowKeyMovementMethod implements MovementMethod {
+ /**
+ * An optional controller for the cursor.
+ * Use {@link #setCursorController(CursorController)} to set this field.
+ */
+ protected CursorController mCursorController;
+
private boolean up(TextView widget, Spannable buffer) {
boolean cap = (MetaKeyKeyListener.getMetaState(buffer,
KeyEvent.META_SHIFT_ON) == 1) ||
@@ -226,13 +231,22 @@ implements MovementMethod
return false;
}
- public boolean onTrackballEvent(TextView widget, Spannable text,
- MotionEvent event) {
+ public boolean onTrackballEvent(TextView widget, Spannable text, MotionEvent event) {
+ if (mCursorController != null) {
+ mCursorController.hide();
+ }
return false;
}
- public boolean onTouchEvent(TextView widget, Spannable buffer,
- MotionEvent event) {
+ public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
+ if (mCursorController != null) {
+ return onTouchEventCursor(widget, buffer, event);
+ } else {
+ return onTouchEventStandard(widget, buffer, event);
+ }
+ }
+
+ private boolean onTouchEventStandard(TextView widget, Spannable buffer, MotionEvent event) {
int initialScrollX = -1, initialScrollY = -1;
if (event.getAction() == MotionEvent.ACTION_UP) {
initialScrollX = Touch.getInitialScrollX(widget, buffer);
@@ -291,13 +305,14 @@ implements MovementMethod
(MetaKeyKeyListener.getMetaState(buffer,
MetaKeyKeyListener.META_SELECTING) != 0);
+
if (cap && handled) {
// Before selecting, make sure we've moved out of the "slop".
// handled will be true, if we're in select mode AND we're
// OUT of the slop
// Turn long press off while we're selecting. User needs to
- // re-tap on the selection to enable longpress
+ // re-tap on the selection to enable long press
widget.cancelLongPress();
// Update selection as we're moving the selection area.
@@ -420,6 +435,39 @@ implements MovementMethod
return handled;
}
+ private boolean onTouchEventCursor(TextView widget, Spannable buffer, MotionEvent event) {
+ if (widget.isFocused() && !widget.didTouchFocusSelect()) {
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_MOVE:
+ 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();
+ return true;
+
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ mCursorController = null;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Defines the cursor controller.
+ *
+ * When set, this object can be used to handle events, that can be translated in cursor updates.
+ * @param cursorController A cursor controller implementation
+ */
+ public void setCursorController(CursorController cursorController) {
+ mCursorController = cursorController;
+ }
+
private static class DoubleTapState implements NoCopySpan {
long mWhen;
}
diff --git a/core/java/android/text/method/Touch.java b/core/java/android/text/method/Touch.java
index c30db20..3b98fc3 100644
--- a/core/java/android/text/method/Touch.java
+++ b/core/java/android/text/method/Touch.java
@@ -17,9 +17,9 @@
package android.text.method;
import android.text.Layout;
+import android.text.Layout.Alignment;
import android.text.NoCopySpan;
import android.text.Spannable;
-import android.text.Layout.Alignment;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
@@ -102,7 +102,7 @@ public class Touch {
MotionEvent event) {
DragState[] ds;
- switch (event.getAction()) {
+ switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
ds = buffer.getSpans(0, buffer.length(), DragState.class);
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 2fda2a5..f591483 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -61,6 +61,7 @@ import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.TextWatcher;
+import android.text.method.ArrowKeyMovementMethod;
import android.text.method.DateKeyListener;
import android.text.method.DateTimeKeyListener;
import android.text.method.DialerKeyListener;
@@ -89,10 +90,11 @@ import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
+import android.view.ViewConfiguration;
import android.view.ViewDebug;
+import android.view.ViewGroup.LayoutParams;
import android.view.ViewRoot;
import android.view.ViewTreeObserver;
-import android.view.ViewGroup.LayoutParams;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.animation.AnimationUtils;
@@ -185,7 +187,7 @@ import java.util.ArrayList;
*/
@RemoteView
public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
- static final String TAG = "TextView";
+ static final String LOG_TAG = "TextView";
static final boolean DEBUG_EXTRACT = false;
private static int PRIORITY = 100;
@@ -696,9 +698,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
try {
setInputExtras(a.getResourceId(attr, 0));
} catch (XmlPullParserException e) {
- Log.w("TextView", "Failure reading input extras", e);
+ Log.w(LOG_TAG, "Failure reading input extras", e);
} catch (IOException e) {
- Log.w("TextView", "Failure reading input extras", e);
+ Log.w(LOG_TAG, "Failure reading input extras", e);
}
break;
}
@@ -924,6 +926,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
setFocusable(focusable);
setClickable(clickable);
setLongClickable(longClickable);
+
+ prepareCursorController();
}
private void setTypefaceByIndex(int typefaceIndex, int styleIndex) {
@@ -2444,7 +2448,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
restored = "(restored) ";
}
- Log.e("TextView", "Saved cursor position " + ss.selStart +
+ Log.e(LOG_TAG, "Saved cursor position " + ss.selStart +
"/" + ss.selEnd + " out of range for " + restored +
"text " + mText);
} else {
@@ -3009,7 +3013,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
} else {
input = TextKeyListener.getInstance();
}
- mInputType = type;
+ setRawInputType(type);
if (direct) mInput = input;
else {
setKeyListenerOnly(input);
@@ -4089,6 +4093,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
*/
canvas.restore();
+
+ if (mInsertionPointCursorController != null) {
+ mInsertionPointCursorController.draw(canvas);
+ }
}
@Override
@@ -4607,7 +4615,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
if (req != null) {
InputMethodManager imm = InputMethodManager.peekInstance();
if (imm != null) {
- if (DEBUG_EXTRACT) Log.v(TAG, "Retrieving extracted start="
+ if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Retrieving extracted start="
+ ims.mChangedStart + " end=" + ims.mChangedEnd
+ " delta=" + ims.mChangedDelta);
if (ims.mChangedStart < 0 && !contentChanged) {
@@ -4615,7 +4623,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd,
ims.mChangedDelta, ims.mTmpExtracted)) {
- if (DEBUG_EXTRACT) Log.v(TAG, "Reporting extracted start="
+ if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Reporting extracted start="
+ ims.mTmpExtracted.partialStartOffset
+ " end=" + ims.mTmpExtracted.partialEndOffset
+ ": " + ims.mTmpExtracted.text);
@@ -4784,7 +4792,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
makeBlink();
}
}
-
+
checkForResize();
}
@@ -5508,7 +5516,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
// FIXME: Is it okay to truncate this, or should we round?
final int x = (int)mLayout.getPrimaryHorizontal(offset);
final int top = mLayout.getLineTop(line);
- final int bottom = mLayout.getLineTop(line+1);
+ final int bottom = mLayout.getLineTop(line + 1);
int left = (int) FloatMath.floor(mLayout.getLineLeft(line));
int right = (int) FloatMath.ceil(mLayout.getLineRight(line));
@@ -5645,8 +5653,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
// viewport coordinates, but requestRectangleOnScreen()
// is in terms of content coordinates.
- Rect r = new Rect();
- getInterestingRect(r, x, top, bottom, line);
+ Rect r = new Rect(x, top, x + 1, bottom);
+ getInterestingRect(r, line);
r.offset(mScrollX, mScrollY);
if (requestRectangleOnScreen(r)) {
@@ -5680,7 +5688,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
int line = mLayout.getLineForOffset(start);
final int top = mLayout.getLineTop(line);
- final int bottom = mLayout.getLineTop(line+1);
+ final int bottom = mLayout.getLineTop(line + 1);
final int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
int vslack = (bottom - top) / 2;
if (vslack > vspace / 4)
@@ -5730,22 +5738,28 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
}
- private void getInterestingRect(Rect r, int h, int top, int bottom,
- int line) {
+ private void getInterestingRect(Rect r, int line) {
+ convertFromViewportToContentCoordinates(r);
+
+ // Rectangle can can be expanded on first and last line to take
+ // padding into account.
+ // TODO Take left/right padding into account too?
+ if (line == 0) r.top -= getExtendedPaddingTop();
+ if (line == mLayout.getLineCount() - 1) r.bottom += getExtendedPaddingBottom();
+ }
+
+ private void convertFromViewportToContentCoordinates(Rect r) {
int paddingTop = getExtendedPaddingTop();
if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
paddingTop += getVerticalOffset(false);
}
- top += paddingTop;
- bottom += paddingTop;
- h += getCompoundPaddingLeft();
+ r.top += paddingTop;
+ r.bottom += paddingTop;
- if (line == 0)
- top -= getExtendedPaddingTop();
- if (line == mLayout.getLineCount() - 1)
- bottom += getExtendedPaddingBottom();
+ int paddingLeft = getCompoundPaddingLeft();
+ r.left += paddingLeft;
+ r.right += paddingLeft;
- r.set(h, top, h+1, bottom);
r.offset(-mScrollX, -mScrollY);
}
@@ -5913,6 +5927,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
} else if (mBlink != null) {
mBlink.removeCallbacks(mBlink);
}
+ prepareCursorController();
}
private boolean canMarquee() {
@@ -6327,7 +6342,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
}
} else {
- if (DEBUG_EXTRACT) Log.v(TAG, "Span change outside of batch: "
+ if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Span change outside of batch: "
+ oldStart + "-" + oldEnd + ","
+ newStart + "-" + newEnd + what);
ims.mContentChanged = true;
@@ -6343,7 +6358,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
public void beforeTextChanged(CharSequence buffer, int start,
int before, int after) {
- if (DEBUG_EXTRACT) Log.v(TAG, "beforeTextChanged start=" + start
+ if (DEBUG_EXTRACT) Log.v(LOG_TAG, "beforeTextChanged start=" + start
+ " before=" + before + " after=" + after + ": " + buffer);
if (AccessibilityManager.getInstance(mContext).isEnabled()
@@ -6356,7 +6371,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
public void onTextChanged(CharSequence buffer, int start,
int before, int after) {
- if (DEBUG_EXTRACT) Log.v(TAG, "onTextChanged start=" + start
+ if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onTextChanged start=" + start
+ " before=" + before + " after=" + after + ": " + buffer);
TextView.this.handleTextChanged(buffer, start, before, after);
@@ -6366,10 +6381,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
sendAccessibilityEventTypeViewTextChanged(mBeforeText, start, before, after);
mBeforeText = null;
}
+
+ // TODO. The cursor controller should hide as soon as text is typed.
+ // But this method is also used for cosmetic changes (underline current word when
+ // spell corrections are displayed. There is currently no way to make the difference
+ // between these cosmetic changes and actual text modifications.
}
public void afterTextChanged(Editable buffer) {
- if (DEBUG_EXTRACT) Log.v(TAG, "afterTextChanged: " + buffer);
+ if (DEBUG_EXTRACT) Log.v(LOG_TAG, "afterTextChanged: " + buffer);
TextView.this.sendAfterTextChanged(buffer);
if (MetaKeyKeyListener.getMetaState(buffer,
@@ -6380,19 +6400,19 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
public void onSpanChanged(Spannable buf,
Object what, int s, int e, int st, int en) {
- if (DEBUG_EXTRACT) Log.v(TAG, "onSpanChanged s=" + s + " e=" + e
+ if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanChanged s=" + s + " e=" + e
+ " st=" + st + " en=" + en + " what=" + what + ": " + buf);
TextView.this.spanChange(buf, what, s, st, e, en);
}
public void onSpanAdded(Spannable buf, Object what, int s, int e) {
- if (DEBUG_EXTRACT) Log.v(TAG, "onSpanAdded s=" + s + " e=" + e
+ if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanAdded s=" + s + " e=" + e
+ " what=" + what + ": " + buf);
TextView.this.spanChange(buf, what, -1, s, -1, e);
}
public void onSpanRemoved(Spannable buf, Object what, int s, int e) {
- if (DEBUG_EXTRACT) Log.v(TAG, "onSpanRemoved s=" + s + " e=" + e
+ if (DEBUG_EXTRACT) Log.v(LOG_TAG, "onSpanRemoved s=" + s + " e=" + e
+ " what=" + what + ": " + buf);
TextView.this.spanChange(buf, what, s, -1, e, -1);
}
@@ -6589,6 +6609,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
if (resultCode == InputMethodManager.RESULT_SHOWN) {
start = mPrevStart;
end = mPrevEnd;
+ } else if (mInsertionPointCursorController != null) {
+ mInsertionPointCursorController.show();
}
final int len = mText.length();
@@ -6631,12 +6653,16 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
int oldSelStart = Selection.getSelectionStart(mText);
int oldSelEnd = Selection.getSelectionEnd(mText);
-
+
+ if (mInsertionPointCursorController != null) {
+ mInsertionPointCursorController.onTouchEvent(event);
+ }
+
if (mMovement != null) {
handled |= mMovement.onTouchEvent(this, (Spannable) mText, event);
}
- if (mText instanceof Editable && onCheckIsTextEditor()) {
+ if (isTextEditable()) {
if (action == MotionEvent.ACTION_UP && isFocused() && !mScrolled) {
InputMethodManager imm = (InputMethodManager)
getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
@@ -6650,7 +6676,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
newSelStart, newSelEnd);
}
- handled = imm.showSoftInput(this, 0, csr) && (csr != null);
+ handled |= imm.showSoftInput(this, 0, csr) && (csr != null);
}
}
@@ -6662,6 +6688,19 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
return superResult;
}
+ private void prepareCursorController() {
+ // TODO Add an extra android:cursorController flag to disable the controller?
+ mInsertionPointCursorController =
+ mCursorVisible ? new InsertionPointCursorController() : null;
+ }
+
+ /**
+ * @return True iff this TextView contains a text that can be edited.
+ */
+ private boolean isTextEditable() {
+ return mText instanceof Editable && onCheckIsTextEditor();
+ }
+
/**
* Returns true, only while processing a touch gesture, if the initial
* touch down event caused focus to move to the text view and as a result
@@ -7341,6 +7380,229 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
return false;
}
+ /**
+ * A CursorController instance can be used to control a cursor in the text.
+ *
+ * It can be passed to an {@link ArrowKeyMovementMethod} which can intercepts events
+ * and send them to this object instead of the cursor.
+ */
+ public interface CursorController {
+ /**
+ * Makes the cursor controller visible on screen. Will be drawn by {@link #draw(Canvas)}.
+ * See also {@link #hide()}.
+ */
+ public void show();
+
+ /**
+ * Hide the cursor controller from screen.
+ * See also {@link #show()}.
+ */
+ public void hide();
+
+ /**
+ * Update the controller's position.
+ */
+ public void updatePosition();
+
+ /**
+ * 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();
+
+ /**
+ * @return Vertical offset between the controller and the cursor.
+ */
+ public int getOffsetY();
+
+ /**
+ * This method is called by {@link #onTouchEvent(MotionEvent)} and gives the controller
+ * a chance to become active and/or visible.
+ * @param event The touch event
+ */
+ public void onTouchEvent(MotionEvent event);
+
+ /**
+ * Draws a visual representation of the controller on the canvas.
+ *
+ * Called at the end of {@link #draw(Canvas)}, in the content coordinates system.
+ * @param canvas The Canvas used by this TextView.
+ */
+ public void draw(Canvas canvas);
+ }
+
+ 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
+ private final Drawable mDrawable;
+ // 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;
+
+ InsertionPointCursorController() {
+ Resources res = mContext.getResources();
+ mDrawable = res.getDrawable(com.android.internal.R.drawable.cursor_controller);
+ }
+
+ public void show() {
+ updatePosition();
+ // Has to be done after updatePosition, so that previous position invalidate
+ // in only done if necessary.
+ mIsVisible = true;
+ }
+
+ public void hide() {
+ if (mIsVisible) {
+ long time = System.currentTimeMillis();
+ // 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);
+ }
+ }
+ }
+
+ public void draw(Canvas canvas) {
+ 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);
+ } else {
+ time -= DELAY_BEFORE_FADE_OUT;
+ if (time <= FADE_OUT_DURATION) {
+ int alpha = 255 * (FADE_OUT_DURATION - time) / FADE_OUT_DURATION;
+ mDrawable.setAlpha(alpha);
+ postInvalidateDelayed(30,
+ mBounds.left, mBounds.top, mBounds.right, mBounds.bottom);
+ } else {
+ mDrawable.setAlpha(0);
+ mIsVisible = false;
+ }
+ }
+ mDrawable.draw(canvas);
+ }
+ }
+
+ public void updatePosition() {
+ if (mIsVisible) {
+ // Clear previous cursor controller before bounds are updated
+ postInvalidate(mBounds.left, mBounds.top, mBounds.right, mBounds.bottom);
+ }
+
+ final int offset = Selection.getSelectionStart(mText);
+
+ if (offset < 0) {
+ // Should never happen, safety check.
+ Log.w(LOG_TAG, "Update cursor controller position called with no cursor", null);
+ mIsVisible = false;
+ return;
+ }
+
+ final int cursorControllerDrawableWidth = mDrawable.getIntrinsicWidth();
+ final int cursorControllerDrawableHeight = mDrawable.getIntrinsicHeight();
+ final int line = mLayout.getLineForOffset(offset);
+
+ mBounds.left = (int) (mLayout.getPrimaryHorizontal(offset) - 0.5 -
+ cursorControllerDrawableWidth / 2.0);
+ mBounds.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) {
+ mBounds.top -= Math.max(0,
+ cursorControllerDrawableHeight / 2 - getExtendedPaddingBottom());
+ }
+
+ mBounds.right = mBounds.left + cursorControllerDrawableWidth;
+ mBounds.bottom = mBounds.top + cursorControllerDrawableHeight;
+
+ convertFromViewportToContentCoordinates(mBounds);
+ mDrawable.setBounds(mBounds);
+
+ mFadeOutTimerStart = System.currentTimeMillis();
+ mDrawable.setAlpha(255);
+
+ postInvalidate(mBounds.left, mBounds.top, mBounds.right, mBounds.bottom);
+ }
+
+ 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);
+ }
+
+ if (mParent != null) {
+ // Prevent possible scrollView parent from scrolling, so that
+ // we can use auto-scrolling.
+ mParent.requestDisallowInterceptTouchEvent(true);
+
+ Resources res = mContext.getResources();
+ final int verticalOffset = res.getDimensionPixelOffset(
+ com.android.internal.R.dimen.cursor_controller_vertical_offset);
+
+ mOffsetX = (mBounds.left + mBounds.right) / 2 - x;
+ mOffsetY = mBounds.top - verticalOffset - y;
+
+ mOnDownTimerStart = System.currentTimeMillis();
+ }
+ }
+ } 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() {
+ return mOffsetX;
+ }
+
+ public int getOffsetY() {
+ return mOffsetY;
+ }
+ }
+
@ViewDebug.ExportedProperty
private CharSequence mText;
private CharSequence mTransformed;
@@ -7369,6 +7631,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
private Blink mBlink;
private boolean mCursorVisible = true;
+ // Cursor Controller. Null when disabled.
+ private CursorController mInsertionPointCursorController;
+
private boolean mSelectAllOnFocus = false;
private int mGravity = Gravity.TOP | Gravity.LEFT;