summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/widget/Editor.java163
-rw-r--r--core/java/android/widget/EditorTouchState.java11
-rw-r--r--core/tests/coretests/src/android/widget/TextViewActivityTest.java109
-rw-r--r--core/tests/coretests/src/android/widget/espresso/TextViewActions.java80
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>