From 2ff2cd873cc2a3f6a17cdaeb3e64b83ae2c6fda9 Mon Sep 17 00:00:00 2001 From: Mady Mellor Date: Mon, 2 Mar 2015 10:37:01 -0800 Subject: [PATCH] Text selection changes - smart handles & drag accelerator Drag accelerator: After entering text selection mode, immediately dragging your finger along the screen will select text. Once you have dragged past the initial word the selection an offset is added so that the end of the selection is not below your finger. Smart handles: When expanding the selection the handles snap to words, when shrinking the selection the handles go by character, once you're in by-character mode you can expand by character until you hit a word boundary where you will then expand by word again. Your finger must past the first ~2 characters in a word before the handle will jump to the word boundary. Bug: 19356160 Bug: 19355947 Change-Id: I79b8ec5ae3159148cd2f15d2e63dd5045c2069e4 --- core/java/android/widget/Editor.java | 389 ++++++++++++++++-- core/java/android/widget/TextView.java | 11 +- .../text_select_handle_left_mtrl_alpha.png | Bin 478 -> 368 bytes .../text_select_handle_right_mtrl_alpha.png | Bin 486 -> 349 bytes .../text_select_handle_left_mtrl_alpha.png | Bin 346 -> 267 bytes .../text_select_handle_right_mtrl_alpha.png | Bin 347 -> 241 bytes .../text_select_handle_left_mtrl_alpha.png | Bin 751 -> 467 bytes .../text_select_handle_right_mtrl_alpha.png | Bin 752 -> 466 bytes .../text_select_handle_left_mtrl_alpha.png | Bin 1148 -> 684 bytes .../text_select_handle_right_mtrl_alpha.png | Bin 1155 -> 630 bytes .../text_select_handle_left_mtrl_alpha.png | Bin 0 -> 889 bytes .../text_select_handle_right_mtrl_alpha.png | Bin 0 -> 858 bytes 12 files changed, 368 insertions(+), 32 deletions(-) create mode 100644 core/res/res/drawable-xxxhdpi/text_select_handle_left_mtrl_alpha.png create mode 100644 core/res/res/drawable-xxxhdpi/text_select_handle_right_mtrl_alpha.png diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index 1ba11da1f6f8c..4f2f52a199326 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -218,6 +218,12 @@ public class Editor { WordIterator mWordIterator; SpellChecker mSpellChecker; + // This word iterator is set with text and used to determine word boundaries + // when a user is selecting text. + private WordIterator mWordIteratorWithText; + // Indicate that the text in the word iterator needs to be updated. + private boolean mUpdateWordIteratorText; + private Rect mTempRect; private TextView mTextView; @@ -684,9 +690,52 @@ public class Editor { return mTextView.getTransformationMethod() instanceof PasswordTransformationMethod; } + private int getWordStart(int offset) { + // FIXME - For this and similar methods we're not doing anything to check if there's + // a LocaleSpan in the text, this may be something we should try handling or checking for. + int retOffset = getWordIteratorWithText().getBeginning(offset); + if (retOffset == BreakIterator.DONE) retOffset = offset; + return retOffset; + } + + private int getWordEnd(int offset, boolean includePunctuation) { + int retOffset = getWordIteratorWithText().getEnd(offset); + if (retOffset == BreakIterator.DONE) { + retOffset = offset; + } else if (includePunctuation) { + retOffset = handlePunctuation(retOffset); + } + return retOffset; + } + + private boolean isEndBoundary(int offset) { + int thisEnd = getWordEnd(offset, false); + return offset == thisEnd; + } + + private boolean isStartBoundary(int offset) { + int thisStart = getWordStart(offset); + return thisStart == offset; + } + + private int handlePunctuation(int offset) { + // FIXME - Check with UX how repeated ending punctuation should be handled. + // FIXME - Check with UX if / how we would handle non sentence ending characters. + // FIXME - Consider punctuation in different languages. + CharSequence text = mTextView.getText(); + if (offset < text.length()) { + int c = Character.codePointAt(text, offset); + if (c == 0x002e /* period */|| c == 0x003f /* question mark */ + || c == 0x0021 /* exclamation mark */) { + offset = Character.offsetByCodePoints(text, offset, 1); + } + } + return offset; + } + /** - * Adjusts selection to the word under last touch offset. - * Return true if the operation was successfully performed. + * Adjusts selection to the word under last touch offset. Return true if the operation was + * successfully performed. */ private boolean selectCurrentWord() { if (!canSelectText()) { @@ -733,6 +782,8 @@ public class Editor { selectionStart = ((Spanned) mTextView.getText()).getSpanStart(urlSpan); selectionEnd = ((Spanned) mTextView.getText()).getSpanEnd(urlSpan); } else { + // FIXME - We should check if there's a LocaleSpan in the text, this may be + // something we should try handling or checking for. final WordIterator wordIterator = getWordIterator(); wordIterator.setCharSequence(mTextView.getText(), minOffset, maxOffset); @@ -755,6 +806,7 @@ public class Editor { void onLocaleChanged() { // Will be re-created on demand in getWordIterator with the proper new locale mWordIterator = null; + mWordIteratorWithText = null; } /** @@ -767,6 +819,23 @@ public class Editor { return mWordIterator; } + private WordIterator getWordIteratorWithText() { + if (mWordIteratorWithText == null) { + mWordIteratorWithText = new WordIterator(mTextView.getTextServicesLocale()); + mUpdateWordIteratorText = true; + } + if (mUpdateWordIteratorText) { + // FIXME - Shouldn't copy all of the text as only the area of the text relevant + // to the user's selection is needed. A possible solution would be to + // copy some number N of characters near the selection and then when the + // user approaches N then we'd do another copy of the next N characters. + CharSequence text = mTextView.getText(); + mWordIteratorWithText.setCharSequence(text, 0, text.length()); + mUpdateWordIteratorText = false; + } + return mWordIteratorWithText; + } + private long getCharRange(int offset) { final int textLength = mTextView.getText().length(); if (offset + 1 < textLength) { @@ -915,9 +984,8 @@ public class Editor { mTextView.startDrag(data, getTextThumbnailBuilder(selectedText), localState, 0); stopSelectionActionMode(); } else { - getSelectionController().hide(); - selectCurrentWord(); - getSelectionController().show(); + stopSelectionActionMode(); + startSelectionActionMode(); } handled = true; } @@ -1053,6 +1121,9 @@ public class Editor { void sendOnTextChanged(int start, int after) { updateSpellCheckSpans(start, start + after, false); + // Flip flag to indicate the word iterator needs to have the text reset. + mUpdateWordIteratorText = true; + // Hide the controllers as soon as text is modified (typing, procedural...) // We do not hide the span controllers, since they can be added when a new text is // inserted into the text view (voice IME). @@ -1608,6 +1679,9 @@ public class Editor { } } + if (selectionStarted) { + getSelectionController().enterDrag(); + } return selectionStarted; } @@ -2889,7 +2963,6 @@ public class Editor { } if (menu.hasVisibleItems() || mode.getCustomView() != null) { - getSelectionController().show(); mTextView.setHasTransientState(true); return true; } else { @@ -3227,6 +3300,8 @@ public class Editor { private Runnable mActionPopupShower; // Minimum touch target size for handles private int mMinSize; + // Indicates the line of text that the handle is on. + protected int mLine = -1; public HandleView(Drawable drawableLtr, Drawable drawableRtl) { super(mTextView.getContext()); @@ -3402,6 +3477,7 @@ public class Editor { addPositionToTouchUpFilter(offset); } final int line = layout.getLineForOffset(offset); + mLine = line; mPositionX = (int) (layout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX - getHorizontalOffset() + getCursorOffset()); @@ -3451,6 +3527,30 @@ public class Editor { } } + public void showAtLocation(int offset) { + // TODO - investigate if there's a better way to show the handles + // after the drag accelerator has occured. + int[] tmpCords = new int[2]; + mTextView.getLocationInWindow(tmpCords); + + Layout layout = mTextView.getLayout(); + int posX = tmpCords[0]; + int posY = tmpCords[1]; + + final int line = layout.getLineForOffset(offset); + + int startX = (int) (layout.getPrimaryHorizontal(offset) - 0.5f + - mHotspotX - getHorizontalOffset() + getCursorOffset()); + int startY = layout.getLineBottom(line); + + // Take TextView's padding and scroll into account. + startX += mTextView.viewportToContentHorizontalOffset(); + startY += mTextView.viewportToContentVerticalOffset(); + + mContainer.showAtLocation(mTextView, Gravity.NO_GRAVITY, + startX + posX, startY + posY); + } + @Override protected void onDraw(Canvas c) { final int drawWidth = mDrawable.getIntrinsicWidth(); @@ -3689,6 +3789,12 @@ public class Editor { } private class SelectionStartHandleView extends HandleView { + // The previous offset this handle was at. + private int mPrevOffset; + // Indicates whether the cursor is making adjustments within a word. + private boolean mInWord = false; + // Offset to track difference between touch and word boundary. + protected int mTouchWordOffset; public SelectionStartHandleView(Drawable drawableLtr, Drawable drawableRtl) { super(drawableLtr, drawableRtl); @@ -3696,11 +3802,7 @@ public class Editor { @Override protected int getHotspotX(Drawable drawable, boolean isRtlRun) { - if (isRtlRun) { - return drawable.getIntrinsicWidth() / 4; - } else { - return (drawable.getIntrinsicWidth() * 3) / 4; - } + return isRtlRun ? 0 : drawable.getIntrinsicWidth(); } @Override @@ -3722,21 +3824,81 @@ public class Editor { @Override public void updatePosition(float x, float y) { - int offset = mTextView.getOffsetForPosition(x, y); + final int trueOffset = mTextView.getOffsetForPosition(x, y); + final int currLine = mTextView.getLineAtCoordinate(y); + int offset = trueOffset; + boolean positionCursor = false; - // Handles can not cross and selection is at least one character - final int selectionEnd = mTextView.getSelectionEnd(); - if (offset >= selectionEnd) offset = Math.max(0, selectionEnd - 1); + int end = getWordEnd(offset, true); + int start = getWordStart(offset); - positionAtCursorOffset(offset, false); + if (offset < mPrevOffset) { + // User is increasing the selection. + if (!mInWord || currLine < mLine) { + // We're not in a word, or we're on a different line so we'll expand by + // word. First ensure the user has at least entered the next word. + int offsetToWord = Math.min((end - start) / 2, 2); + if (offset <= end - offsetToWord || currLine < mLine) { + offset = start; + } else { + offset = mPrevOffset; + } + } + mPrevOffset = offset; + mTouchWordOffset = trueOffset - offset; + mInWord = !isStartBoundary(offset); + positionCursor = true; + } else if (offset - mTouchWordOffset > mPrevOffset) { + // User is shrinking the selection. + if (currLine > mLine) { + // We're on a different line, so we'll snap to word boundaries. + offset = end; + } + offset -= mTouchWordOffset; + mPrevOffset = offset; + mInWord = !isEndBoundary(offset); + positionCursor = true; + } + + // Handles can not cross and selection is at least one character. + if (positionCursor) { + final int selectionEnd = mTextView.getSelectionEnd(); + if (offset >= selectionEnd) { + // We can't cross the handles so let's just constrain the Y value. + int alteredOffset = mTextView.getOffsetAtCoordinate(mLine, x); + if (alteredOffset >= selectionEnd) { + // Can't pass the other drag handle. + offset = Math.max(0, selectionEnd - 1); + } else { + offset = alteredOffset; + } + } + positionAtCursorOffset(offset, false); + } } public ActionPopupWindow getActionPopupWindow() { return mActionPopupWindow; } + + @Override + public boolean onTouchEvent(MotionEvent event) { + boolean superResult = super.onTouchEvent(event); + if (event.getActionMasked() == MotionEvent.ACTION_UP) { + // Reset the touch word offset when the user has lifted their finger. + mTouchWordOffset = 0; + } + return superResult; + } } private class SelectionEndHandleView extends HandleView { + // The previous offset this handle was at. + private int mPrevOffset; + // Indicates whether the cursor is making adjustments within a word. + private boolean mInWord = false; + // Offset to track difference between touch and word boundary. + protected int mTouchWordOffset; public SelectionEndHandleView(Drawable drawableLtr, Drawable drawableRtl) { super(drawableLtr, drawableRtl); @@ -3744,11 +3906,7 @@ public class Editor { @Override protected int getHotspotX(Drawable drawable, boolean isRtlRun) { - if (isRtlRun) { - return (drawable.getIntrinsicWidth() * 3) / 4; - } else { - return drawable.getIntrinsicWidth() / 4; - } + return isRtlRun ? drawable.getIntrinsicWidth() : 0; } @Override @@ -3770,20 +3928,72 @@ public class Editor { @Override public void updatePosition(float x, float y) { - int offset = mTextView.getOffsetForPosition(x, y); + final int trueOffset = mTextView.getOffsetForPosition(x, y); + final int currLine = mTextView.getLineAtCoordinate(y); + int offset = trueOffset; + boolean positionCursor = false; - // Handles can not cross and selection is at least one character - final int selectionStart = mTextView.getSelectionStart(); - if (offset <= selectionStart) { - offset = Math.min(selectionStart + 1, mTextView.getText().length()); + int end = getWordEnd(offset, true); + int start = getWordStart(offset); + + if (offset > mPrevOffset) { + // User is increasing the selection. + if (!mInWord || currLine > mLine) { + // We're not in a word, or we're on a different line so we'll expand by + // word. First ensure the user has at least entered the next word. + int midPoint = Math.min((end - start) / 2, 2); + if (offset >= start + midPoint || currLine > mLine) { + offset = end; + } else { + offset = mPrevOffset; + } + } + mPrevOffset = offset; + mTouchWordOffset = offset - trueOffset; + mInWord = !isEndBoundary(offset); + positionCursor = true; + } else if (offset + mTouchWordOffset < mPrevOffset) { + // User is shrinking the selection. + if (currLine > mLine) { + // We're on a different line, so we'll snap to word boundaries. + offset = getWordStart(offset); + } + offset += mTouchWordOffset; + mPrevOffset = offset; + positionCursor = true; + mInWord = !isStartBoundary(offset); } - positionAtCursorOffset(offset, false); + if (positionCursor) { + final int selectionStart = mTextView.getSelectionStart(); + if (offset <= selectionStart) { + // We can't cross the handles so let's just constrain the Y value. + int alteredOffset = mTextView.getOffsetAtCoordinate(mLine, x); + int length = mTextView.getText().length(); + if (alteredOffset <= selectionStart) { + // Can't pass the other drag handle. + offset = Math.min(selectionStart + 1, length); + } else { + offset = Math.min(alteredOffset, length); + } + } + positionAtCursorOffset(offset, false); + } } public void setActionPopupWindow(ActionPopupWindow actionPopupWindow) { mActionPopupWindow = actionPopupWindow; } + + @Override + public boolean onTouchEvent(MotionEvent event) { + boolean superResult = super.onTouchEvent(event); + if (event.getActionMasked() == MotionEvent.ACTION_UP) { + // Reset the touch word offset when the user has lifted their finger. + mTouchWordOffset = 0; + } + return superResult; + } } /** @@ -3866,6 +4076,11 @@ public class Editor { private float mDownPositionX, mDownPositionY; private boolean mGestureStayedInTapRegion; + // Where the user first starts the drag motion. + private int mStartOffset = -1; + // Indicates whether the user is selecting text and using the drag accelerator. + private boolean mDragAcceleratorActive; + SelectionModifierCursorController() { resetTouchOffsets(); } @@ -3915,6 +4130,22 @@ public class Editor { if (mEndHandle != null) mEndHandle.hide(); } + public void enterDrag() { + // Just need to init the handles / hide insertion cursor. + show(); + mDragAcceleratorActive = true; + // Start location of selection. + mStartOffset = mTextView.getOffsetForPosition(mLastDownPositionX, + mLastDownPositionY); + // Don't show the handles until user has lifted finger. + hide(); + + // This stops scrolling parents from intercepting the touch event, allowing + // the user to continue dragging across the screen to select text; TextView will + // scroll as necessary. + mTextView.getParent().requestDisallowInterceptTouchEvent(true); + } + public void onTouchEvent(MotionEvent event) { // This is done even when the View does not have focus, so that long presses can start // selection and tap can move cursor from this tap position. @@ -3923,7 +4154,7 @@ public class Editor { final float x = event.getX(); final float y = event.getY(); - // Remember finger down position, to be able to start selection from there + // Remember finger down position, to be able to start selection from there. mMinTouchOffset = mMaxTouchOffset = mTextView.getOffsetForPosition(x, y); // Double tap detection @@ -3962,23 +4193,112 @@ public class Editor { break; case MotionEvent.ACTION_MOVE: + final ViewConfiguration viewConfiguration = ViewConfiguration.get( + mTextView.getContext()); + if (mGestureStayedInTapRegion) { final float deltaX = event.getX() - mDownPositionX; final float deltaY = event.getY() - mDownPositionY; final float distanceSquared = deltaX * deltaX + deltaY * deltaY; - final ViewConfiguration viewConfiguration = ViewConfiguration.get( - mTextView.getContext()); int doubleTapTouchSlop = viewConfiguration.getScaledDoubleTapTouchSlop(); if (distanceSquared > doubleTapTouchSlop * doubleTapTouchSlop) { mGestureStayedInTapRegion = false; } } + + if (mStartHandle != null && mStartHandle.isShowing()) { + // Don't do the drag if the handles are showing already. + break; + } + + if (mStartOffset != -1) { + final int rawOffset = mTextView.getOffsetForPosition(event.getX(), + event.getY()); + int offset = rawOffset; + + // We don't start "dragging" until the user is past the initial word that + // gets selected on long press. + int firstWordStart = getWordStart(mStartOffset); + int firstWordEnd = getWordEnd(mStartOffset, false); + if (offset > firstWordEnd || offset < firstWordStart) { + + // Basically the goal in the below code is to have the highlight be + // offset so that your finger isn't covering the end point. + int fingerOffset = viewConfiguration.getScaledTouchSlop(); + float mx = event.getX(); + float my = event.getY(); + if (mx > fingerOffset) mx -= fingerOffset; + if (my > fingerOffset) my -= fingerOffset; + offset = mTextView.getOffsetForPosition(mx, my); + + // Perform the check for closeness at edge of view, if we're very close + // don't adjust the offset to be in front of the finger - otherwise the + // user can't select words at the edge. + if (mTextView.getWidth() - fingerOffset > mx) { + // We're going by word, so we need to make sure that the offset + // that we get is within this, so we'll get the previous boundary. + final WordIterator wordIterator = getWordIteratorWithText(); + + final int precedingOffset = wordIterator.preceding(offset); + if (mStartOffset < offset) { + // Expanding with bottom handle, in this case the selection end + // is before the finger. + offset = Math.max(precedingOffset - 1, 0); + } else { + // Expand with the start handle, in this case the selection + // start is before the finger. + if (precedingOffset == WordIterator.DONE) { + offset = 0; + } else { + offset = wordIterator.preceding(precedingOffset); + } + } + } + if (offset == WordIterator.DONE) + offset = rawOffset; + + // Need to adjust start offset based on direction of movement. + int newStart = mStartOffset < offset ? getWordStart(mStartOffset) + : getWordEnd(mStartOffset, true); + Selection.setSelection((Spannable) mTextView.getText(), newStart, + offset); + } + } break; case MotionEvent.ACTION_UP: mPreviousTapUpTime = SystemClock.uptimeMillis(); + if (mDragAcceleratorActive) { + // No longer dragging to select text, let the parent intercept events. + mTextView.getParent().requestDisallowInterceptTouchEvent(false); + + show(); + int startOffset = mTextView.getSelectionStart(); + int endOffset = mTextView.getSelectionEnd(); + + // Since we don't let drag handles pass once they're visible, we need to + // make sure the start / end locations are correct because the user *can* + // switch directions during the initial drag. + if (endOffset < startOffset) { + int tmp = endOffset; + endOffset = startOffset; + startOffset = tmp; + + // Also update the selection with the right offsets in this case. + Selection.setSelection((Spannable) mTextView.getText(), + startOffset, endOffset); + } + + // Need to do this to display the handles. + mStartHandle.showAtLocation(startOffset); + mEndHandle.showAtLocation(endOffset); + + // No longer the first dragging motion, reset. + mDragAcceleratorActive = false; + mStartOffset = -1; + } break; } } @@ -4005,6 +4325,8 @@ public class Editor { public void resetTouchOffsets() { mMinTouchOffset = mMaxTouchOffset = -1; + mStartOffset = -1; + mDragAcceleratorActive = false; } /** @@ -4014,6 +4336,13 @@ public class Editor { return mStartHandle != null && mStartHandle.isDragging(); } + /** + * @return true if the user is selecting text using the drag accelerator. + */ + public boolean isDragAcceleratorActive() { + return mDragAcceleratorActive; + } + public void onTouchModeChanged(boolean isInTouchMode) { if (!isInTouchMode) { hide(); diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 2d0a9cbb9d6dd..d94ee89d35900 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -8093,7 +8093,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener public boolean onTouchEvent(MotionEvent event) { final int action = event.getActionMasked(); - if (mEditor != null) mEditor.onTouchEvent(event); + if (mEditor != null) { + mEditor.onTouchEvent(event); + + if (mEditor.mSelectionModifierCursorController != null && + mEditor.mSelectionModifierCursorController.isDragAcceleratorActive()) { + return true; + } + } final boolean superResult = super.onTouchEvent(event); @@ -9099,7 +9106,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return getLayout().getLineForVertical((int) y); } - private int getOffsetAtCoordinate(int line, float x) { + int getOffsetAtCoordinate(int line, float x) { x = convertToLocalHorizontalCoordinate(x); return getLayout().getOffsetForHorizontal(line, x); } diff --git a/core/res/res/drawable-hdpi/text_select_handle_left_mtrl_alpha.png b/core/res/res/drawable-hdpi/text_select_handle_left_mtrl_alpha.png index 9cdc25b4581dba8cf1d440f233ad79458e1ad24b..1550b442d6dfec67cfdf4a63506d7e81daea962e 100644 GIT binary patch literal 368 zcmV-$0gwKPP)aT z0Vj|V9Kj8U6QC2I6W|DJfE&OD^=b}-AEcL$7reR4DQLJD@ z0?Wi94O@y0#R41=SXQ1iCJQb64xn6l)?@dBXJg% zD3K4dwV=dWA!0)p2O)um=0CMx$5BXP|8~IDeyO<>o*Ny$;fuJh;fXwnq_&>ZwMaI! z`E@0depR+yBV!e7#eL0hn1}z6skogO2hL1Fk@L&@C`>!ar}#nK1Q-CPhz|i59u__T O0000T2qefINpU!KQvos*e*=z3A6l{P8Y1o7eWMK<(u+4U0 z7e<}IDT6xb<{$b3J_I4iT8piQUO0yLkRpG|Sc;9q3_L{;T9mdFyAok&k!R&Lrk7D( zAb8zEa38!1#G1IJ8{tAIHsIwo&d)TR~QgoBqggNH)gL6twyHFPV=ygrTl@TPsQNSu?WfP6#zJs8jKN2Qv2;e(Vd?OWp5^_B zV(Hjup8v3>rUA}FexCDuI3IIHr-={V!??Ylc^esb#)rN*jKjk}uQnYQT|qyehL*%s zKnu*m9ba+(mnDbZ^rw)7gvDp}uj@2@Nb7+qSNJ$~1ed(b9J<2Cv|(uDmtUmw4FG)L U{GPf85dZ)H07*qoM6N<$g0b%0j{pDw diff --git a/core/res/res/drawable-hdpi/text_select_handle_right_mtrl_alpha.png b/core/res/res/drawable-hdpi/text_select_handle_right_mtrl_alpha.png index 276d4809159289a7c65f44d9a6e0dd81b6f07984..b309dfdc02c4be325a04083444763771c3ee80f4 100644 GIT binary patch literal 349 zcmV-j0iyniP)!^P6X7UAVx7jZgr;js{-kSsgfMYkxq;{pq&`CBS$nEq9uqm8X}S%Q=%aT zH_#BQ?Ghl4OMuWzfQSM_fCvz_zh*3%5UD1XP>R%z+mX>t4TM-JNmnnrBmjUR>c#SK z2Dwm~IWu85Jm<{Ir|6$~1bFhoZD)|e0{}a|`PwA52?{W?BklkONGiRXWC*P@;0Uw- v1BO4IPPU=hN@P)4l((?8 zv5~wi$e8hay3f>PqqygJ&gIl+J@dWyo_kN{81uU^3fr&+n=A_(unrkmgEXXI6_SvE zIK*HCx}eSkU{F-S2YGnn3)c;`Q08KA#;4#?uW$&x5Oys%Z6Ubm0cIfLQgF&*aM7|$ z(JKPMIexi?|N0|c>KURg1Sf=o$r&_g2PXxC-(f&II4&4WuAovgcu6?;1zP`8Fs;aj zG=t}bgA4raR~w58!F!s)3kt!Pn!z!iSCYZkn!ytaclby%xF0@*gLgE8+l7N^`b=sD z)7ti4FqkfGr)F>!?_)^?-$IS{dO9lfrl#MRhLCn}HJk{oqnlmX`RL$1C|j{_Fb3tW z1k)dxIRF3v diff --git a/core/res/res/drawable-mdpi/text_select_handle_left_mtrl_alpha.png b/core/res/res/drawable-mdpi/text_select_handle_left_mtrl_alpha.png index 95c0168cdc36bb16e2050e9cb18e32f4400b55be..b36a41373aeff97cb789315599168f0a4ef6dba7 100644 GIT binary patch literal 267 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@H1|*Mc$*~4fk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5X)_S@)hE&{2N>O0`^Z)<-#tBklHnUh8LKp;oGaH^zU}W=R zyl{a@q3-{DriQZ@e*Ry{kddU(b%r}a{#`{4hTZ#`o)??=BJtUrAsRQHJzixiv?5QVXamE?Iw77t{95NHo@L-LVDOG%pT+$&$OPzc N22WQ%mvv4FO#t{!TQ&dy literal 346 zcmV-g0j2(lP)2%5q#;Np}YBC1J4MMNxLqM~Rn68s1?m>J>i z|G*Ji9PYj6c;GkRKbPmg@x1U8LkhbnqJkDW=;8)F^fAC4?(u+2&v=qJl7LUl;Rn#S zjN8H)z5+)5cWe3QE-;G_=gSjNANS^t@f|VFm-neYX*?;!`LamrvyZ7#VA~?_G{m+p z)3OL0D8)Gqi@=d$VACRStQc6c2z*ftEL#L#DaE-(D_8^`;zuFy1A7*MJ9twFbQjIG zMPM8y{6>g#2gU)`td50s)I&zjD=g7$w6bt*2Kj^1MTbf#tEdT%j07*qoM6N<$f;4`YrvLx| diff --git a/core/res/res/drawable-mdpi/text_select_handle_right_mtrl_alpha.png b/core/res/res/drawable-mdpi/text_select_handle_right_mtrl_alpha.png index 569332abfeaaf6e7e259a2d9fd68bd0df3065a09..afd0bd226b72ed94c756987d8c15e4a69469349e 100644 GIT binary patch literal 241 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@H1|*Mc$*~4fk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5XCVRR#hE&{2N?~aF|Np*hl79)u~3)RFPewIt89!J0%fkvj||0Pq~3@ zEL#U&2Mqjej6>us13M7{y%~-ypJX>;pf|;i)jQc%2=tz?ViDL<3Y=iwBCx3x=)dl& zMc|cE;6nbP>eEmRe76X^Pz?OA2s~B{e6a|u$P2C!cxn+?#!w;d3tJX}1=(bx1`bfP z2uxs84*sxl-#9?Z>O4-!fj?)Qlkofc9v&_YiyD_}?J-kei z10=s6x(0#*kzKq@k*~_%w!*-!q+eA9 zS;GDb26iev>(qmRYKV$Ij!DIu+(8hcVYUG>;%ON^b7Ah{1;|W-*ywupc$oC(6iOPP0*Q-NC;1P;> zdGzvAG>;*6_M7v(&w0*sM#M&p1odDZSPa&J z&0q)E1$KiLum`k)edHiHOxnOv&;gEv6QGlv1j{97F%B#PXTU@70=x$WPy#7P_*#x$ zEAP{vd=`Hv{%(;{-N$gy$PBvy-bJ#i#q=koJP4cw)_`tiT2ez{xf9p`+Ck3%i>pxJ z^U-CD0N23h8ZXm732gV8c^5Lux@v((yveI)`S3Agc@=?&yzRE@Seld#2-M%O*OS2M z;I;vAX;cU9_abi;PYRz53GDMEa2B|4Wa*~SiKf+qygHs+Q)47NQ=5+|toh2u1orU{ zYd&U!Ck8W6%b1-W1U4JnJmNBDs|SI)(|t22@T->$JR7yA{tNtzxN6|s`zyA25O~w5 zJUxkQ_aN}DQGs8;77qfuj4ojL#+(gTM}hrx4{8SmZ(A1|tpB>LU6n znBzg@LPjxXA9!uypAGtH4l?0*6Sr+U&}xKd56Q zr@=6B9$LIU%V6nVmM|INK6Lpk0QY#Hul2IkQ;^Q$Op_diN}S7PYi|v+?zx0noCe8Z z=reJK{Fo6e_G8xR^G?S|*5_r}{!Zcq&;-u2*e?8oIBiiq00+QKo`MEVOa@E9F>njK zk#HSaidP$^XB%@`JMja3Mjm*l866TmMvi#`=*ZKaOgnwyR h&7FO!gvr-T<2y|(8@#<}Ci4IQ002ovPDHLkV1gD8WLf|K diff --git a/core/res/res/drawable-xhdpi/text_select_handle_right_mtrl_alpha.png b/core/res/res/drawable-xhdpi/text_select_handle_right_mtrl_alpha.png index d3602d9888ec5b8fcf1061180588fbbbb7ff0fbe..42a893da515bc99e2930a62997f11fbe63f87ecf 100644 GIT binary patch literal 466 zcmV;@0WJQCP)ysQ3JyEa z3QjxF4lF@B&vjcCI{GV*h9Z}@)Pr@V8GZZi-kE03U3Z z*bxBlEwFyLgbeHF$rZ30bB0=FWlMfrIJSXiIkD4vvEQI$OtHy=YGWd^KbK}VOin^+ zWANgxBX!hyeXYLX_@T>tA@uoSSOdN87*_u>czY^HdbRLZ2NBq`dI#t*&i!0%D|OE^ z|04&UwUEn+a07*qo IM6N<$g5>hVsQ>@~ literal 752 zcmV-+A-&_LhII_c`aWQ=i_|cCkL4^E~Hwe&-b_eQW@o;Jk7U zoCO`A9h?RymE+10r4_V*X3zu$(X=AJYQFM5ZG+2g!5U*-f*r5 zfd`GWF@?MUOFRhNYov`S_A*)HB?G@06!=X$gmHm?2q#3BJqX-qP+tCg+~Yyueq$$* zICnh=+yg!v75LDDz)eQm7?$u&4+7U2eTk489t5reZw<<0Z`$EO;8O6?U^C4D>kbbB zXY2ICS_ZxYD?JFT0vDvbSDU}WGcUsgj*K>f4~FD%I>a>-H|ro=w|H(y;2RwzRCp1{ ze{>i4WTgMQ2BwR%;Pd8zf$20`s^#J^CW6fp)-951X$B=+^v@KhF$r9hux^pW{EtUB zUTIDc$HC2j>j{+dYi;UN5w#L_Rfft?&KzVs0xR?}lA7S64|i4i{$Vx_>dSU0ytFo)4@fe$FuKGJsZ5Mn&Msn0000)Mc@<6G$HNQSD_Q^zlve?OJ_GQf!SXJEKprq|I)Kt3 zH~~)Un)J%bEePQm4{O^X6avAaFt;TXN`XKKV-5#v<>r7eadm zk(<#d%|Vb%9x-idt6Uif^akn?o%1jWFz2Vp+x*%9?YH1yc z2O+Usnm{}$h(w`y5Zp|CK|>>R^{F5fgi1ju2nC@a6hvMSOU00Y z*eeFu5Qs130DBFML@NTZQ4Sayj^pgrE0P>R`Vb6Zht9D30kIGaVsU7wh=|}^u{!Q7 z--U+7;ui=!5zz%Z8d zkLX#vD%~K2$*E;HobU?D69Y>_qGQ^R00WFDomDN-fPdutAQ%RDMPyN4FwW)do+W;{ zX8{Z2t+DpG@-Q8_}W=H;UW;JYVuIyri)gS zcR0>M<12N8he{Ayo4unhYZDqr2glVp?iz0*dfSIyfJ58H|D!EGM_wP%fY8=A0`OTo z04IP1Xnh`R)sye@v*VS@xkcze@;?$GB#$!sknaJuC1-!S6Spjy{EF&ZfB^tr=;NIJ Say2jj0000x}*Pp3fyNZ+t3oE5Oe% z#2AOkvWpZ@02*WfA3+>grVsE*0MI7>=-QPK4}dZv%D_V$;yBMUM%aPrN(SK3Sn{#i z#8zBoXUsz)fX$s}BL?h|WrA_07^MWnh;=6X{~}ggAHNel&pbTCYfTAY|kRrB{EvTFYp$}c#gCo*pM(vz!QAHBm-)WWliMB0O$CeJw#eO zt=c`!E;S2#A`Q!E0Pp2Pj;-*?=757J0FUwxM@bsTRo>k602i6!2sy0=vYO(uozNY%PD|oZbLq4y)YJ|>)*WX_ zu2OLaT;+GmOIk4JvH(zF+G>Hv=OT5N*;2^YR)Hlqskp66*EvhW8g7+K+;_R8;U8vL zvgD}+3BFRxmDGV7e8;p}?6VFF{zT0~>j0MA;3q3~>@&@E%O%5AWPo3g7G59en5>Rd zzmR)8%b?Leb&>OYtagm16L^Ut!<}|f6VhUi)BLEzbo89--p6MrTa8t?mN?I+))SRA z>v zB8MPGw~wydP1n7HHV@RGCMZ%&+bAH)qMSjcshF#Aec@Nzfu-nHme+rA&n?fsW*0E%_H>)6KwGczPZH O0000JP)H@rA1Q{2d2kNf_d(Np_ zki;IfRasj>EYemGi4bdv6+8E0O$9|Qk%CwYMGZ|&b82cDX4I9mWo|u*e!a~rhC!rw z&Ha|1zppfvaakW)VrA6bgL9a3M9r5D8#kuQ?KM+ku}CcHwE6!0^~T=KTSLo1d+o9^ z+aZ?{{rWK@Dkz}PX*&J+>y5peS*)aJz*<|4sH&LK6n7u+)(&t|D|E(E((eK5Z8T^` z(_FU!Z%GOGTJ)mDsy11r?7Zo2ThdRc+;Xwr?Wii&7&hRv)7=ET*@%T107KcJhurUk z@iZS>?BYG3SJiHBTGBnhaW+bN-5VbEj?LWy+|C@hplGL8J<=^px{&$8F6gzx3m)u- z&~n<+0KQzb#FKt?Aq~YYCX|()0CP;)de0b2<6TLC&t&e!S2MHc4CM`62!_idPkYLO ze1W5Yz$I0WTb^`TCj*{U4dTOBY`}KwlK#cXfaip@jh?kAPvE5{0xoLUkhBbx47eE> zH?rDp3-gS$!X_U@6>E~IMzR4<1c|h+-<;N2kzZ)pr!O#Y*%p`co8f*Z1A8$8M)CtL z2FInQ`u~WuuEy`n^7G-E)q!zim1IKs--@)Zlmk}e2R!6taO7B&A8;c;RqLN~JeVhN zD==`ZZ6ZJ5!vm$)Uz_ z6MOxZ#|qPGM-6mKfI8%T7c<5^;`ua+!s+;4!vezwGxX%sKCv(VBM(>Q&v|@JuPruX+##dBa4PMC|6zcy98J7W)h5F>SY?H>J{7S@ zS*#%zsjHiI-1kNub=Ji!OTS@K_k~KTRvNb4GE4O->D4rA)_IdonJ{5mjjY4RzW{r9 VK~u{Msc8TJ002ovPDHLkV1gvREKdLc diff --git a/core/res/res/drawable-xxxhdpi/text_select_handle_left_mtrl_alpha.png b/core/res/res/drawable-xxxhdpi/text_select_handle_left_mtrl_alpha.png new file mode 100644 index 0000000000000000000000000000000000000000..643168f15b78d86ee6f3ed9ae0e673866aedd577 GIT binary patch literal 889 zcmV-<1BU#GP)e9v2WQh*AM| z1-OIY3cwYRbdXd4R3K9UrUHu!A}RnXh&xd3O`hPeC0nv?(ckXOjXX9s%s0#4dpGA( zU|OIwO=tEJ>NwM%0w&<00e&F{0w!Q8SFr$oB?qj7V`6|Q5x@yK!1Ou_f?&fO#yZ$ACNQ$* zjWfWH#Npq)ZYQ@Q5C0|F8(__0LiVdi*TJF(GVz+ft8Kt^us^NbM#DHUu_PNWx_jg5 zKC5S+t;wgBHrA{sI}Xn3HrB+ob^C$o_N#PpZGE#weaUq&U0hpB7jCYD>0*EG&2_Nj zVpbH@^m`j_p@Y@MJPfHq<)H4}Y`-wt(}~CrFUYQx7I|>@0(ROxc6bN6wyZ7AWq9b_ z8WnOVT}YNYlFXeJx%Z%hov4^=&QKq7cfJp9DZmOjVIJ@3r*bj10Xun2$nTR{sWP_G z-ZcHtp+mkY3%ArHS1|EinqeMXC$Vr-$&|X+45B-V@3u=2yrVU@&3$N3z8HV=!IbpXyUg**mu#UKON7&3s3 zAp^L$x_PP#0H0S~+#Xx?vCt?kU4;B$C17N?BL06c>O z4z8YHB>_pWP&fi^FT41dNNrYmx*!heuer zyGv|80GHLI?TYlad(hgqGzu3F`+#5J5=7(V75FaspUr0T9&N88N5%C$S^DlDi+F+0 zdA0BUx?mcct0|AN=al|$-k*rh$`n_@1g zC~_&YIa2Bw4gcaH>~d+C)p^Qgy;Gd5U6uMU%sl2W$a=vsz#0*Mqis)J^jSJrb;@B#!G--?1 zO>^{~*1@j|kUKJ_HHSM2arYN%&<`>eH3K{Kb4q