Merge "Ability to start a cursor drag from anywhere in an editable TextView"
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index eb0d9bf..3b6a009 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -148,8 +148,11 @@
public class Editor {
private static final String TAG = "Editor";
private static final boolean DEBUG_UNDO = false;
- // Specifies whether to use or not the magnifier when pressing the insertion or selection
- // handles.
+
+ // Specifies whether to allow starting a cursor drag by dragging anywhere over the text.
+ @VisibleForTesting
+ public static boolean FLAG_ENABLE_CURSOR_DRAG = false;
+ // Specifies whether to use the magnifier when pressing the insertion or selection handles.
private static final boolean FLAG_USE_MAGNIFIER = true;
private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000;
@@ -204,7 +207,7 @@
private final MetricsLogger mMetricsLogger = new MetricsLogger();
// Cursor Controllers.
- private InsertionPointCursorController mInsertionPointCursorController;
+ InsertionPointCursorController mInsertionPointCursorController;
SelectionModifierCursorController mSelectionModifierCursorController;
// Action mode used when text is selected or when actions on an insertion cursor are triggered.
private ActionMode mTextActionMode;
@@ -1471,6 +1474,9 @@
mTouchState.update(event, viewConfiguration);
updateFloatingToolbarVisibility(event);
+ if (hasInsertionController()) {
+ getInsertionController().onTouchEvent(event);
+ }
if (hasSelectionController()) {
getSelectionController().onTouchEvent(event);
}
@@ -5179,15 +5185,11 @@
case MotionEvent.ACTION_UP:
if (!offsetHasBeenChanged()) {
- final float deltaX = mLastDownRawX - ev.getRawX();
- final float deltaY = mLastDownRawY - ev.getRawY();
- final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
-
- final ViewConfiguration viewConfiguration = ViewConfiguration.get(
- mTextView.getContext());
- final int touchSlop = viewConfiguration.getScaledTouchSlop();
-
- if (distanceSquared < touchSlop * touchSlop) {
+ ViewConfiguration config = ViewConfiguration.get(mTextView.getContext());
+ boolean isWithinTouchSlop = EditorTouchState.isDistanceWithin(
+ mLastDownRawX, mLastDownRawY, ev.getRawX(), ev.getRawY(),
+ config.getScaledTouchSlop());
+ if (isWithinTouchSlop) {
// Tapping on the handle toggles the insertion action mode.
if (mTextActionMode != null) {
stopTextActionMode();
@@ -5237,6 +5239,10 @@
} else {
offset = -1;
}
+ if (TextView.DEBUG_CURSOR) {
+ logCursor("InsertionHandleView: updatePosition", "x=%f, y=%f, offset=%d, line=%d",
+ x, y, offset, mPreviousLineTouched);
+ }
positionAtCursorOffset(offset, false, fromTouchScreen);
if (mTextActionMode != null) {
invalidateActionMode();
@@ -5716,8 +5722,82 @@
}
}
- private class InsertionPointCursorController implements CursorController {
+ class InsertionPointCursorController implements CursorController {
private InsertionHandleView mHandle;
+ private boolean mIsDraggingCursor;
+
+ public void onTouchEvent(MotionEvent event) {
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ mIsDraggingCursor = false;
+ break;
+ case MotionEvent.ACTION_MOVE:
+ if (mIsDraggingCursor) {
+ performCursorDrag(event);
+ } else if (FLAG_ENABLE_CURSOR_DRAG
+ && mTextView.getLayout() != null
+ && mTextView.isFocused()
+ && mTouchState.isMovedEnoughForDrag()) {
+ startCursorDrag(event);
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ if (mIsDraggingCursor) {
+ endCursorDrag(event);
+ }
+ break;
+ }
+ }
+
+ private void positionCursorDuringDrag(MotionEvent event) {
+ int line = mTextView.getLineAtCoordinate(event.getY());
+ int offset = mTextView.getOffsetAtCoordinate(line, event.getX());
+ int oldSelectionStart = mTextView.getSelectionStart();
+ int oldSelectionEnd = mTextView.getSelectionEnd();
+ if (offset == oldSelectionStart && offset == oldSelectionEnd) {
+ return;
+ }
+ Selection.setSelection((Spannable) mTextView.getText(), offset);
+ updateCursorPosition();
+ if (mHapticTextHandleEnabled) {
+ mTextView.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE);
+ }
+ }
+
+ private void startCursorDrag(MotionEvent event) {
+ if (TextView.DEBUG_CURSOR) {
+ logCursor("InsertionPointCursorController", "start cursor drag");
+ }
+ mIsDraggingCursor = true;
+ // We don't want the parent scroll/long-press handlers to take over while dragging.
+ mTextView.getParent().requestDisallowInterceptTouchEvent(true);
+ mTextView.cancelLongPress();
+ // Update the cursor position.
+ positionCursorDuringDrag(event);
+ // Show the cursor handle and magnifier.
+ show();
+ getHandle().removeHiderCallback();
+ getHandle().updateMagnifier(event);
+ // TODO(b/146555651): Figure out if suspendBlink() should be called here.
+ }
+
+ private void performCursorDrag(MotionEvent event) {
+ positionCursorDuringDrag(event);
+ getHandle().updateMagnifier(event);
+ }
+
+ private void endCursorDrag(MotionEvent event) {
+ if (TextView.DEBUG_CURSOR) {
+ logCursor("InsertionPointCursorController", "end cursor drag");
+ }
+ mIsDraggingCursor = false;
+ // Hide the magnifier and set the handle to be hidden after a delay.
+ getHandle().dismissMagnifier();
+ getHandle().hideAfterDelay();
+ // We're no longer dragging, so let the parent receive events.
+ mTextView.getParent().requestDisallowInterceptTouchEvent(false);
+ }
public void show() {
getHandle().show();
@@ -5725,15 +5805,19 @@
final long durationSinceCutOrCopy =
SystemClock.uptimeMillis() - TextView.sLastCutCopyOrTextChangedTime;
- // Cancel the single tap delayed runnable.
- if (mInsertionActionModeRunnable != null
- && (mTouchState.isMultiTap() || isCursorInsideEasyCorrectionSpan())) {
- mTextView.removeCallbacks(mInsertionActionModeRunnable);
+ if (mInsertionActionModeRunnable != null) {
+ if (mIsDraggingCursor
+ || mTouchState.isMultiTap()
+ || isCursorInsideEasyCorrectionSpan()) {
+ // Cancel the runnable for showing the floating toolbar.
+ mTextView.removeCallbacks(mInsertionActionModeRunnable);
+ }
}
- // Prepare and schedule the single tap runnable to run exactly after the double tap
- // timeout has passed.
- if (!mTouchState.isMultiTap()
+ // If the user recently performed a Cut or Copy action, we want to show the floating
+ // toolbar even for a single tap.
+ if (!mIsDraggingCursor
+ && !mTouchState.isMultiTap()
&& !isCursorInsideEasyCorrectionSpan()
&& (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION_MS)) {
if (mTextActionMode == null) {
@@ -5751,7 +5835,9 @@
}
}
- getHandle().hideAfterDelay();
+ if (!mIsDraggingCursor) {
+ getHandle().hideAfterDelay();
+ }
if (mSelectionModifierCursorController != null) {
mSelectionModifierCursorController.hide();
@@ -5797,7 +5883,7 @@
@Override
public boolean isCursorBeingModified() {
- return mHandle != null && mHandle.isDragging();
+ return mIsDraggingCursor || (mHandle != null && mHandle.isDragging());
}
@Override
@@ -5959,7 +6045,6 @@
case MotionEvent.ACTION_MOVE:
final ViewConfiguration viewConfig = ViewConfiguration.get(
mTextView.getContext());
- final int touchSlop = viewConfig.getScaledTouchSlop();
if (mGestureStayedInTapRegion || mHaventMovedEnoughToStartDrag) {
final float deltaX = eventX - mTouchState.getLastDownX();
@@ -5973,6 +6058,7 @@
}
if (mHaventMovedEnoughToStartDrag) {
// We don't start dragging until the user has moved enough.
+ int touchSlop = viewConfig.getScaledTouchSlop();
mHaventMovedEnoughToStartDrag =
distanceSquared <= touchSlop * touchSlop;
}
diff --git a/core/java/android/widget/EditorTouchState.java b/core/java/android/widget/EditorTouchState.java
index f880939..3798d00 100644
--- a/core/java/android/widget/EditorTouchState.java
+++ b/core/java/android/widget/EditorTouchState.java
@@ -55,6 +55,8 @@
private int mMultiTapStatus = MultiTapStatus.NONE;
private boolean mMultiTapInSameArea;
+ private boolean mMovedEnoughForDrag;
+
public float getLastDownX() {
return mLastDownX;
}
@@ -88,10 +90,14 @@
return isMultiTap() && mMultiTapInSameArea;
}
+ public boolean isMovedEnoughForDrag() {
+ return mMovedEnoughForDrag;
+ }
+
/**
* Updates the state based on the new event.
*/
- public void update(MotionEvent event, ViewConfiguration viewConfiguration) {
+ public void update(MotionEvent event, ViewConfiguration config) {
final int action = event.getActionMasked();
if (action == MotionEvent.ACTION_DOWN) {
final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
@@ -105,11 +111,8 @@
} else {
mMultiTapStatus = MultiTapStatus.TRIPLE_CLICK;
}
- final float deltaX = event.getX() - mLastDownX;
- final float deltaY = event.getY() - mLastDownY;
- final int distanceSquared = (int) ((deltaX * deltaX) + (deltaY * deltaY));
- int doubleTapSlop = viewConfiguration.getScaledDoubleTapSlop();
- mMultiTapInSameArea = distanceSquared < doubleTapSlop * doubleTapSlop;
+ mMultiTapInSameArea = isDistanceWithin(mLastDownX, mLastDownY,
+ event.getX(), event.getY(), config.getScaledDoubleTapSlop());
if (TextView.DEBUG_CURSOR) {
String status = isDoubleTap() ? "double" : "triple";
String inSameArea = mMultiTapInSameArea ? "in same area" : "not in same area";
@@ -125,6 +128,7 @@
}
mLastDownX = event.getX();
mLastDownY = event.getY();
+ mMovedEnoughForDrag = false;
} else if (action == MotionEvent.ACTION_UP) {
if (TextView.DEBUG_CURSOR) {
logCursor("EditorTouchState", "ACTION_UP");
@@ -132,6 +136,23 @@
mLastUpX = event.getX();
mLastUpY = event.getY();
mLastUpMillis = event.getEventTime();
+ mMovedEnoughForDrag = false;
+ } else if (action == MotionEvent.ACTION_MOVE) {
+ mMovedEnoughForDrag = !isDistanceWithin(mLastDownX, mLastDownY,
+ event.getX(), event.getY(), config.getScaledTouchSlop());
}
}
+
+ /**
+ * Returns true if the distance between the given coordinates is <= to the specified max.
+ * This is useful to be able to determine e.g. when the user's touch has moved enough in
+ * order to be considered a drag (no longer within touch slop).
+ */
+ public static boolean isDistanceWithin(float x1, float y1, float x2, float y2,
+ int maxDistance) {
+ float deltaX = x2 - x1;
+ float deltaY = y2 - y1;
+ float distanceSquared = (deltaX * deltaX) + (deltaY * deltaY);
+ return distanceSquared <= maxDistance * maxDistance;
+ }
}
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index ee169f2..43d9895 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -10870,6 +10870,10 @@
if (mEditor != null) {
mEditor.onTouchEvent(event);
+ if (mEditor.mInsertionPointCursorController != null
+ && mEditor.mInsertionPointCursorController.isCursorBeingModified()) {
+ return true;
+ }
if (mEditor.mSelectionModifierCursorController != null
&& mEditor.mSelectionModifierCursorController.isDragAcceleratorActive()) {
return true;
diff --git a/core/tests/coretests/src/android/widget/EditorCursorDragTest.java b/core/tests/coretests/src/android/widget/EditorCursorDragTest.java
new file mode 100644
index 0000000..b4f2c91
--- /dev/null
+++ b/core/tests/coretests/src/android/widget/EditorCursorDragTest.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import static android.widget.espresso.TextViewActions.dragOnText;
+import static android.widget.espresso.TextViewAssertions.hasInsertionPointerAtIndex;
+
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.action.ViewActions.replaceText;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.frameworks.coretests.R;
+
+import com.google.common.base.Strings;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class EditorCursorDragTest {
+ @Rule
+ public ActivityTestRule<TextViewActivity> mActivityRule = new ActivityTestRule<>(
+ TextViewActivity.class);
+
+ private boolean mOriginalFlagValue;
+ private Instrumentation mInstrumentation;
+ private Activity mActivity;
+
+ @Before
+ public void before() throws Throwable {
+ mInstrumentation = InstrumentationRegistry.getInstrumentation();
+ mActivity = mActivityRule.getActivity();
+ mOriginalFlagValue = Editor.FLAG_ENABLE_CURSOR_DRAG;
+ Editor.FLAG_ENABLE_CURSOR_DRAG = true;
+ }
+ @After
+ public void after() throws Throwable {
+ Editor.FLAG_ENABLE_CURSOR_DRAG = mOriginalFlagValue;
+ }
+
+ @Test
+ public void testCursorDrag_horizontal_whenTextViewContentsFitOnScreen() throws Throwable {
+ String text = "Hello world!";
+ onView(withId(R.id.textview)).perform(replaceText(text));
+ onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(0));
+
+ // Drag left to right. The cursor should end up at the position where the finger is lifted.
+ onView(withId(R.id.textview)).perform(dragOnText(text.indexOf("llo"), text.indexOf("!")));
+ onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(11));
+
+ // Drag right to left. The cursor should end up at the position where the finger is lifted.
+ onView(withId(R.id.textview)).perform(dragOnText(text.indexOf("!"), text.indexOf("llo")));
+ onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(2));
+ }
+
+ @Test
+ public void testCursorDrag_horizontal_whenTextViewContentsLargerThanScreen() throws Throwable {
+ String text = "Hello world!"
+ + Strings.repeat("\n", 500) + "012345middle"
+ + Strings.repeat("\n", 10) + "012345last";
+ onView(withId(R.id.textview)).perform(replaceText(text));
+ onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(0));
+
+ // Drag left to right. The cursor should end up at the position where the finger is lifted.
+ onView(withId(R.id.textview)).perform(dragOnText(text.indexOf("llo"), text.indexOf("!")));
+ onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(11));
+
+ // Drag right to left. The cursor should end up at the position where the finger is lifted.
+ onView(withId(R.id.textview)).perform(dragOnText(text.indexOf("!"), text.indexOf("llo")));
+ onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(2));
+ }
+
+ @Test
+ public void testCursorDrag_diagonal_whenTextViewContentsLargerThanScreen() throws Throwable {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 1; i <= 9; i++) {
+ sb.append("line").append(i).append("\n");
+ }
+ sb.append(Strings.repeat("0123456789\n\n", 500)).append("Last line");
+ String text = sb.toString();
+ onView(withId(R.id.textview)).perform(replaceText(text));
+ onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(0));
+
+ // Drag along a diagonal path.
+ onView(withId(R.id.textview)).perform(dragOnText(text.indexOf("line1"), text.indexOf("2")));
+ onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.indexOf("2")));
+
+ // Drag along a steeper diagonal path.
+ onView(withId(R.id.textview)).perform(dragOnText(text.indexOf("line1"), text.indexOf("9")));
+ onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.indexOf("9")));
+
+ // Drag along an almost vertical path.
+ // TODO(b/145833335): Consider whether this should scroll instead of dragging the cursor.
+ onView(withId(R.id.textview)).perform(dragOnText(text.indexOf("ne1"), text.indexOf("9")));
+ onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.indexOf("9")));
+
+ // Drag along a vertical path from line 1 to line 9.
+ // TODO(b/145833335): Consider whether this should scroll instead of dragging the cursor.
+ onView(withId(R.id.textview)).perform(dragOnText(text.indexOf("e1"), text.indexOf("e9")));
+ onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.indexOf("e9")));
+
+ // Drag along a vertical path from line 9 to line 1.
+ // TODO(b/145833335): Consider whether this should scroll instead of dragging the cursor.
+ onView(withId(R.id.textview)).perform(dragOnText(text.indexOf("e9"), text.indexOf("e1")));
+ onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.indexOf("e1")));
+ }
+
+ @Test
+ public void testCursorDrag_vertical_whenTextViewContentsFitOnScreen() throws Throwable {
+ String text = "012first\n\n" + Strings.repeat("012345\n\n", 10) + "012last";
+ onView(withId(R.id.textview)).perform(replaceText(text));
+ onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(0));
+
+ // Drag down. Since neither the TextView nor its container require scrolling, the cursor
+ // drag should execute and the cursor should end up at the position where the finger is
+ // lifted.
+ onView(withId(R.id.textview)).perform(
+ dragOnText(text.indexOf("first"), text.indexOf("last")));
+ onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.length() - 4));
+
+ // Drag up. Since neither the TextView nor its container require scrolling, the cursor
+ // drag should execute and the cursor should end up at the position where the finger is
+ // lifted.
+ onView(withId(R.id.textview)).perform(
+ dragOnText(text.indexOf("last"), text.indexOf("first")));
+ onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(3));
+ }
+
+ @Test
+ public void testCursorDrag_vertical_whenTextViewContentsLargerThanScreen() throws Throwable {
+ String text = "012345first\n\n"
+ + Strings.repeat("0123456789\n\n", 10) + "012345middle"
+ + Strings.repeat("0123456789\n\n", 500) + "012345last";
+ onView(withId(R.id.textview)).perform(replaceText(text));
+ int initialCursorPosition = 0;
+ onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(initialCursorPosition));
+
+ // Drag up.
+ // TODO(b/145833335): Consider whether this should scroll instead of dragging the cursor.
+ onView(withId(R.id.textview)).perform(
+ dragOnText(text.indexOf("middle"), text.indexOf("first")));
+ onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.indexOf("first")));
+
+ // Drag down.
+ // TODO(b/145833335): Consider whether this should scroll instead of dragging the cursor.
+ onView(withId(R.id.textview)).perform(
+ dragOnText(text.indexOf("first"), text.indexOf("middle")));
+ onView(withId(R.id.textview)).check(hasInsertionPointerAtIndex(text.indexOf("middle")));
+ }
+}
diff --git a/core/tests/coretests/src/android/widget/EditorTouchStateTest.java b/core/tests/coretests/src/android/widget/EditorTouchStateTest.java
index 6d50e3a..6adb1b8 100644
--- a/core/tests/coretests/src/android/widget/EditorTouchStateTest.java
+++ b/core/tests/coretests/src/android/widget/EditorTouchStateTest.java
@@ -16,6 +16,9 @@
package android.widget;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
@@ -50,19 +53,19 @@
long event1Time = 1000;
MotionEvent event1 = downEvent(event1Time, event1Time, 20f, 30f);
mTouchState.update(event1, mConfig);
- assertSingleTap(mTouchState, 20f, 30f, 0, 0);
+ assertSingleTap(mTouchState, 20f, 30f, 0, 0, false);
// Simulate an ACTION_UP event.
long event2Time = 1001;
MotionEvent event2 = upEvent(event1Time, event2Time, 20f, 30f);
mTouchState.update(event2, mConfig);
- assertSingleTap(mTouchState, 20f, 30f, 20f, 30f);
+ assertSingleTap(mTouchState, 20f, 30f, 20f, 30f, false);
// Generate an ACTION_DOWN event whose time is after the double-tap timeout.
long event3Time = event2Time + ViewConfiguration.getDoubleTapTimeout() + 1;
MotionEvent event3 = downEvent(event3Time, event3Time, 22f, 33f);
mTouchState.update(event3, mConfig);
- assertSingleTap(mTouchState, 22f, 33f, 20f, 30f);
+ assertSingleTap(mTouchState, 22f, 33f, 20f, 30f, false);
}
@Test
@@ -71,19 +74,19 @@
long event1Time = 1000;
MotionEvent event1 = downEvent(event1Time, event1Time, 20f, 30f);
mTouchState.update(event1, mConfig);
- assertSingleTap(mTouchState, 20f, 30f, 0, 0);
+ assertSingleTap(mTouchState, 20f, 30f, 0, 0, false);
// Simulate an ACTION_UP event.
long event2Time = 1001;
MotionEvent event2 = upEvent(event1Time, event2Time, 20f, 30f);
mTouchState.update(event2, mConfig);
- assertSingleTap(mTouchState, 20f, 30f, 20f, 30f);
+ assertSingleTap(mTouchState, 20f, 30f, 20f, 30f, false);
// Generate an ACTION_DOWN event whose time is within the double-tap timeout.
long event3Time = 1002;
MotionEvent event3 = downEvent(event3Time, event3Time, 22f, 33f);
mTouchState.update(event3, mConfig);
- assertTap(mTouchState, 22f, 33f, 20f, 30f,
+ assertMultiTap(mTouchState, 22f, 33f, 20f, 30f,
MultiTapStatus.DOUBLE_TAP, true);
}
@@ -93,26 +96,26 @@
long event1Time = 1000;
MotionEvent event1 = downEvent(event1Time, event1Time, 20f, 30f);
mTouchState.update(event1, mConfig);
- assertSingleTap(mTouchState, 20f, 30f, 0, 0);
+ assertSingleTap(mTouchState, 20f, 30f, 0, 0, false);
// Simulate an ACTION_UP event.
long event2Time = 1001;
MotionEvent event2 = upEvent(event1Time, event2Time, 20f, 30f);
mTouchState.update(event2, mConfig);
- assertSingleTap(mTouchState, 20f, 30f, 20f, 30f);
+ assertSingleTap(mTouchState, 20f, 30f, 20f, 30f, false);
// Generate an ACTION_DOWN event whose time is within the double-tap timeout.
long event3Time = 1002;
MotionEvent event3 = downEvent(event3Time, event3Time, 200f, 300f);
mTouchState.update(event3, mConfig);
- assertTap(mTouchState, 200f, 300f, 20f, 30f,
+ assertMultiTap(mTouchState, 200f, 300f, 20f, 30f,
MultiTapStatus.DOUBLE_TAP, false);
// Simulate an ACTION_UP event.
long event4Time = 1003;
MotionEvent event4 = upEvent(event3Time, event4Time, 200f, 300f);
mTouchState.update(event4, mConfig);
- assertTap(mTouchState, 200f, 300f, 200f, 300f,
+ assertMultiTap(mTouchState, 200f, 300f, 200f, 300f,
MultiTapStatus.DOUBLE_TAP, false);
}
@@ -123,21 +126,21 @@
MotionEvent event1 = downEvent(event1Time, event1Time, 20f, 30f);
event1.setSource(InputDevice.SOURCE_MOUSE);
mTouchState.update(event1, mConfig);
- assertSingleTap(mTouchState, 20f, 30f, 0, 0);
+ assertSingleTap(mTouchState, 20f, 30f, 0, 0, false);
// Simulate an ACTION_UP event.
long event2Time = 1001;
MotionEvent event2 = upEvent(event1Time, event2Time, 20f, 30f);
event2.setSource(InputDevice.SOURCE_MOUSE);
mTouchState.update(event2, mConfig);
- assertSingleTap(mTouchState, 20f, 30f, 20f, 30f);
+ assertSingleTap(mTouchState, 20f, 30f, 20f, 30f, false);
// Generate a second ACTION_DOWN event whose time is within the double-tap timeout.
long event3Time = 1002;
MotionEvent event3 = downEvent(event3Time, event3Time, 21f, 31f);
event3.setSource(InputDevice.SOURCE_MOUSE);
mTouchState.update(event3, mConfig);
- assertTap(mTouchState, 21f, 31f, 20f, 30f,
+ assertMultiTap(mTouchState, 21f, 31f, 20f, 30f,
MultiTapStatus.DOUBLE_TAP, true);
// Simulate an ACTION_UP event.
@@ -145,7 +148,7 @@
MotionEvent event4 = upEvent(event3Time, event4Time, 21f, 31f);
event4.setSource(InputDevice.SOURCE_MOUSE);
mTouchState.update(event4, mConfig);
- assertTap(mTouchState, 21f, 31f, 21f, 31f,
+ assertMultiTap(mTouchState, 21f, 31f, 21f, 31f,
MultiTapStatus.DOUBLE_TAP, true);
// Generate a third ACTION_DOWN event whose time is within the double-tap timeout.
@@ -153,7 +156,7 @@
MotionEvent event5 = downEvent(event5Time, event5Time, 22f, 32f);
event5.setSource(InputDevice.SOURCE_MOUSE);
mTouchState.update(event5, mConfig);
- assertTap(mTouchState, 22f, 32f, 21f, 31f,
+ assertMultiTap(mTouchState, 22f, 32f, 21f, 31f,
MultiTapStatus.TRIPLE_CLICK, true);
}
@@ -163,34 +166,71 @@
long event1Time = 1000;
MotionEvent event1 = downEvent(event1Time, event1Time, 20f, 30f);
mTouchState.update(event1, mConfig);
- assertSingleTap(mTouchState, 20f, 30f, 0, 0);
+ assertSingleTap(mTouchState, 20f, 30f, 0, 0, false);
// Simulate an ACTION_UP event.
long event2Time = 1001;
MotionEvent event2 = upEvent(event1Time, event2Time, 20f, 30f);
mTouchState.update(event2, mConfig);
- assertSingleTap(mTouchState, 20f, 30f, 20f, 30f);
+ assertSingleTap(mTouchState, 20f, 30f, 20f, 30f, false);
// Generate a second ACTION_DOWN event whose time is within the double-tap timeout.
long event3Time = 1002;
MotionEvent event3 = downEvent(event3Time, event3Time, 21f, 31f);
mTouchState.update(event3, mConfig);
- assertTap(mTouchState, 21f, 31f, 20f, 30f,
+ assertMultiTap(mTouchState, 21f, 31f, 20f, 30f,
MultiTapStatus.DOUBLE_TAP, true);
// Simulate an ACTION_UP event.
long event4Time = 1003;
MotionEvent event4 = upEvent(event3Time, event4Time, 21f, 31f);
mTouchState.update(event4, mConfig);
- assertTap(mTouchState, 21f, 31f, 21f, 31f,
+ assertMultiTap(mTouchState, 21f, 31f, 21f, 31f,
MultiTapStatus.DOUBLE_TAP, true);
// Generate a third ACTION_DOWN event whose time is within the double-tap timeout.
long event5Time = 1004;
MotionEvent event5 = downEvent(event5Time, event5Time, 22f, 32f);
mTouchState.update(event5, mConfig);
- assertTap(mTouchState, 22f, 32f, 21f, 31f,
- MultiTapStatus.FIRST_TAP, false);
+ assertSingleTap(mTouchState, 22f, 32f, 21f, 31f, false);
+ }
+
+ @Test
+ public void testUpdate_drag() 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 whose location is not far enough to start a drag.
+ long event2Time = 1001;
+ MotionEvent event2 = moveEvent(event1Time, event2Time, 21f, 30f);
+ mTouchState.update(event2, mConfig);
+ assertSingleTap(mTouchState, 20f, 30f, 0, 0, false);
+
+ // Simulate another ACTION_MOVE event whose location is far enough to start a drag.
+ int touchSlop = mConfig.getScaledTouchSlop();
+ float newX = event1.getX() + touchSlop + 1;
+ float newY = event1.getY();
+ long event3Time = 1002;
+ MotionEvent event3 = moveEvent(event3Time, event3Time, newX, newY);
+ mTouchState.update(event3, mConfig);
+ assertSingleTap(mTouchState, 20f, 30f, 0, 0, true);
+
+ // Simulate an ACTION_UP event.
+ long event4Time = 1003;
+ MotionEvent event4 = upEvent(event3Time, event4Time, 200f, 300f);
+ mTouchState.update(event4, mConfig);
+ assertSingleTap(mTouchState, 20f, 30f, 200f, 300f, false);
+ }
+
+ @Test
+ public void testIsDistanceWithin() throws Exception {
+ assertTrue(EditorTouchState.isDistanceWithin(0, 0, 0, 0, 8));
+ assertTrue(EditorTouchState.isDistanceWithin(3, 9, 5, 11, 8));
+ assertTrue(EditorTouchState.isDistanceWithin(5, 11, 3, 9, 8));
+ assertFalse(EditorTouchState.isDistanceWithin(5, 10, 5, 20, 8));
}
private static MotionEvent downEvent(long downTime, long eventTime, float x, float y) {
@@ -201,8 +241,12 @@
return MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_UP, x, y, 0);
}
+ private static MotionEvent moveEvent(long downTime, long eventTime, float x, float y) {
+ return MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_MOVE, x, y, 0);
+ }
+
private static void assertSingleTap(EditorTouchState touchState, float lastDownX,
- float lastDownY, float lastUpX, float lastUpY) {
+ float lastDownY, float lastUpX, float lastUpY, boolean isMovedEnoughForDrag) {
assertThat(touchState.getLastDownX(), is(lastDownX));
assertThat(touchState.getLastDownY(), is(lastDownY));
assertThat(touchState.getLastUpX(), is(lastUpX));
@@ -211,9 +255,10 @@
assertThat(touchState.isTripleClick(), is(false));
assertThat(touchState.isMultiTap(), is(false));
assertThat(touchState.isMultiTapInSameArea(), is(false));
+ assertThat(touchState.isMovedEnoughForDrag(), is(isMovedEnoughForDrag));
}
- private static void assertTap(EditorTouchState touchState,
+ private static void assertMultiTap(EditorTouchState touchState,
float lastDownX, float lastDownY, float lastUpX, float lastUpY,
@MultiTapStatus int multiTapStatus, boolean isMultiTapInSameArea) {
assertThat(touchState.getLastDownX(), is(lastDownX));
@@ -225,5 +270,6 @@
assertThat(touchState.isMultiTap(), is(multiTapStatus == MultiTapStatus.DOUBLE_TAP
|| multiTapStatus == MultiTapStatus.TRIPLE_CLICK));
assertThat(touchState.isMultiTapInSameArea(), is(isMultiTapInSameArea));
+ assertThat(touchState.isMovedEnoughForDrag(), is(false));
}
}
diff --git a/core/tests/coretests/src/android/widget/espresso/TextViewActions.java b/core/tests/coretests/src/android/widget/espresso/TextViewActions.java
index 83ce67b..4808a0b 100644
--- a/core/tests/coretests/src/android/widget/espresso/TextViewActions.java
+++ b/core/tests/coretests/src/android/widget/espresso/TextViewActions.java
@@ -358,6 +358,27 @@
}
/**
+ * Returns an action that drags on text from startIndex to endIndex on the TextView.<br>
+ * <br>
+ * View constraints:
+ * <ul>
+ * <li>must be a TextView displayed on screen
+ * <ul>
+ *
+ * @param startIndex The index of the TextView's text to start a drag from
+ * @param endIndex The index of the TextView's text to end the drag at
+ */
+ public static ViewAction dragOnText(int startIndex, int endIndex) {
+ return actionWithAssertions(
+ new DragAction(
+ DragAction.Drag.TAP,
+ new TextCoordinates(startIndex),
+ new TextCoordinates(endIndex),
+ Press.FINGER,
+ TextView.class));
+ }
+
+ /**
* A provider of the x, y coordinates of the handle dragging point.
*/
private static final class CurrentHandleCoordinates implements CoordinatesProvider {