diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index a0cf53437a500..20af76b0d5ca7 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -5921,8 +5921,6 @@ public class Editor { // The offsets of that last touch down event. Remembered to start selection there. private int mMinTouchOffset, mMaxTouchOffset; - private boolean mGestureStayedInTapRegion; - // Where the user first starts the drag motion. private int mStartOffset = -1; @@ -6029,8 +6027,7 @@ public class Editor { eventX, eventY); // Double tap detection - if (mGestureStayedInTapRegion - && mTouchState.isMultiTapInSameArea() + if (mTouchState.isMultiTapInSameArea() && (isMouse || isPositionOnText(eventX, eventY))) { if (TextView.DEBUG_CURSOR) { logCursor("SelectionModifierCursorController: onTouchEvent", @@ -6043,7 +6040,6 @@ public class Editor { } mDiscardNextActionUp = true; } - mGestureStayedInTapRegion = true; mHaventMovedEnoughToStartDrag = true; } break; @@ -6059,25 +6055,8 @@ public class Editor { break; case MotionEvent.ACTION_MOVE: - final ViewConfiguration viewConfig = ViewConfiguration.get( - mTextView.getContext()); - - if (mGestureStayedInTapRegion || mHaventMovedEnoughToStartDrag) { - final float deltaX = eventX - mTouchState.getLastDownX(); - final float deltaY = eventY - mTouchState.getLastDownY(); - final float distanceSquared = deltaX * deltaX + deltaY * deltaY; - - if (mGestureStayedInTapRegion) { - int doubleTapTouchSlop = viewConfig.getScaledDoubleTapTouchSlop(); - mGestureStayedInTapRegion = - distanceSquared <= doubleTapTouchSlop * doubleTapTouchSlop; - } - if (mHaventMovedEnoughToStartDrag) { - // We don't start dragging until the user has moved enough. - int touchSlop = viewConfig.getScaledTouchSlop(); - mHaventMovedEnoughToStartDrag = - distanceSquared <= touchSlop * touchSlop; - } + if (mHaventMovedEnoughToStartDrag) { + mHaventMovedEnoughToStartDrag = !mTouchState.isMovedEnoughForDrag(); } if (isMouse && !isDragAcceleratorActive()) { diff --git a/core/java/android/widget/EditorTouchState.java b/core/java/android/widget/EditorTouchState.java index d53099d44f6fc..6277afe2f613a 100644 --- a/core/java/android/widget/EditorTouchState.java +++ b/core/java/android/widget/EditorTouchState.java @@ -31,13 +31,15 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** - * Helper class used by {@link Editor} to track state for touch events. + * Helper class used by {@link Editor} to track state for touch events. Ideally the logic here + * should be replaced with {@link android.view.GestureDetector}. * * @hide */ @VisibleForTesting(visibility = PACKAGE) public class EditorTouchState { private float mLastDownX, mLastDownY; + private long mLastDownMillis; private float mLastUpX, mLastUpY; private long mLastUpMillis; @@ -106,9 +108,18 @@ public class EditorTouchState { final int action = event.getActionMasked(); if (action == MotionEvent.ACTION_DOWN) { final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE); + + // We check both the time between the last up and current down event, as well as the + // time between the first down and up events. The latter check is necessary to handle + // the case when the user taps, drags/holds for some time, and then lifts up and + // quickly taps in the same area. This scenario should not be treated as a double-tap. + // This follows the behavior in GestureDetector. final long millisSinceLastUp = event.getEventTime() - mLastUpMillis; + final long millisBetweenLastDownAndLastUp = mLastUpMillis - mLastDownMillis; + // Detect double tap and triple click. if (millisSinceLastUp <= ViewConfiguration.getDoubleTapTimeout() + && millisBetweenLastDownAndLastUp <= ViewConfiguration.getDoubleTapTimeout() && (mMultiTapStatus == MultiTapStatus.FIRST_TAP || (mMultiTapStatus == MultiTapStatus.DOUBLE_TAP && isMouse))) { if (mMultiTapStatus == MultiTapStatus.FIRST_TAP) { @@ -133,6 +144,7 @@ public class EditorTouchState { } mLastDownX = event.getX(); mLastDownY = event.getY(); + mLastDownMillis = event.getEventTime(); mMovedEnoughForDrag = false; mIsDragCloseToVertical = false; } else if (action == MotionEvent.ACTION_UP) { diff --git a/core/tests/coretests/src/android/widget/EditorCursorDragTest.java b/core/tests/coretests/src/android/widget/EditorCursorDragTest.java index f497db2256dd1..89c237498e5c3 100644 --- a/core/tests/coretests/src/android/widget/EditorCursorDragTest.java +++ b/core/tests/coretests/src/android/widget/EditorCursorDragTest.java @@ -225,6 +225,61 @@ public class EditorCursorDragTest { onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(initialCursorPosition)); } + @Test + public void testEditor_onTouchEvent_quickTapAfterDrag() throws Throwable { + String text = "Hi world!"; + onView(withId(R.id.textview)).perform(replaceText(text)); + onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(0)); + + TextView tv = mActivity.findViewById(R.id.textview); + Editor editor = tv.getEditorForTesting(); + + // Simulate a tap-and-drag gesture. + long event1Time = 1001; + MotionEvent event1 = downEvent(event1Time, event1Time, 5f, 10f); + mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event1)); + assertFalse(editor.getInsertionController().isCursorBeingModified()); + assertFalse(editor.getSelectionController().isCursorBeingModified()); + + long event2Time = 1002; + MotionEvent event2 = moveEvent(event1Time, event2Time, 50f, 10f); + mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event2)); + assertTrue(editor.getInsertionController().isCursorBeingModified()); + assertFalse(editor.getSelectionController().isCursorBeingModified()); + + long event3Time = 1003; + MotionEvent event3 = moveEvent(event1Time, event3Time, 100f, 10f); + mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event3)); + assertTrue(editor.getInsertionController().isCursorBeingModified()); + assertFalse(editor.getSelectionController().isCursorBeingModified()); + + long event4Time = 2004; + MotionEvent event4 = upEvent(event1Time, event4Time, 100f, 10f); + mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event4)); + assertFalse(editor.getInsertionController().isCursorBeingModified()); + assertFalse(editor.getSelectionController().isCursorBeingModified()); + + // Simulate a quick tap after the drag, near the location where the drag ended. + long event5Time = 2005; + MotionEvent event5 = downEvent(event5Time, event5Time, 90f, 10f); + mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event5)); + assertFalse(editor.getInsertionController().isCursorBeingModified()); + assertFalse(editor.getSelectionController().isCursorBeingModified()); + + long event6Time = 2006; + MotionEvent event6 = upEvent(event5Time, event6Time, 90f, 10f); + mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event6)); + assertFalse(editor.getInsertionController().isCursorBeingModified()); + assertFalse(editor.getSelectionController().isCursorBeingModified()); + + // Simulate another quick tap in the same location; now selection should be triggered. + long event7Time = 2007; + MotionEvent event7 = downEvent(event7Time, event7Time, 90f, 10f); + mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event7)); + assertFalse(editor.getInsertionController().isCursorBeingModified()); + assertTrue(editor.getSelectionController().isCursorBeingModified()); + } + @Test public void testEditor_onTouchEvent_cursorDrag() throws Throwable { String text = "testEditor_onTouchEvent_cursorDrag"; @@ -237,29 +292,25 @@ public class EditorCursorDragTest { // Simulate a tap-and-drag gesture. This should trigger a cursor drag. long event1Time = 1001; MotionEvent event1 = downEvent(event1Time, event1Time, 20f, 30f); - mActivity.runOnUiThread(() -> editor.onTouchEvent(event1)); - mInstrumentation.waitForIdleSync(); + mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event1)); assertFalse(editor.getInsertionController().isCursorBeingModified()); assertFalse(editor.getSelectionController().isCursorBeingModified()); long event2Time = 1002; MotionEvent event2 = moveEvent(event1Time, event2Time, 21f, 30f); - mActivity.runOnUiThread(() -> editor.onTouchEvent(event2)); - mInstrumentation.waitForIdleSync(); + mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event2)); assertFalse(editor.getInsertionController().isCursorBeingModified()); assertFalse(editor.getSelectionController().isCursorBeingModified()); long event3Time = 1003; - MotionEvent event3 = moveEvent(event3Time, event3Time, 120f, 30f); - mActivity.runOnUiThread(() -> editor.onTouchEvent(event3)); - mInstrumentation.waitForIdleSync(); + MotionEvent event3 = moveEvent(event1Time, event3Time, 120f, 30f); + mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event3)); assertTrue(editor.getInsertionController().isCursorBeingModified()); assertFalse(editor.getSelectionController().isCursorBeingModified()); long event4Time = 1004; - MotionEvent event4 = upEvent(event3Time, event4Time, 120f, 30f); - mActivity.runOnUiThread(() -> editor.onTouchEvent(event4)); - mInstrumentation.waitForIdleSync(); + MotionEvent event4 = upEvent(event1Time, event4Time, 120f, 30f); + mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event4)); assertFalse(editor.getInsertionController().isCursorBeingModified()); assertFalse(editor.getSelectionController().isCursorBeingModified()); } @@ -276,36 +327,31 @@ public class EditorCursorDragTest { // Simulate a double-tap followed by a drag. This should trigger a selection drag. long event1Time = 1001; MotionEvent event1 = downEvent(event1Time, event1Time, 20f, 30f); - mActivity.runOnUiThread(() -> editor.onTouchEvent(event1)); - mInstrumentation.waitForIdleSync(); + mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event1)); assertFalse(editor.getInsertionController().isCursorBeingModified()); assertFalse(editor.getSelectionController().isCursorBeingModified()); long event2Time = 1002; MotionEvent event2 = upEvent(event1Time, event2Time, 20f, 30f); - mActivity.runOnUiThread(() -> editor.onTouchEvent(event2)); - mInstrumentation.waitForIdleSync(); + mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event2)); assertFalse(editor.getInsertionController().isCursorBeingModified()); assertFalse(editor.getSelectionController().isCursorBeingModified()); long event3Time = 1003; MotionEvent event3 = downEvent(event3Time, event3Time, 20f, 30f); - mActivity.runOnUiThread(() -> editor.onTouchEvent(event3)); - mInstrumentation.waitForIdleSync(); + mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event3)); assertFalse(editor.getInsertionController().isCursorBeingModified()); assertTrue(editor.getSelectionController().isCursorBeingModified()); long event4Time = 1004; MotionEvent event4 = moveEvent(event3Time, event4Time, 120f, 30f); - mActivity.runOnUiThread(() -> editor.onTouchEvent(event4)); - mInstrumentation.waitForIdleSync(); + mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event4)); assertFalse(editor.getInsertionController().isCursorBeingModified()); assertTrue(editor.getSelectionController().isCursorBeingModified()); long event5Time = 1005; MotionEvent event5 = upEvent(event3Time, event5Time, 120f, 30f); - mActivity.runOnUiThread(() -> editor.onTouchEvent(event5)); - mInstrumentation.waitForIdleSync(); + mInstrumentation.runOnMainSync(() -> editor.onTouchEvent(event5)); assertFalse(editor.getInsertionController().isCursorBeingModified()); assertFalse(editor.getSelectionController().isCursorBeingModified()); } diff --git a/core/tests/coretests/src/android/widget/EditorTouchStateTest.java b/core/tests/coretests/src/android/widget/EditorTouchStateTest.java index 6adb1b8fa0d60..215d0b800074e 100644 --- a/core/tests/coretests/src/android/widget/EditorTouchStateTest.java +++ b/core/tests/coretests/src/android/widget/EditorTouchStateTest.java @@ -119,6 +119,60 @@ public class EditorTouchStateTest { MultiTapStatus.DOUBLE_TAP, false); } + @Test + public void testUpdate_doubleTap_delayAfterFirstDownEvent() throws Exception { + // Simulate an ACTION_DOWN event. + long event1Time = 1000; + MotionEvent event1 = downEvent(event1Time, event1Time, 20f, 30f); + mTouchState.update(event1, mConfig); + assertSingleTap(mTouchState, 20f, 30f, 0, 0, false); + + // Simulate an ACTION_UP event with a delay that's longer than the double-tap timeout. + long event2Time = 1000 + ViewConfiguration.getDoubleTapTimeout() + 1; + MotionEvent event2 = upEvent(event1Time, event2Time, 20f, 30f); + mTouchState.update(event2, mConfig); + assertSingleTap(mTouchState, 20f, 30f, 20f, 30f, false); + + // Generate an ACTION_DOWN event whose time is within the double-tap timeout when + // calculated from the last ACTION_UP event time. Even though the time between the last up + // and this down event is within the double-tap timeout, this should not be considered a + // double-tap (since the first down event had a longer delay). + long event3Time = event2Time + 1; + MotionEvent event3 = downEvent(event3Time, event3Time, 22f, 33f); + mTouchState.update(event3, mConfig); + assertSingleTap(mTouchState, 22f, 33f, 20f, 30f, false); + } + + @Test + public void testUpdate_quickTapAfterDrag() throws Exception { + // Simulate an ACTION_DOWN event. + long event1Time = 1000; + MotionEvent event1 = downEvent(event1Time, event1Time, 20f, 30f); + mTouchState.update(event1, mConfig); + assertSingleTap(mTouchState, 20f, 30f, 0, 0, false); + + // Simulate an ACTION_MOVE event. + long event2Time = 1001; + MotionEvent event2 = moveEvent(event1Time, event2Time, 200f, 31f); + mTouchState.update(event2, mConfig); + assertSingleTap(mTouchState, 20f, 30f, 0, 0, true); + + // Simulate an ACTION_UP event with a delay that's longer than the double-tap timeout. + long event3Time = 5000; + MotionEvent event3 = upEvent(event1Time, event3Time, 200f, 31f); + mTouchState.update(event3, mConfig); + assertSingleTap(mTouchState, 20f, 30f, 200f, 31f, false); + + // Generate an ACTION_DOWN event whose time is within the double-tap timeout when + // calculated from the last ACTION_UP event time. Even though the time between the last up + // and this down event is within the double-tap timeout, this should not be considered a + // double-tap (since the first down event had a longer delay). + long event4Time = event3Time + 1; + MotionEvent event4 = downEvent(event4Time, event4Time, 200f, 31f); + mTouchState.update(event4, mConfig); + assertSingleTap(mTouchState, 200f, 31f, 200f, 31f, false); + } + @Test public void testUpdate_tripleClick_mouse() throws Exception { // Simulate an ACTION_DOWN event.