diff --git a/api/current.xml b/api/current.xml
index 16de702b6ec3f..db2456a339570 100644
--- a/api/current.xml
+++ b/api/current.xml
@@ -166294,6 +166294,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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;
diff --git a/core/res/res/drawable-hdpi/cursor_controller.png b/core/res/res/drawable-hdpi/cursor_controller.png
new file mode 100644
index 0000000000000..720adedae2184
Binary files /dev/null and b/core/res/res/drawable-hdpi/cursor_controller.png differ
diff --git a/core/res/res/drawable-mdpi/cursor_controller.png b/core/res/res/drawable-mdpi/cursor_controller.png
new file mode 100644
index 0000000000000..1a8a459098744
Binary files /dev/null and b/core/res/res/drawable-mdpi/cursor_controller.png differ
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index 61e7b882c085a..4d67bdd77a9a7 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -45,4 +45,6 @@
56dip
4dip
+
+ 12dp
diff --git a/graphics/java/android/graphics/Canvas.java b/graphics/java/android/graphics/Canvas.java
index a4df80cc3e9d7..77a1930b5fc3f 100644
--- a/graphics/java/android/graphics/Canvas.java
+++ b/graphics/java/android/graphics/Canvas.java
@@ -126,6 +126,7 @@ public class Canvas {
*
* @deprecated This constructor is not supported and should not be invoked.
*/
+ @Deprecated
public Canvas(GL gl) {
mNativeCanvas = initGL();
mGL = gl;
@@ -151,6 +152,7 @@ public class Canvas {
*
* @deprecated This method is not supported and should not be invoked.
*/
+ @Deprecated
public GL getGL() {
return mGL;
}
@@ -162,6 +164,7 @@ public class Canvas {
*
* @deprecated This method is not supported and should not be invoked.
*/
+ @Deprecated
public static void freeGlCaches() {
freeCaches();
}
@@ -198,6 +201,7 @@ public class Canvas {
*
* @deprecated This method is not supported and should not be invoked.
*/
+ @Deprecated
public void setViewport(int width, int height) {
if (mGL != null) {
nativeSetViewport(mNativeCanvas, width, height);
@@ -415,8 +419,8 @@ public class Canvas {
*
* @param sx The amount to scale in X
* @param sy The amount to scale in Y
- * @param px The x-coord for the pivot point (unchanged by the rotation)
- * @param py The y-coord for the pivot point (unchanged by the rotation)
+ * @param px The x-coord for the pivot point (unchanged by the scale)
+ * @param py The y-coord for the pivot point (unchanged by the scale)
*/
public final void scale(float sx, float sy, float px, float py) {
translate(px, py);
@@ -1585,6 +1589,7 @@ public class Canvas {
restore();
}
+ @Override
protected void finalize() throws Throwable {
super.finalize();
// If the constructor threw an exception before setting mNativeCanvas, the native finalizer
diff --git a/graphics/java/android/graphics/Rect.java b/graphics/java/android/graphics/Rect.java
index 98ffb8b4318ba..78302244e1c1b 100644
--- a/graphics/java/android/graphics/Rect.java
+++ b/graphics/java/android/graphics/Rect.java
@@ -75,6 +75,7 @@ public final class Rect implements Parcelable {
bottom = r.bottom;
}
+ @Override
public boolean equals(Object obj) {
Rect r = (Rect) obj;
if (r != null) {
@@ -84,6 +85,7 @@ public final class Rect implements Parcelable {
return false;
}
+ @Override
public String toString() {
StringBuilder sb = new StringBuilder(32);
sb.append("Rect("); sb.append(left); sb.append(", ");
@@ -351,7 +353,7 @@ public final class Rect implements Parcelable {
* rectangle, return true and set this rectangle to that intersection,
* otherwise return false and do not change this rectangle. No check is
* performed to see if either rectangle is empty. Note: To just test for
- * intersection, use intersects()
+ * intersection, use {@link #intersects(Rect, Rect)}.
*
* @param left The left side of the rectangle being intersected with this
* rectangle
@@ -445,7 +447,7 @@ public final class Rect implements Parcelable {
/**
* Returns true iff the two specified rectangles intersect. In no event are
* either of the rectangles modified. To record the intersection,
- * use intersect() or setIntersect().
+ * use {@link #intersect(Rect)} or {@link #setIntersect(Rect, Rect)}.
*
* @param a The first rectangle being tested for intersection
* @param b The second rectangle being tested for intersection