diff options
| author | 2019-12-06 16:13:39 +0000 | |
|---|---|---|
| committer | 2019-12-06 16:13:39 +0000 | |
| commit | 5bfa0bf08b657840ffbea1d3026e083a61a93d8d (patch) | |
| tree | 469432d09e0b3fb2505073ea98b3c9efa910ea4c | |
| parent | eb7fd8c9fad564f0516c55da2543bf5fbb1a6172 (diff) | |
| parent | d63f2035bd19558ecde8f92a1d2452cb6eadcec8 (diff) | |
Merge "Consolidate tracking of touch state in Editor (editable TextView)"
| -rw-r--r-- | core/java/android/widget/Editor.java | 219 | ||||
| -rw-r--r-- | core/java/android/widget/EditorTouchState.java | 137 | ||||
| -rw-r--r-- | core/java/android/widget/TextView.java | 5 | ||||
| -rw-r--r-- | core/tests/coretests/src/android/widget/EditorTouchStateTest.java | 229 |
4 files changed, 448 insertions, 142 deletions
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index 187ab46b2e08..eb0d9bf088af 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -152,6 +152,9 @@ public class Editor { // handles. private static final boolean FLAG_USE_MAGNIFIER = true; + private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000; + private static final int RECENT_CUT_COPY_DURATION_MS = 15 * 1000; // 15 seconds in millis + static final int BLINK = 500; private static final int DRAG_SHADOW_MAX_TEXT_LENGTH = 20; private static final float LINE_SLOP_MULTIPLIER_FOR_HANDLEVIEWS = 0.5f; @@ -326,8 +329,6 @@ public class Editor { // Global listener that detects changes in the global position of the TextView private PositionListener mPositionListener; - private float mLastDownPositionX, mLastDownPositionY; - private float mLastUpPositionX, mLastUpPositionY; private float mContextMenuAnchorX, mContextMenuAnchorY; Callback mCustomSelectionActionModeCallback; Callback mCustomInsertionActionModeCallback; @@ -336,18 +337,11 @@ public class Editor { @UnsupportedAppUsage boolean mCreatedWithASelection; - // Indicates the current tap state (first tap, double tap, or triple click). - private int mTapState = TAP_STATE_INITIAL; - private long mLastTouchUpTime = 0; - private static final int TAP_STATE_INITIAL = 0; - private static final int TAP_STATE_FIRST_TAP = 1; - private static final int TAP_STATE_DOUBLE_TAP = 2; - // Only for mouse input. - private static final int TAP_STATE_TRIPLE_CLICK = 3; - // The button state as of the last time #onTouchEvent is called. private int mLastButtonState; + private final EditorTouchState mTouchState = new EditorTouchState(); + private Runnable mInsertionActionModeRunnable; // The span controller helps monitoring the changes to which the Editor needs to react: @@ -1193,10 +1187,10 @@ public class Editor { logCursor("performLongClick", "handled=%s", handled); } // Long press in empty space moves cursor and starts the insertion action mode. - if (!handled && !isPositionOnText(mLastDownPositionX, mLastDownPositionY) + if (!handled && !isPositionOnText(mTouchState.getLastDownX(), mTouchState.getLastDownY()) && mInsertionControllerEnabled) { - final int offset = mTextView.getOffsetForPosition(mLastDownPositionX, - mLastDownPositionY); + final int offset = mTextView.getOffsetForPosition(mTouchState.getLastDownX(), + mTouchState.getLastDownY()); Selection.setSelection((Spannable) mTextView.getText(), offset); getInsertionController().show(); mIsInsertionActionModeStartPending = true; @@ -1240,11 +1234,11 @@ public class Editor { } float getLastUpPositionX() { - return mLastUpPositionX; + return mTouchState.getLastUpX(); } float getLastUpPositionY() { - return mLastUpPositionY; + return mTouchState.getLastUpY(); } private long getLastTouchOffsets() { @@ -1279,6 +1273,9 @@ public class Editor { // Has to be done before onTakeFocus, which can be overloaded. final int lastTapPosition = getLastTapPosition(); if (lastTapPosition >= 0) { + if (TextView.DEBUG_CURSOR) { + logCursor("onFocusChanged", "setting cursor position: %d", lastTapPosition); + } Selection.setSelection((Spannable) mTextView.getText(), lastTapPosition); } @@ -1443,39 +1440,6 @@ public class Editor { } } - private void updateTapState(MotionEvent event) { - final int action = event.getActionMasked(); - if (action == MotionEvent.ACTION_DOWN) { - final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE); - // Detect double tap and triple click. - if (((mTapState == TAP_STATE_FIRST_TAP) - || ((mTapState == TAP_STATE_DOUBLE_TAP) && isMouse)) - && (SystemClock.uptimeMillis() - mLastTouchUpTime) - <= ViewConfiguration.getDoubleTapTimeout()) { - if (mTapState == TAP_STATE_FIRST_TAP) { - mTapState = TAP_STATE_DOUBLE_TAP; - } else { - mTapState = TAP_STATE_TRIPLE_CLICK; - } - if (TextView.DEBUG_CURSOR) { - logCursor("updateTapState", "ACTION_DOWN: %s tap detected", - (mTapState == TAP_STATE_DOUBLE_TAP ? "double" : "triple")); - } - } else { - mTapState = TAP_STATE_FIRST_TAP; - if (TextView.DEBUG_CURSOR) { - logCursor("updateTapState", "ACTION_DOWN: first tap detected"); - } - } - } - if (action == MotionEvent.ACTION_UP) { - mLastTouchUpTime = SystemClock.uptimeMillis(); - if (TextView.DEBUG_CURSOR) { - logCursor("updateTapState", "ACTION_UP"); - } - } - } - private boolean shouldFilterOutTouchEvent(MotionEvent event) { if (!event.isFromSource(InputDevice.SOURCE_MOUSE)) { return false; @@ -1503,7 +1467,8 @@ public class Editor { } return; } - updateTapState(event); + ViewConfiguration viewConfiguration = ViewConfiguration.get(mTextView.getContext()); + mTouchState.update(event, viewConfiguration); updateFloatingToolbarVisibility(event); if (hasSelectionController()) { @@ -1515,15 +1480,7 @@ public class Editor { mShowSuggestionRunnable = null; } - if (event.getActionMasked() == MotionEvent.ACTION_UP) { - mLastUpPositionX = event.getX(); - mLastUpPositionY = event.getY(); - } - if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { - mLastDownPositionX = event.getX(); - mLastDownPositionY = event.getY(); - // Reset this state; it will be re-set if super.onTouchEvent // causes focus to move to the view. mTouchFocusSelected = false; @@ -5067,7 +5024,10 @@ public class Editor { public boolean onTouchEvent(MotionEvent ev) { if (TextView.DEBUG_CURSOR) { logCursor(this.getClass().getSimpleName() + ": HandleView: onTouchEvent", - MotionEvent.actionToString(ev.getActionMasked())); + "%d: %s (%f,%f)", + ev.getSequenceNumber(), + MotionEvent.actionToString(ev.getActionMasked()), + ev.getX(), ev.getY()); } updateFloatingToolbarVisibility(ev); @@ -5145,56 +5105,14 @@ public class Editor { } private class InsertionHandleView extends HandleView { - private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000; - private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds - // Used to detect taps on the insertion handle, which will affect the insertion action mode - private float mDownPositionX, mDownPositionY; + private float mLastDownRawX, mLastDownRawY; private Runnable mHider; public InsertionHandleView(Drawable drawable) { super(drawable, drawable, com.android.internal.R.id.insertion_handle); } - @Override - public void show() { - super.show(); - - final long durationSinceCutOrCopy = - SystemClock.uptimeMillis() - TextView.sLastCutCopyOrTextChangedTime; - - // Cancel the single tap delayed runnable. - if (mInsertionActionModeRunnable != null - && ((mTapState == TAP_STATE_DOUBLE_TAP) - || (mTapState == TAP_STATE_TRIPLE_CLICK) - || isCursorInsideEasyCorrectionSpan())) { - mTextView.removeCallbacks(mInsertionActionModeRunnable); - } - - // Prepare and schedule the single tap runnable to run exactly after the double tap - // timeout has passed. - if ((mTapState != TAP_STATE_DOUBLE_TAP) && (mTapState != TAP_STATE_TRIPLE_CLICK) - && !isCursorInsideEasyCorrectionSpan() - && (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION)) { - if (mTextActionMode == null) { - if (mInsertionActionModeRunnable == null) { - mInsertionActionModeRunnable = new Runnable() { - @Override - public void run() { - startInsertionActionMode(); - } - }; - } - mTextView.postDelayed( - mInsertionActionModeRunnable, - ViewConfiguration.getDoubleTapTimeout() + 1); - } - - } - - hideAfterDelay(); - } - private void hideAfterDelay() { if (mHider == null) { mHider = new Runnable() { @@ -5250,8 +5168,8 @@ public class Editor { switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: - mDownPositionX = ev.getRawX(); - mDownPositionY = ev.getRawY(); + mLastDownRawX = ev.getRawX(); + mLastDownRawY = ev.getRawY(); updateMagnifier(ev); break; @@ -5261,8 +5179,8 @@ public class Editor { case MotionEvent.ACTION_UP: if (!offsetHasBeenChanged()) { - final float deltaX = mDownPositionX - ev.getRawX(); - final float deltaY = mDownPositionY - ev.getRawY(); + final float deltaX = mLastDownRawX - ev.getRawX(); + final float deltaY = mLastDownRawY - ev.getRawY(); final float distanceSquared = deltaX * deltaX + deltaY * deltaY; final ViewConfiguration viewConfiguration = ViewConfiguration.get( @@ -5804,6 +5722,37 @@ public class Editor { public void show() { getHandle().show(); + final long durationSinceCutOrCopy = + SystemClock.uptimeMillis() - TextView.sLastCutCopyOrTextChangedTime; + + // Cancel the single tap delayed runnable. + if (mInsertionActionModeRunnable != null + && (mTouchState.isMultiTap() || isCursorInsideEasyCorrectionSpan())) { + mTextView.removeCallbacks(mInsertionActionModeRunnable); + } + + // Prepare and schedule the single tap runnable to run exactly after the double tap + // timeout has passed. + if (!mTouchState.isMultiTap() + && !isCursorInsideEasyCorrectionSpan() + && (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION_MS)) { + if (mTextActionMode == null) { + if (mInsertionActionModeRunnable == null) { + mInsertionActionModeRunnable = new Runnable() { + @Override + public void run() { + startInsertionActionMode(); + } + }; + } + mTextView.postDelayed( + mInsertionActionModeRunnable, + ViewConfiguration.getDoubleTapTimeout() + 1); + } + } + + getHandle().hideAfterDelay(); + if (mSelectionModifierCursorController != null) { mSelectionModifierCursorController.hide(); } @@ -5870,7 +5819,6 @@ public class Editor { // The offsets of that last touch down event. Remembered to start selection there. private int mMinTouchOffset, mMaxTouchOffset; - private float mDownPositionX, mDownPositionY; private boolean mGestureStayedInTapRegion; // Where the user first starts the drag motion. @@ -5940,13 +5888,18 @@ public class Editor { } public void enterDrag(int dragAcceleratorMode) { + if (TextView.DEBUG_CURSOR) { + logCursor("SelectionModifierCursorController: enterDrag", + "starting selection drag: mode=%s", dragAcceleratorMode); + } + // Just need to init the handles / hide insertion cursor. show(); mDragAcceleratorMode = dragAcceleratorMode; // Start location of selection. - mStartOffset = mTextView.getOffsetForPosition(mLastDownPositionX, - mLastDownPositionY); - mLineSelectionIsOn = mTextView.getLineAtCoordinate(mLastDownPositionY); + mStartOffset = mTextView.getOffsetForPosition(mTouchState.getLastDownX(), + mTouchState.getLastDownY()); + mLineSelectionIsOn = mTextView.getLineAtCoordinate(mTouchState.getLastDownY()); // Don't show the handles until user has lifted finger. hide(); @@ -5974,36 +5927,20 @@ public class Editor { eventX, eventY); // Double tap detection - if (mGestureStayedInTapRegion) { - if (mTapState == TAP_STATE_DOUBLE_TAP - || mTapState == TAP_STATE_TRIPLE_CLICK) { - final float deltaX = eventX - mDownPositionX; - final float deltaY = eventY - mDownPositionY; - final float distanceSquared = deltaX * deltaX + deltaY * deltaY; - - ViewConfiguration viewConfiguration = ViewConfiguration.get( - mTextView.getContext()); - int doubleTapSlop = viewConfiguration.getScaledDoubleTapSlop(); - boolean stayedInArea = - distanceSquared < doubleTapSlop * doubleTapSlop; - - if (stayedInArea && (isMouse || isPositionOnText(eventX, eventY))) { - if (TextView.DEBUG_CURSOR) { - logCursor("SelectionModifierCursorController: onTouchEvent", - "ACTION_DOWN: select and start drag"); - } - if (mTapState == TAP_STATE_DOUBLE_TAP) { - selectCurrentWordAndStartDrag(); - } else if (mTapState == TAP_STATE_TRIPLE_CLICK) { - selectCurrentParagraphAndStartDrag(); - } - mDiscardNextActionUp = true; - } + if (mGestureStayedInTapRegion + && mTouchState.isMultiTapInSameArea() + && (isMouse || isPositionOnText(eventX, eventY))) { + if (TextView.DEBUG_CURSOR) { + logCursor("SelectionModifierCursorController: onTouchEvent", + "ACTION_DOWN: select and start drag"); + } + if (mTouchState.isDoubleTap()) { + selectCurrentWordAndStartDrag(); + } else if (mTouchState.isTripleClick()) { + selectCurrentParagraphAndStartDrag(); } + mDiscardNextActionUp = true; } - - mDownPositionX = eventX; - mDownPositionY = eventY; mGestureStayedInTapRegion = true; mHaventMovedEnoughToStartDrag = true; } @@ -6025,8 +5962,8 @@ public class Editor { final int touchSlop = viewConfig.getScaledTouchSlop(); if (mGestureStayedInTapRegion || mHaventMovedEnoughToStartDrag) { - final float deltaX = eventX - mDownPositionX; - final float deltaY = eventY - mDownPositionY; + final float deltaX = eventX - mTouchState.getLastDownX(); + final float deltaY = eventY - mTouchState.getLastDownY(); final float distanceSquared = deltaX * deltaX + deltaY * deltaY; if (mGestureStayedInTapRegion) { @@ -7164,7 +7101,7 @@ public class Editor { } } - private static void logCursor(String location, @Nullable String msgFormat, Object ... msgArgs) { + static void logCursor(String location, @Nullable String msgFormat, Object ... msgArgs) { if (msgFormat == null) { Log.d(TAG, location); } else { diff --git a/core/java/android/widget/EditorTouchState.java b/core/java/android/widget/EditorTouchState.java new file mode 100644 index 000000000000..f880939bee35 --- /dev/null +++ b/core/java/android/widget/EditorTouchState.java @@ -0,0 +1,137 @@ +/* + * 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.Editor.logCursor; + +import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; + +import android.annotation.IntDef; +import android.view.InputDevice; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +import com.android.internal.annotations.VisibleForTesting; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Helper class used by {@link Editor} to track state for touch events. + * + * @hide + */ +@VisibleForTesting(visibility = PACKAGE) +public class EditorTouchState { + private float mLastDownX, mLastDownY; + private float mLastUpX, mLastUpY; + private long mLastUpMillis; + + @IntDef({MultiTapStatus.NONE, MultiTapStatus.FIRST_TAP, MultiTapStatus.DOUBLE_TAP, + MultiTapStatus.TRIPLE_CLICK}) + @Retention(RetentionPolicy.SOURCE) + @VisibleForTesting + public @interface MultiTapStatus { + int NONE = 0; + int FIRST_TAP = 1; + int DOUBLE_TAP = 2; + int TRIPLE_CLICK = 3; // Only for mouse input. + } + @MultiTapStatus + private int mMultiTapStatus = MultiTapStatus.NONE; + private boolean mMultiTapInSameArea; + + public float getLastDownX() { + return mLastDownX; + } + + public float getLastDownY() { + return mLastDownY; + } + + public float getLastUpX() { + return mLastUpX; + } + + public float getLastUpY() { + return mLastUpY; + } + + public boolean isDoubleTap() { + return mMultiTapStatus == MultiTapStatus.DOUBLE_TAP; + } + + public boolean isTripleClick() { + return mMultiTapStatus == MultiTapStatus.TRIPLE_CLICK; + } + + public boolean isMultiTap() { + return mMultiTapStatus == MultiTapStatus.DOUBLE_TAP + || mMultiTapStatus == MultiTapStatus.TRIPLE_CLICK; + } + + public boolean isMultiTapInSameArea() { + return isMultiTap() && mMultiTapInSameArea; + } + + /** + * Updates the state based on the new event. + */ + public void update(MotionEvent event, ViewConfiguration viewConfiguration) { + final int action = event.getActionMasked(); + if (action == MotionEvent.ACTION_DOWN) { + final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE); + final long millisSinceLastUp = event.getEventTime() - mLastUpMillis; + // Detect double tap and triple click. + if (millisSinceLastUp <= ViewConfiguration.getDoubleTapTimeout() + && (mMultiTapStatus == MultiTapStatus.FIRST_TAP + || (mMultiTapStatus == MultiTapStatus.DOUBLE_TAP && isMouse))) { + if (mMultiTapStatus == MultiTapStatus.FIRST_TAP) { + mMultiTapStatus = MultiTapStatus.DOUBLE_TAP; + } 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; + if (TextView.DEBUG_CURSOR) { + String status = isDoubleTap() ? "double" : "triple"; + String inSameArea = mMultiTapInSameArea ? "in same area" : "not in same area"; + logCursor("EditorTouchState", "ACTION_DOWN: %s tap detected, %s", + status, inSameArea); + } + } else { + mMultiTapStatus = MultiTapStatus.FIRST_TAP; + mMultiTapInSameArea = false; + if (TextView.DEBUG_CURSOR) { + logCursor("EditorTouchState", "ACTION_DOWN: first tap detected"); + } + } + mLastDownX = event.getX(); + mLastDownY = event.getY(); + } else if (action == MotionEvent.ACTION_UP) { + if (TextView.DEBUG_CURSOR) { + logCursor("EditorTouchState", "ACTION_UP"); + } + mLastUpX = event.getX(); + mLastUpY = event.getY(); + mLastUpMillis = event.getEventTime(); + } + } +} diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 90e8ef2c6423..ee169f25b778 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -10860,7 +10860,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener @Override public boolean onTouchEvent(MotionEvent event) { if (DEBUG_CURSOR) { - logCursor("onTouchEvent", MotionEvent.actionToString(event.getActionMasked())); + logCursor("onTouchEvent", "%d: %s (%f,%f)", + event.getSequenceNumber(), + MotionEvent.actionToString(event.getActionMasked()), + event.getX(), event.getY()); } final int action = event.getActionMasked(); diff --git a/core/tests/coretests/src/android/widget/EditorTouchStateTest.java b/core/tests/coretests/src/android/widget/EditorTouchStateTest.java new file mode 100644 index 000000000000..6d50e3aa3b8f --- /dev/null +++ b/core/tests/coretests/src/android/widget/EditorTouchStateTest.java @@ -0,0 +1,229 @@ +/* + * 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 org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import android.view.InputDevice; +import android.view.MotionEvent; +import android.view.ViewConfiguration; +import android.widget.EditorTouchState.MultiTapStatus; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +@SmallTest +public class EditorTouchStateTest { + + private EditorTouchState mTouchState; + private ViewConfiguration mConfig; + + @Before + public void before() throws Exception { + mTouchState = new EditorTouchState(); + mConfig = new ViewConfiguration(); + } + + @Test + public void testUpdate_singleTap() 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); + + // 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); + + // 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); + } + + @Test + public void testUpdate_doubleTap_sameArea() 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); + + // 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); + + // 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, + MultiTapStatus.DOUBLE_TAP, true); + } + + @Test + public void testUpdate_doubleTap_notSameArea() 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); + + // 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); + + // 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, + 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, + MultiTapStatus.DOUBLE_TAP, false); + } + + @Test + public void testUpdate_tripleClick_mouse() throws Exception { + // Simulate an ACTION_DOWN event. + long event1Time = 1000; + MotionEvent event1 = downEvent(event1Time, event1Time, 20f, 30f); + event1.setSource(InputDevice.SOURCE_MOUSE); + mTouchState.update(event1, mConfig); + assertSingleTap(mTouchState, 20f, 30f, 0, 0); + + // 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); + + // 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, + MultiTapStatus.DOUBLE_TAP, true); + + // Simulate an ACTION_UP event. + long event4Time = 1003; + MotionEvent event4 = upEvent(event3Time, event4Time, 21f, 31f); + event4.setSource(InputDevice.SOURCE_MOUSE); + mTouchState.update(event4, mConfig); + assertTap(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); + event5.setSource(InputDevice.SOURCE_MOUSE); + mTouchState.update(event5, mConfig); + assertTap(mTouchState, 22f, 32f, 21f, 31f, + MultiTapStatus.TRIPLE_CLICK, true); + } + + @Test + public void testUpdate_tripleClick_touch() 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); + + // 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); + + // 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, + 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, + 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); + } + + private static MotionEvent downEvent(long downTime, long eventTime, float x, float y) { + return MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_DOWN, x, y, 0); + } + + private static MotionEvent upEvent(long downTime, long eventTime, float x, float y) { + return MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_UP, x, y, 0); + } + + private static void assertSingleTap(EditorTouchState touchState, float lastDownX, + float lastDownY, float lastUpX, float lastUpY) { + assertThat(touchState.getLastDownX(), is(lastDownX)); + assertThat(touchState.getLastDownY(), is(lastDownY)); + assertThat(touchState.getLastUpX(), is(lastUpX)); + assertThat(touchState.getLastUpY(), is(lastUpY)); + assertThat(touchState.isDoubleTap(), is(false)); + assertThat(touchState.isTripleClick(), is(false)); + assertThat(touchState.isMultiTap(), is(false)); + assertThat(touchState.isMultiTapInSameArea(), is(false)); + } + + private static void assertTap(EditorTouchState touchState, + float lastDownX, float lastDownY, float lastUpX, float lastUpY, + @MultiTapStatus int multiTapStatus, boolean isMultiTapInSameArea) { + assertThat(touchState.getLastDownX(), is(lastDownX)); + assertThat(touchState.getLastDownY(), is(lastDownY)); + assertThat(touchState.getLastUpX(), is(lastUpX)); + assertThat(touchState.getLastUpY(), is(lastUpY)); + assertThat(touchState.isDoubleTap(), is(multiTapStatus == MultiTapStatus.DOUBLE_TAP)); + assertThat(touchState.isTripleClick(), is(multiTapStatus == MultiTapStatus.TRIPLE_CLICK)); + assertThat(touchState.isMultiTap(), is(multiTapStatus == MultiTapStatus.DOUBLE_TAP + || multiTapStatus == MultiTapStatus.TRIPLE_CLICK)); + assertThat(touchState.isMultiTapInSameArea(), is(isMultiTapInSameArea)); + } +} |