diff options
4 files changed, 358 insertions, 5 deletions
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index 513e72f27901..2732b2e0285a 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -387,7 +387,7 @@ public class Editor { // Specifies whether the cursor control feature set is enabled. // This can only be true if the text view is editable. - private final boolean mCursorControlEnabled; + private boolean mCursorControlEnabled; Editor(TextView textView) { mTextView = textView; @@ -411,6 +411,16 @@ public class Editor { } } + @VisibleForTesting + public void setCursorControlEnabled(boolean enabled) { + mCursorControlEnabled = enabled; + } + + @VisibleForTesting + public boolean getCursorControlEnabled() { + return mCursorControlEnabled; + } + ParcelableParcel saveInstanceState() { ParcelableParcel state = new ParcelableParcel(getClass().getClassLoader()); Parcel parcel = state.getParcel(); @@ -1204,7 +1214,7 @@ public class Editor { } // Long press in empty space moves cursor and starts the insertion action mode. if (!handled && !isPositionOnText(mTouchState.getLastDownX(), mTouchState.getLastDownY()) - && mInsertionControllerEnabled) { + && !mTouchState.isOnHandle() && mInsertionControllerEnabled) { final int offset = mTextView.getOffsetForPosition(mTouchState.getLastDownX(), mTouchState.getLastDownY()); Selection.setSelection((Spannable) mTextView.getText(), offset); @@ -5135,6 +5145,37 @@ public class Editor { private float mLastDownRawX, mLastDownRawY; private Runnable mHider; + // Members for fake-dismiss effect in touch through mode. + // It is to make InsertionHandleView can receive the MOVE/UP events after calling dismiss(), + // which could happen in case of long-press (making selection will dismiss the insertion + // handle). + + // Whether the finger is down and hasn't been up yet. + private boolean mIsTouchDown = false; + // Whether the popup window is in the invisible state and will be dismissed when finger up. + private boolean mPendingDismissOnUp = false; + // The alpha value of the drawable. + private final int mDrawableOpacity = 255; + + // Members for toggling the insertion menu in touch through mode. + + // The coordinate for the touch down event, which is used for transforming the coordinates + // of the events to the text view. + private float mTouchDownX; + private float mTouchDownY; + // The cursor offset when touch down. This is to detect whether the cursor is moved when + // finger move/up. + private int mOffsetDown; + // Whether the cursor offset has been changed on the move/up events. + private boolean mOffsetChanged; + // Whether it is in insertion action mode when finger down. + private boolean mIsInActionMode; + // The timestamp for the last up event, which is used for double tap detection. + private long mLastUpTime; + // The text height of the font of the text view, which is used to calculate the Y coordinate + // of the touch through events. + private float mTextHeight; + public InsertionHandleView(Drawable drawable) { super(drawable, drawable, com.android.internal.R.id.insertion_handle); } @@ -5190,6 +5231,11 @@ public class Editor { @Override public boolean onTouchEvent(MotionEvent ev) { + if (mCursorControlEnabled && FLAG_ENABLE_CURSOR_DRAG) { + // Should only enable touch through when cursor drag is enabled. + // Otherwise the insertion handle view cannot be moved. + return touchThrough(ev); + } final boolean result = super.onTouchEvent(ev); switch (ev.getActionMasked()) { @@ -5235,6 +5281,115 @@ public class Editor { return result; } + // Handles the touch events in touch through mode. + private boolean touchThrough(MotionEvent ev) { + final int actionType = ev.getActionMasked(); + switch (actionType) { + case MotionEvent.ACTION_DOWN: + mIsTouchDown = true; + mOffsetChanged = false; + mOffsetDown = mTextView.getSelectionStart(); + mTouchDownX = ev.getX(); + mTouchDownY = ev.getY(); + mIsInActionMode = mTextActionMode != null; + if (ev.getEventTime() - mLastUpTime < ViewConfiguration.getDoubleTapTimeout()) { + stopTextActionMode(); // Avoid crash when double tap and drag backwards. + } + final Paint.FontMetrics fontMetrics = mTextView.getPaint().getFontMetrics(); + mTextHeight = fontMetrics.descent - fontMetrics.ascent; + mTouchState.setIsOnHandle(true); + break; + case MotionEvent.ACTION_UP: + mLastUpTime = ev.getEventTime(); + break; + } + // Performs the touch through by forward the events to the text view. + boolean ret = mTextView.onTouchEvent(transformEventForTouchThrough(ev)); + + if (actionType == MotionEvent.ACTION_UP || actionType == MotionEvent.ACTION_CANCEL) { + mIsTouchDown = false; + if (mPendingDismissOnUp) { + dismiss(); + } + mTouchState.setIsOnHandle(false); + } + + // Checks for cursor offset change. + if (!mOffsetChanged) { + int start = mTextView.getSelectionStart(); + int end = mTextView.getSelectionEnd(); + if (start != end || mOffsetDown != start) { + mOffsetChanged = true; + } + } + + // Toggling the insertion action mode on finger up. + if (!mOffsetChanged && actionType == MotionEvent.ACTION_UP) { + if (mIsInActionMode) { + stopTextActionMode(); + } else { + startInsertionActionMode(); + } + } + return ret; + } + + private MotionEvent transformEventForTouchThrough(MotionEvent ev) { + // Transforms the touch events to screen coordinates. + // And also shift up to make the hit point is on the text. + // Note: + // - The revised X should reflect the distance to the horizontal center of touch down. + // - The revised Y should be at the top of the text. + Matrix m = new Matrix(); + m.setTranslate(ev.getRawX() - ev.getX() + (getMeasuredWidth() >> 1) - mTouchDownX, + ev.getRawY() - ev.getY() - mTouchDownY - mTextHeight); + ev.transform(m); + // Transforms the touch events to text view coordinates. + mTextView.toLocalMotionEvent(ev); + if (TextView.DEBUG_CURSOR) { + logCursor("InsertionHandleView#transformEventForTouchThrough", + "Touch through: %d, (%f, %f)", + ev.getAction(), ev.getX(), ev.getY()); + } + return ev; + } + + @Override + public boolean isShowing() { + if (mPendingDismissOnUp) { + return false; + } + return super.isShowing(); + } + + @Override + public void show() { + super.show(); + mPendingDismissOnUp = false; + mDrawable.setAlpha(mDrawableOpacity); + } + + @Override + public void dismiss() { + if (mIsTouchDown) { + if (TextView.DEBUG_CURSOR) { + logCursor("InsertionHandleView#dismiss", + "Suppressed the real dismiss, only become invisible"); + } + mPendingDismissOnUp = true; + mDrawable.setAlpha(0); + } else { + super.dismiss(); + mPendingDismissOnUp = false; + } + } + + @Override + protected void updateDrawable(final boolean updateDrawableWhenDragging) { + super.updateDrawable(updateDrawableWhenDragging); + mDrawable.setAlpha(mDrawableOpacity); + } + @Override public int getCurrentCursorOffset() { return mTextView.getSelectionStart(); @@ -6039,8 +6194,8 @@ public class Editor { eventX, eventY); // Double tap detection - if (mTouchState.isMultiTapInSameArea() - && (isMouse || isPositionOnText(eventX, eventY))) { + if (mTouchState.isMultiTapInSameArea() && (isMouse + || mTouchState.isOnHandle() || isPositionOnText(eventX, eventY))) { if (TextView.DEBUG_CURSOR) { logCursor("SelectionModifierCursorController: onTouchEvent", "ACTION_DOWN: select and start drag"); diff --git a/core/java/android/widget/EditorTouchState.java b/core/java/android/widget/EditorTouchState.java index b13ca4210612..ff3ac0732aa2 100644 --- a/core/java/android/widget/EditorTouchState.java +++ b/core/java/android/widget/EditorTouchState.java @@ -42,6 +42,7 @@ public class EditorTouchState { private long mLastDownMillis; private float mLastUpX, mLastUpY; private long mLastUpMillis; + private boolean mIsOnHandle; @IntDef({MultiTapStatus.NONE, MultiTapStatus.FIRST_TAP, MultiTapStatus.DOUBLE_TAP, MultiTapStatus.TRIPLE_CLICK}) @@ -98,7 +99,15 @@ public class EditorTouchState { } public boolean isDragCloseToVertical() { - return mIsDragCloseToVertical; + return mIsDragCloseToVertical && !mIsOnHandle; + } + + public void setIsOnHandle(boolean onHandle) { + mIsOnHandle = onHandle; + } + + public boolean isOnHandle() { + return mIsOnHandle; } /** diff --git a/core/tests/coretests/src/android/widget/TextViewActivityTest.java b/core/tests/coretests/src/android/widget/TextViewActivityTest.java index fbe4c1a10fe1..0c38e7136655 100644 --- a/core/tests/coretests/src/android/widget/TextViewActivityTest.java +++ b/core/tests/coretests/src/android/widget/TextViewActivityTest.java @@ -27,9 +27,13 @@ import static android.widget.espresso.FloatingToolbarEspressoUtils.sleepForFloat import static android.widget.espresso.TextViewActions.Handle; import static android.widget.espresso.TextViewActions.clickOnTextAtIndex; import static android.widget.espresso.TextViewActions.doubleClickOnTextAtIndex; +import static android.widget.espresso.TextViewActions.doubleTapAndDragHandle; import static android.widget.espresso.TextViewActions.doubleTapAndDragOnText; +import static android.widget.espresso.TextViewActions.doubleTapHandle; import static android.widget.espresso.TextViewActions.dragHandle; import static android.widget.espresso.TextViewActions.longPressAndDragOnText; +import static android.widget.espresso.TextViewActions.longPressAndDragHandle; +import static android.widget.espresso.TextViewActions.longPressHandle; import static android.widget.espresso.TextViewActions.longPressOnTextAtIndex; import static android.widget.espresso.TextViewAssertions.doesNotHaveStyledText; import static android.widget.espresso.TextViewAssertions.hasInsertionPointerAtIndex; @@ -511,6 +515,111 @@ public class TextViewActivityTest { } @Test + public void testInsertionHandle_touchThrough() { + final TextView textView = mActivity.findViewById(R.id.textview); + boolean cursorControlEnabled = textView.getEditorForTesting().getCursorControlEnabled(); + boolean cursorDragEnabled = Editor.FLAG_ENABLE_CURSOR_DRAG; + textView.getEditorForTesting().setCursorControlEnabled(true); + Editor.FLAG_ENABLE_CURSOR_DRAG = true; + + testInsertionHandle(); + testInsertionHandle_multiLine(); + + textView.getEditorForTesting().setCursorControlEnabled(cursorControlEnabled); + Editor.FLAG_ENABLE_CURSOR_DRAG = cursorDragEnabled; + } + + @Test + public void testInsertionHandle_longPressToSelect() { + // This test only makes sense when Cursor Control flag is enabled. + final TextView textView = mActivity.findViewById(R.id.textview); + boolean cursorControlEnabled = textView.getEditorForTesting().getCursorControlEnabled(); + boolean cursorDragEnabled = Editor.FLAG_ENABLE_CURSOR_DRAG; + textView.getEditorForTesting().setCursorControlEnabled(true); + Editor.FLAG_ENABLE_CURSOR_DRAG = true; + + final String text = "hello the world"; + onView(withId(R.id.textview)).perform(replaceText(text)); + + onView(withId(R.id.textview)).perform(clickOnTextAtIndex(text.length())); + onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.length())); + + onHandleView(com.android.internal.R.id.insertion_handle).perform(longPressHandle(textView)); + onView(withId(R.id.textview)).check(hasSelection("world")); + + textView.getEditorForTesting().setCursorControlEnabled(cursorControlEnabled); + Editor.FLAG_ENABLE_CURSOR_DRAG = cursorDragEnabled; + } + + @Test + public void testInsertionHandle_longPressAndDragToSelect() { + // This test only makes sense when Cursor Control flag is enabled. + final TextView textView = mActivity.findViewById(R.id.textview); + boolean cursorControlEnabled = textView.getEditorForTesting().getCursorControlEnabled(); + boolean cursorDragEnabled = Editor.FLAG_ENABLE_CURSOR_DRAG; + textView.getEditorForTesting().setCursorControlEnabled(true); + Editor.FLAG_ENABLE_CURSOR_DRAG = true; + + final String text = "hello the world"; + onView(withId(R.id.textview)).perform(replaceText(text)); + + onView(withId(R.id.textview)).perform(clickOnTextAtIndex(text.length())); + onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.length())); + + onHandleView(com.android.internal.R.id.insertion_handle) + .perform(longPressAndDragHandle(textView, Handle.INSERTION, text.indexOf('t'))); + onView(withId(R.id.textview)).check(hasSelection("the world")); + + textView.getEditorForTesting().setCursorControlEnabled(cursorControlEnabled); + Editor.FLAG_ENABLE_CURSOR_DRAG = cursorDragEnabled; + } + + @Test + public void testInsertionHandle_doubleTapToSelect() { + // This test only makes sense when Cursor Control flag is enabled. + final TextView textView = mActivity.findViewById(R.id.textview); + boolean cursorControlEnabled = textView.getEditorForTesting().getCursorControlEnabled(); + boolean cursorDragEnabled = Editor.FLAG_ENABLE_CURSOR_DRAG; + textView.getEditorForTesting().setCursorControlEnabled(true); + Editor.FLAG_ENABLE_CURSOR_DRAG = true; + + final String text = "hello the world"; + onView(withId(R.id.textview)).perform(replaceText(text)); + + onView(withId(R.id.textview)).perform(clickOnTextAtIndex(text.length())); + onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.length())); + + onHandleView(com.android.internal.R.id.insertion_handle).perform(doubleTapHandle(textView)); + onView(withId(R.id.textview)).check(hasSelection("world")); + + textView.getEditorForTesting().setCursorControlEnabled(cursorControlEnabled); + Editor.FLAG_ENABLE_CURSOR_DRAG = cursorDragEnabled; + } + + @Test + public void testInsertionHandle_doubleTapAndDragToSelect() { + // This test only makes sense when Cursor Control flag is enabled. + final TextView textView = mActivity.findViewById(R.id.textview); + boolean cursorControlEnabled = textView.getEditorForTesting().getCursorControlEnabled(); + boolean cursorDragEnabled = Editor.FLAG_ENABLE_CURSOR_DRAG; + textView.getEditorForTesting().setCursorControlEnabled(true); + Editor.FLAG_ENABLE_CURSOR_DRAG = true; + + final String text = "hello the world"; + onView(withId(R.id.textview)).perform(replaceText(text)); + + onView(withId(R.id.textview)).perform(clickOnTextAtIndex(text.length())); + onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.length())); + + onHandleView(com.android.internal.R.id.insertion_handle) + .perform(doubleTapAndDragHandle(textView, Handle.INSERTION, text.indexOf('t'))); + onView(withId(R.id.textview)).check(hasSelection("the world")); + + textView.getEditorForTesting().setCursorControlEnabled(cursorControlEnabled); + Editor.FLAG_ENABLE_CURSOR_DRAG = cursorDragEnabled; + } + + @Test public void testSelectionHandles() { final String text = "abcd efg hijk lmn"; onView(withId(R.id.textview)).perform(replaceText(text)); diff --git a/core/tests/coretests/src/android/widget/espresso/TextViewActions.java b/core/tests/coretests/src/android/widget/espresso/TextViewActions.java index 4808a0b0656a..d4c9971613e5 100644 --- a/core/tests/coretests/src/android/widget/espresso/TextViewActions.java +++ b/core/tests/coretests/src/android/widget/espresso/TextViewActions.java @@ -199,6 +199,86 @@ public final class TextViewActions { } /** + * Returns an action that long presses then drags on handle from the current position to + * endIndex on the TextView.<br> + * <br> + * View constraints: + * <ul> + * <li>must be a TextView's drag-handle displayed on screen + * <ul> + * + * @param textView TextView the handle is on + * @param handleType Type of the handle + * @param endIndex The index of the TextView's text to end the drag at + */ + public static ViewAction longPressAndDragHandle(TextView textView, Handle handleType, + int endIndex) { + return actionWithAssertions( + new DragAction( + DragAction.Drag.LONG_PRESS, + new CurrentHandleCoordinates(textView), + new HandleCoordinates(textView, handleType, endIndex, true), + Press.FINGER, + Editor.HandleView.class)); + } + + /** + * Returns an action that long presses on the current handle.<br> + * <br> + * View constraints: + * <ul> + * <li>must be a TextView's drag-handle displayed on screen + * <ul> + * + * @param textView TextView the handle is on + */ + public static ViewAction longPressHandle(TextView textView) { + return actionWithAssertions( + new ViewClickAction(Tap.LONG, new CurrentHandleCoordinates(textView), + Press.FINGER)); + } + + /** + * Returns an action that double tap then drags on handle from the current position to + * endIndex on the TextView.<br> + * <br> + * View constraints: + * <ul> + * <li>must be a TextView's drag-handle displayed on screen + * <ul> + * + * @param textView TextView the handle is on + * @param handleType Type of the handle + * @param endIndex The index of the TextView's text to end the drag at + */ + public static ViewAction doubleTapAndDragHandle(TextView textView, Handle handleType, + int endIndex) { + return actionWithAssertions( + new DragAction( + DragAction.Drag.DOUBLE_TAP, + new CurrentHandleCoordinates(textView), + new HandleCoordinates(textView, handleType, endIndex, true), + Press.FINGER, + Editor.HandleView.class)); + } + + /** + * Returns an action that double tap on the current handle.<br> + * <br> + * View constraints: + * <ul> + * <li>must be a TextView's drag-handle displayed on screen + * <ul> + * + * @param textView TextView the handle is on + */ + public static ViewAction doubleTapHandle(TextView textView) { + return actionWithAssertions( + new ViewClickAction(Tap.DOUBLE, new CurrentHandleCoordinates(textView), + Press.FINGER)); + } + + /** * Returns an action that double taps then drags on text from startIndex to endIndex on the * TextView.<br> * <br> |