From f75c97e023af7d4ad9a8c129d4ea282b1c3b8f94 Mon Sep 17 00:00:00 2001 From: Gilles Debunne Date: Thu, 10 Feb 2011 16:09:53 -0800 Subject: [PATCH] Text insertion cursor is now defined by a Drawable. Bug 3261766 If defined, the drawable is used instead of directly drawing a 1 pixel line. This makes the cursor more fancy and more visible. The drawable is currently clipped by the TextView's limits, which is currently visible on the left when the cursor is at the first position. To solve this issue properly, we would need to propagate a do-not-clip up in the hierarchy. Change-Id: I99f6001048eed14104994acf6bab942dda8eb38e --- core/java/android/text/Layout.java | 6 +- core/java/android/widget/TextView.java | 169 +++++++++++++----- .../drawable-mdpi/text_cursor_holo_dark.9.png | Bin 0 -> 274 bytes .../text_cursor_holo_light.9.png | Bin 0 -> 249 bytes core/res/res/values/attrs.xml | 6 + core/res/res/values/public.xml | 3 + core/res/res/values/styles.xml | 1 + core/res/res/values/themes.xml | 3 + 8 files changed, 140 insertions(+), 48 deletions(-) create mode 100644 core/res/res/drawable-mdpi/text_cursor_holo_dark.9.png create mode 100644 core/res/res/drawable-mdpi/text_cursor_holo_light.9.png diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index 8700af802ae64..97a216a8415d0 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -600,8 +600,9 @@ public abstract class Layout { * are at different run levels (and thus there's a split caret). * @param offset the offset * @return true if at a level boundary + * @hide */ - private boolean isLevelBoundary(int offset) { + public boolean isLevelBoundary(int offset) { int line = getLineForOffset(offset); Directions dirs = getLineDirections(line); if (dirs == DIRS_ALL_LEFT_TO_RIGHT || dirs == DIRS_ALL_RIGHT_TO_LEFT) { @@ -1148,8 +1149,7 @@ public abstract class Layout { int bottom = getLineTop(line+1); float h1 = getPrimaryHorizontal(point) - 0.5f; - float h2 = isLevelBoundary(point) ? - getSecondaryHorizontal(point) - 0.5f : h1; + float h2 = isLevelBoundary(point) ? getSecondaryHorizontal(point) - 0.5f : h1; int caps = TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SHIFT_ON) | TextKeyListener.getMetaState(editingBuffer, TextKeyListener.META_SELECTING); diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 28b106bc4be4b..993af31c31cd6 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -304,15 +304,19 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } InputMethodState mInputMethodState; - int mTextSelectHandleLeftRes; - int mTextSelectHandleRightRes; - int mTextSelectHandleRes; - int mTextEditPasteWindowLayout, mTextEditSidePasteWindowLayout; - int mTextEditNoPasteWindowLayout, mTextEditSideNoPasteWindowLayout; + private int mTextSelectHandleLeftRes; + private int mTextSelectHandleRightRes; + private int mTextSelectHandleRes; + private int mTextEditPasteWindowLayout, mTextEditSidePasteWindowLayout; + private int mTextEditNoPasteWindowLayout, mTextEditSideNoPasteWindowLayout; - Drawable mSelectHandleLeft; - Drawable mSelectHandleRight; - Drawable mSelectHandleCenter; + private int mCursorDrawableRes; + private final Drawable[] mCursorDrawable = new Drawable[2]; + private int mCursorCount; // Actual current number of used mCursorDrawable: 0, 1 or 2 + + private Drawable mSelectHandleLeft; + private Drawable mSelectHandleRight; + private Drawable mSelectHandleCenter; private int mLastDownPositionX, mLastDownPositionY; private Callback mCustomSelectionActionModeCallback; @@ -742,6 +746,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } break; + case com.android.internal.R.styleable.TextView_textCursorDrawable: + mCursorDrawableRes = a.getResourceId(attr, 0); + break; + case com.android.internal.R.styleable.TextView_textSelectHandleLeft: mTextSelectHandleLeftRes = a.getResourceId(attr, 0); break; @@ -3770,33 +3778,40 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (mHighlightPathBogus) { invalidateCursor(); } else { - synchronized (sTempRect) { - /* - * The reason for this concern about the thickness of the - * cursor and doing the floor/ceil on the coordinates is that - * some EditTexts (notably textfields in the Browser) have - * anti-aliased text where not all the characters are - * necessarily at integer-multiple locations. This should - * make sure the entire cursor gets invalidated instead of - * sometimes missing half a pixel. - */ + final int horizontalPadding = getCompoundPaddingLeft(); + final int verticalPadding = getExtendedPaddingTop() + getVerticalOffset(true); - float thick = FloatMath.ceil(mTextPaint.getStrokeWidth()); - if (thick < 1.0f) { - thick = 1.0f; + if (mCursorCount == 0) { + synchronized (sTempRect) { + /* + * The reason for this concern about the thickness of the + * cursor and doing the floor/ceil on the coordinates is that + * some EditTexts (notably textfields in the Browser) have + * anti-aliased text where not all the characters are + * necessarily at integer-multiple locations. This should + * make sure the entire cursor gets invalidated instead of + * sometimes missing half a pixel. + */ + float thick = FloatMath.ceil(mTextPaint.getStrokeWidth()); + if (thick < 1.0f) { + thick = 1.0f; + } + + thick /= 2.0f; + + mHighlightPath.computeBounds(sTempRect, false); + + invalidate((int) FloatMath.floor(horizontalPadding + sTempRect.left - thick), + (int) FloatMath.floor(verticalPadding + sTempRect.top - thick), + (int) FloatMath.ceil(horizontalPadding + sTempRect.right + thick), + (int) FloatMath.ceil(verticalPadding + sTempRect.bottom + thick)); + } + } else { + for (int i = 0; i < mCursorCount; i++) { + Rect bounds = mCursorDrawable[i].getBounds(); + invalidate(bounds.left + horizontalPadding, bounds.top + verticalPadding, + bounds.right + horizontalPadding, bounds.bottom + verticalPadding); } - - thick /= 2; - - mHighlightPath.computeBounds(sTempRect, false); - - int left = getCompoundPaddingLeft(); - int top = getExtendedPaddingTop() + getVerticalOffset(true); - - invalidate((int) FloatMath.floor(left + sTempRect.left - thick), - (int) FloatMath.floor(top + sTempRect.top - thick), - (int) FloatMath.ceil(left + sTempRect.right + thick), - (int) FloatMath.ceil(top + sTempRect.bottom + thick)); } } } @@ -3836,13 +3851,23 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener line2 = mLayout.getLineForOffset(last); int bottom = mLayout.getLineTop(line2 + 1); - int voffset = getVerticalOffset(true); - int left = getCompoundPaddingLeft() + mScrollX; - invalidate(left, top + voffset + getExtendedPaddingTop(), - left + getWidth() - getCompoundPaddingLeft() - - getCompoundPaddingRight(), - bottom + voffset + getExtendedPaddingTop()); + final int horizontalPadding = getCompoundPaddingLeft(); + final int verticalPadding = getExtendedPaddingTop() + getVerticalOffset(true); + + // If used, the cursor drawables can have an arbitrary dimension that can go beyond + // the invalidated lines specified above. + for (int i = 0; i < mCursorCount; i++) { + Rect bounds = mCursorDrawable[i].getBounds(); + top = Math.min(top, bounds.top); + bottom = Math.max(bottom, bounds.bottom); + // Horizontal bounds are already full width, no need to update + } + + invalidate(horizontalPadding + mScrollX, top + verticalPadding, + horizontalPadding + mScrollX + getWidth() - + getCompoundPaddingLeft() - getCompoundPaddingRight(), + bottom + verticalPadding); } } } @@ -4346,6 +4371,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener Path highlight = null; int selStart = -1, selEnd = -1; + boolean drawCursor = false; // If there is no movement method, then there can be no selection. // Check that first and attempt to skip everything having to do with @@ -4366,6 +4392,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (mHighlightPathBogus) { mHighlightPath.reset(); mLayout.getCursorPath(selStart, mHighlightPath, mText); + updateCursorsPositions(); mHighlightPathBogus = false; } @@ -4377,7 +4404,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } mHighlightPaint.setStyle(Paint.Style.STROKE); - highlight = mHighlightPath; + if (mCursorCount > 0) { + drawCursor = true; + } else { + highlight = mHighlightPath; + } } } else { if (mHighlightPathBogus) { @@ -4460,6 +4491,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mCorrectionHighlighter.draw(canvas, cursorOffsetVertical); } + if (drawCursor) drawCursor(canvas, cursorOffsetVertical); + layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical); if (mMarquee != null && mMarquee.shouldDrawGhost()) { @@ -4478,6 +4511,52 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener updateCursorControllerPositions(); } + private void updateCursorsPositions() { + if (mCursorDrawableRes == 0) return; + + final int offset = getSelectionStart(); + final int line = mLayout.getLineForOffset(offset); + final int top = mLayout.getLineTop(line); + final int bottom = mLayout.getLineTop(line + 1); + + mCursorCount = mLayout.isLevelBoundary(offset) ? 2 : 1; + + int middle = bottom; + if (mCursorCount == 2) { + // Similar to what is done in {@link Layout.#getCursorPath(int, Path, CharSequence)} + middle = (top + bottom) >> 1; + } + + updateCursorPosition(0, top, middle, mLayout.getPrimaryHorizontal(offset)); + + if (mCursorCount == 2) { + updateCursorPosition(1, middle, bottom, mLayout.getSecondaryHorizontal(offset)); + } + } + + private void updateCursorPosition(int cursorIndex, int top, int bottom, float horizontal) { + if (mCursorDrawable[cursorIndex] == null) + mCursorDrawable[cursorIndex] = mContext.getResources().getDrawable(mCursorDrawableRes); + + if (mTempRect == null) mTempRect = new Rect(); + + mCursorDrawable[cursorIndex].getPadding(mTempRect); + final int width = mCursorDrawable[cursorIndex].getIntrinsicWidth(); + horizontal = Math.max(0.5f, horizontal - 0.5f); + final int left = (int) (horizontal) - mTempRect.left; + mCursorDrawable[cursorIndex].setBounds(left, top - mTempRect.top, left + width, + bottom + mTempRect.bottom); + } + + private void drawCursor(Canvas canvas, int cursorOffsetVertical) { + final boolean translate = cursorOffsetVertical != 0; + if (translate) canvas.translate(0, cursorOffsetVertical); + for (int i = 0; i < mCursorCount; i++) { + mCursorDrawable[i].draw(canvas); + } + if (translate) canvas.translate(0, -cursorOffsetVertical); + } + /** * Update the positions of the CursorControllers. Needed by WebTextView, * which does not draw. @@ -8699,7 +8778,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } mDrawable = mSelectHandleLeft; handleWidth = mDrawable.getIntrinsicWidth(); - mHotspotX = (handleWidth * 3) / 4; + mHotspotX = handleWidth * 3.0f / 4.0f; break; } @@ -8710,7 +8789,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } mDrawable = mSelectHandleRight; handleWidth = mDrawable.getIntrinsicWidth(); - mHotspotX = handleWidth / 4; + mHotspotX = handleWidth / 4.0f; break; } @@ -8722,7 +8801,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } mDrawable = mSelectHandleCenter; handleWidth = mDrawable.getIntrinsicWidth(); - mHotspotX = handleWidth / 2; + mHotspotX = handleWidth / 2.0f; mIsInsertionHandle = true; break; } @@ -8937,8 +9016,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener final int lineBottom = mLayout.getLineBottom(line); final Rect bounds = sCursorControllerTempRect; - bounds.left = (int) (mLayout.getPrimaryHorizontal(offset) - mHotspotX) - + TextView.this.mScrollX; + bounds.left = (int) (mLayout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX) + + TextView.this.mScrollX; bounds.top = (bottom ? lineBottom : lineTop - mHeight) + TextView.this.mScrollY; bounds.right = bounds.left + width; diff --git a/core/res/res/drawable-mdpi/text_cursor_holo_dark.9.png b/core/res/res/drawable-mdpi/text_cursor_holo_dark.9.png new file mode 100644 index 0000000000000000000000000000000000000000..b9435b64511773d32ecda560c00c6e077de9ea72 GIT binary patch literal 274 zcmeAS@N?(olHy`uVBq!ia0vp^oIuRZ!3HF6%}!4MQjEnx?oJHr&dIz4a@dl*-CY>| zgW!U_%O?Xxc6quuhDcm)?YHGSn84r=Z*QmDIe%fGy`rL8^AQEL?jwS84(yzBfY&4D zfLfxX$6r@r&(K?2G8d_FH=FN!Y5PO@ zV4Xxlwchlbb6TGlaWdy9)ihWf$d#9|4Y!ero>$Vmt1=-s`r%ig(f;4>?>4u2=#cwQ Td*TnEFBm*s{an^LB{Ts578`0K literal 0 HcmV?d00001 diff --git a/core/res/res/drawable-mdpi/text_cursor_holo_light.9.png b/core/res/res/drawable-mdpi/text_cursor_holo_light.9.png new file mode 100644 index 0000000000000000000000000000000000000000..477d820cc965f31f065dc5abd08bcb2fbbec63a7 GIT binary patch literal 249 zcmeAS@N?(olHy`uVBq!ia0vp^oIuRZ!3HF6%}!4MQjEnx?oJHr&dIz4a@dl*-CY>| zgW!U_%O?Xx7J9lkhDcm)?YHJTxKAlGt;B_kA5hZ!Q1*{w{Y}k&lXWVDD+5V;MYM{an^LB{Ts5J(OaC literal 0 HcmV?d00001 diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 8802003af5180..6f37dc0970105 100755 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -778,6 +778,9 @@ + + + @@ -2783,6 +2786,9 @@ + + + diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml index aaf071b329ec9..454257522a09c 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -1642,4 +1642,7 @@ + + + diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml index 5700641f3986a..8cc5944dc0588 100644 --- a/core/res/res/values/styles.xml +++ b/core/res/res/values/styles.xml @@ -426,6 +426,7 @@ ?android:attr/textEditNoPasteWindowLayout ?android:attr/textEditSidePasteWindowLayout ?android:attr/textEditSideNoPasteWindowLayout + ?android:attr/textCursorDrawable