1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.widget; 18 19 import static android.widget.Editor.logCursor; 20 21 import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; 22 23 import android.annotation.IntDef; 24 import android.view.InputDevice; 25 import android.view.MotionEvent; 26 import android.view.ViewConfiguration; 27 28 import com.android.internal.annotations.VisibleForTesting; 29 30 import java.lang.annotation.Retention; 31 import java.lang.annotation.RetentionPolicy; 32 33 /** 34 * Helper class used by {@link Editor} to track state for touch events. Ideally the logic here 35 * should be replaced with {@link android.view.GestureDetector}. 36 * 37 * @hide 38 */ 39 @VisibleForTesting(visibility = PACKAGE) 40 public class EditorTouchState { 41 private float mLastDownX, mLastDownY; 42 private long mLastDownMillis; 43 private float mLastUpX, mLastUpY; 44 private long mLastUpMillis; 45 private boolean mIsOnHandle; 46 47 @IntDef({MultiTapStatus.NONE, MultiTapStatus.FIRST_TAP, MultiTapStatus.DOUBLE_TAP, 48 MultiTapStatus.TRIPLE_CLICK}) 49 @Retention(RetentionPolicy.SOURCE) 50 @VisibleForTesting 51 public @interface MultiTapStatus { 52 int NONE = 0; 53 int FIRST_TAP = 1; 54 int DOUBLE_TAP = 2; 55 int TRIPLE_CLICK = 3; // Only for mouse input. 56 } 57 @MultiTapStatus 58 private int mMultiTapStatus = MultiTapStatus.NONE; 59 private boolean mMultiTapInSameArea; 60 61 private boolean mMovedEnoughForDrag; 62 private boolean mIsDragCloseToVertical; 63 getLastDownX()64 public float getLastDownX() { 65 return mLastDownX; 66 } 67 getLastDownY()68 public float getLastDownY() { 69 return mLastDownY; 70 } 71 getLastUpX()72 public float getLastUpX() { 73 return mLastUpX; 74 } 75 getLastUpY()76 public float getLastUpY() { 77 return mLastUpY; 78 } 79 isDoubleTap()80 public boolean isDoubleTap() { 81 return mMultiTapStatus == MultiTapStatus.DOUBLE_TAP; 82 } 83 isTripleClick()84 public boolean isTripleClick() { 85 return mMultiTapStatus == MultiTapStatus.TRIPLE_CLICK; 86 } 87 isMultiTap()88 public boolean isMultiTap() { 89 return mMultiTapStatus == MultiTapStatus.DOUBLE_TAP 90 || mMultiTapStatus == MultiTapStatus.TRIPLE_CLICK; 91 } 92 isMultiTapInSameArea()93 public boolean isMultiTapInSameArea() { 94 return isMultiTap() && mMultiTapInSameArea; 95 } 96 isMovedEnoughForDrag()97 public boolean isMovedEnoughForDrag() { 98 return mMovedEnoughForDrag; 99 } 100 isDragCloseToVertical()101 public boolean isDragCloseToVertical() { 102 return mIsDragCloseToVertical && !mIsOnHandle; 103 } 104 setIsOnHandle(boolean onHandle)105 public void setIsOnHandle(boolean onHandle) { 106 mIsOnHandle = onHandle; 107 } 108 isOnHandle()109 public boolean isOnHandle() { 110 return mIsOnHandle; 111 } 112 113 /** 114 * Updates the state based on the new event. 115 */ update(MotionEvent event, ViewConfiguration config)116 public void update(MotionEvent event, ViewConfiguration config) { 117 final int action = event.getActionMasked(); 118 if (action == MotionEvent.ACTION_DOWN) { 119 final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE); 120 121 // We check both the time between the last up and current down event, as well as the 122 // time between the first down and up events. The latter check is necessary to handle 123 // the case when the user taps, drags/holds for some time, and then lifts up and 124 // quickly taps in the same area. This scenario should not be treated as a double-tap. 125 // This follows the behavior in GestureDetector. 126 final long millisSinceLastUp = event.getEventTime() - mLastUpMillis; 127 final long millisBetweenLastDownAndLastUp = mLastUpMillis - mLastDownMillis; 128 129 // Detect double tap and triple click. 130 if (millisSinceLastUp <= ViewConfiguration.getDoubleTapTimeout() 131 && millisBetweenLastDownAndLastUp <= ViewConfiguration.getDoubleTapTimeout() 132 && (mMultiTapStatus == MultiTapStatus.FIRST_TAP 133 || (mMultiTapStatus == MultiTapStatus.DOUBLE_TAP && isMouse))) { 134 if (mMultiTapStatus == MultiTapStatus.FIRST_TAP) { 135 mMultiTapStatus = MultiTapStatus.DOUBLE_TAP; 136 } else { 137 mMultiTapStatus = MultiTapStatus.TRIPLE_CLICK; 138 } 139 mMultiTapInSameArea = isDistanceWithin(mLastDownX, mLastDownY, 140 event.getX(), event.getY(), config.getScaledDoubleTapSlop()); 141 if (TextView.DEBUG_CURSOR) { 142 String status = isDoubleTap() ? "double" : "triple"; 143 String inSameArea = mMultiTapInSameArea ? "in same area" : "not in same area"; 144 logCursor("EditorTouchState", "ACTION_DOWN: %s tap detected, %s", 145 status, inSameArea); 146 } 147 } else { 148 mMultiTapStatus = MultiTapStatus.FIRST_TAP; 149 mMultiTapInSameArea = false; 150 if (TextView.DEBUG_CURSOR) { 151 logCursor("EditorTouchState", "ACTION_DOWN: first tap detected"); 152 } 153 } 154 mLastDownX = event.getX(); 155 mLastDownY = event.getY(); 156 mLastDownMillis = event.getEventTime(); 157 mMovedEnoughForDrag = false; 158 mIsDragCloseToVertical = false; 159 } else if (action == MotionEvent.ACTION_UP) { 160 if (TextView.DEBUG_CURSOR) { 161 logCursor("EditorTouchState", "ACTION_UP"); 162 } 163 mLastUpX = event.getX(); 164 mLastUpY = event.getY(); 165 mLastUpMillis = event.getEventTime(); 166 mMovedEnoughForDrag = false; 167 mIsDragCloseToVertical = false; 168 } else if (action == MotionEvent.ACTION_MOVE) { 169 if (!mMovedEnoughForDrag) { 170 float deltaX = event.getX() - mLastDownX; 171 float deltaY = event.getY() - mLastDownY; 172 float deltaXSquared = deltaX * deltaX; 173 float distanceSquared = (deltaXSquared) + (deltaY * deltaY); 174 int touchSlop = config.getScaledTouchSlop(); 175 mMovedEnoughForDrag = distanceSquared > touchSlop * touchSlop; 176 if (mMovedEnoughForDrag) { 177 // If the direction of the swipe motion is within 45 degrees of vertical, it is 178 // considered a vertical drag. 179 mIsDragCloseToVertical = Math.abs(deltaX) <= Math.abs(deltaY); 180 } 181 } 182 } else if (action == MotionEvent.ACTION_CANCEL) { 183 mLastDownMillis = 0; 184 mLastUpMillis = 0; 185 mMultiTapStatus = MultiTapStatus.NONE; 186 mMultiTapInSameArea = false; 187 mMovedEnoughForDrag = false; 188 mIsDragCloseToVertical = false; 189 } 190 } 191 192 /** 193 * Returns true if the distance between the given coordinates is <= to the specified max. 194 * This is useful to be able to determine e.g. when the user's touch has moved enough in 195 * order to be considered a drag (no longer within touch slop). 196 */ isDistanceWithin(float x1, float y1, float x2, float y2, int maxDistance)197 public static boolean isDistanceWithin(float x1, float y1, float x2, float y2, 198 int maxDistance) { 199 float deltaX = x2 - x1; 200 float deltaY = y2 - y1; 201 float distanceSquared = (deltaX * deltaX) + (deltaY * deltaY); 202 return distanceSquared <= maxDistance * maxDistance; 203 } 204 } 205