diff options
-rw-r--r-- | api/current.xml | 112 | ||||
-rw-r--r-- | core/java/android/text/TextLine.java | 4 | ||||
-rw-r--r-- | core/java/android/text/method/ArrowKeyMovementMethod.java | 76 | ||||
-rw-r--r-- | core/java/android/text/method/Touch.java | 4 | ||||
-rw-r--r-- | core/java/android/widget/TextView.java | 331 | ||||
-rw-r--r-- | core/res/res/drawable-hdpi/cursor_controller.png | bin | 0 -> 2886 bytes | |||
-rw-r--r-- | core/res/res/drawable-mdpi/cursor_controller.png | bin | 0 -> 1622 bytes | |||
-rw-r--r-- | core/res/res/values/dimens.xml | 2 | ||||
-rw-r--r-- | graphics/java/android/graphics/Canvas.java | 9 | ||||
-rw-r--r-- | graphics/java/android/graphics/Rect.java | 6 |
10 files changed, 489 insertions, 55 deletions
diff --git a/api/current.xml b/api/current.xml index 16de702..db2456a 100644 --- a/api/current.xml +++ b/api/current.xml @@ -166294,6 +166294,29 @@ <parameter name="event" type="android.view.MotionEvent"> </parameter> </method> +<method name="setCursorController" + return="void" + abstract="false" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="cursorController" type="android.widget.TextView.CursorController"> +</parameter> +</method> +<field name="mCursorController" + type="android.widget.TextView.CursorController" + transient="false" + volatile="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="protected" +> +</field> </class> <class name="BaseKeyListener" extends="android.text.method.MetaKeyKeyListener" @@ -222813,6 +222836,95 @@ > </method> </class> +<interface name="TextView.CursorController" + abstract="true" + static="true" + final="false" + deprecated="not deprecated" + visibility="public" +> +<method name="draw" + return="void" + abstract="true" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="canvas" type="android.graphics.Canvas"> +</parameter> +</method> +<method name="getOffsetX" + return="int" + abstract="true" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="getOffsetY" + return="int" + abstract="true" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="hide" + return="void" + abstract="true" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="onTouchEvent" + return="void" + abstract="true" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +<parameter name="event" type="android.view.MotionEvent"> +</parameter> +</method> +<method name="show" + return="void" + abstract="true" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> +<method name="updatePosition" + return="void" + abstract="true" + native="false" + synchronized="false" + static="false" + final="false" + deprecated="not deprecated" + visibility="public" +> +</method> +</interface> <interface name="TextView.OnEditorActionListener" abstract="true" static="true" 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; diff --git a/core/res/res/drawable-hdpi/cursor_controller.png b/core/res/res/drawable-hdpi/cursor_controller.png Binary files differnew file mode 100644 index 0000000..720aded --- /dev/null +++ b/core/res/res/drawable-hdpi/cursor_controller.png diff --git a/core/res/res/drawable-mdpi/cursor_controller.png b/core/res/res/drawable-mdpi/cursor_controller.png Binary files differnew file mode 100644 index 0000000..1a8a459 --- /dev/null +++ b/core/res/res/drawable-mdpi/cursor_controller.png diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index 61e7b88..4d67bdd 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -45,4 +45,6 @@ <dimen name="password_keyboard_key_height">56dip</dimen> <!-- Default correction for the space key in the password keyboard --> <dimen name="password_keyboard_spacebar_vertical_correction">4dip</dimen> + <!-- Distance between the text base line and virtual finger position used to position cursor --> + <dimen name="cursor_controller_vertical_offset">12dp</dimen> </resources> diff --git a/graphics/java/android/graphics/Canvas.java b/graphics/java/android/graphics/Canvas.java index a4df80c..77a1930 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 98ffb8b..7830224 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 |