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