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 float mInitialDragDirectionXYRatio; 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 101 /** 102 * When {@link #isMovedEnoughForDrag()} is {@code true}, this function returns the x/y ratio for 103 * the initial drag direction. Smaller values indicate that the direction is closer to vertical, 104 * while larger values indicate that the direction is closer to horizontal. For example: 105 * <ul> 106 * <li>if the drag direction is exactly vertical, this returns 0 107 * <li>if the drag direction is exactly horizontal, this returns {@link Float#MAX_VALUE} 108 * <li>if the drag direction is 45 deg from vertical, this returns 1 109 * <li>if the drag direction is 30 deg from vertical, this returns 0.58 (x delta is smaller 110 * than y delta) 111 * <li>if the drag direction is 60 deg from vertical, this returns 1.73 (x delta is bigger 112 * than y delta) 113 * </ul> 114 * This function never returns negative values, regardless of the direction of the drag. 115 */ getInitialDragDirectionXYRatio()116 public float getInitialDragDirectionXYRatio() { 117 return mInitialDragDirectionXYRatio; 118 } 119 setIsOnHandle(boolean onHandle)120 public void setIsOnHandle(boolean onHandle) { 121 mIsOnHandle = onHandle; 122 } 123 isOnHandle()124 public boolean isOnHandle() { 125 return mIsOnHandle; 126 } 127 128 /** 129 * Updates the state based on the new event. 130 */ update(MotionEvent event, ViewConfiguration config)131 public void update(MotionEvent event, ViewConfiguration config) { 132 final int action = event.getActionMasked(); 133 if (action == MotionEvent.ACTION_DOWN) { 134 final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE); 135 136 // We check both the time between the last up and current down event, as well as the 137 // time between the first down and up events. The latter check is necessary to handle 138 // the case when the user taps, drags/holds for some time, and then lifts up and 139 // quickly taps in the same area. This scenario should not be treated as a double-tap. 140 // This follows the behavior in GestureDetector. 141 final long millisSinceLastUp = event.getEventTime() - mLastUpMillis; 142 final long millisBetweenLastDownAndLastUp = mLastUpMillis - mLastDownMillis; 143 144 // Detect double tap and triple click. 145 if (millisSinceLastUp <= ViewConfiguration.getDoubleTapTimeout() 146 && millisBetweenLastDownAndLastUp <= ViewConfiguration.getDoubleTapTimeout() 147 && (mMultiTapStatus == MultiTapStatus.FIRST_TAP 148 || (mMultiTapStatus == MultiTapStatus.DOUBLE_TAP && isMouse))) { 149 if (mMultiTapStatus == MultiTapStatus.FIRST_TAP) { 150 mMultiTapStatus = MultiTapStatus.DOUBLE_TAP; 151 } else { 152 mMultiTapStatus = MultiTapStatus.TRIPLE_CLICK; 153 } 154 mMultiTapInSameArea = isDistanceWithin(mLastDownX, mLastDownY, 155 event.getX(), event.getY(), config.getScaledDoubleTapSlop()); 156 if (TextView.DEBUG_CURSOR) { 157 String status = isDoubleTap() ? "double" : "triple"; 158 String inSameArea = mMultiTapInSameArea ? "in same area" : "not in same area"; 159 logCursor("EditorTouchState", "ACTION_DOWN: %s tap detected, %s", 160 status, inSameArea); 161 } 162 } else { 163 mMultiTapStatus = MultiTapStatus.FIRST_TAP; 164 mMultiTapInSameArea = false; 165 if (TextView.DEBUG_CURSOR) { 166 logCursor("EditorTouchState", "ACTION_DOWN: first tap detected"); 167 } 168 } 169 mLastDownX = event.getX(); 170 mLastDownY = event.getY(); 171 mLastDownMillis = event.getEventTime(); 172 mMovedEnoughForDrag = false; 173 mInitialDragDirectionXYRatio = 0.0f; 174 } else if (action == MotionEvent.ACTION_UP) { 175 if (TextView.DEBUG_CURSOR) { 176 logCursor("EditorTouchState", "ACTION_UP"); 177 } 178 mLastUpX = event.getX(); 179 mLastUpY = event.getY(); 180 mLastUpMillis = event.getEventTime(); 181 mMovedEnoughForDrag = false; 182 mInitialDragDirectionXYRatio = 0.0f; 183 } else if (action == MotionEvent.ACTION_MOVE) { 184 if (!mMovedEnoughForDrag) { 185 float deltaX = event.getX() - mLastDownX; 186 float deltaY = event.getY() - mLastDownY; 187 float deltaXSquared = deltaX * deltaX; 188 float distanceSquared = (deltaXSquared) + (deltaY * deltaY); 189 int touchSlop = config.getScaledTouchSlop(); 190 mMovedEnoughForDrag = distanceSquared > touchSlop * touchSlop; 191 if (mMovedEnoughForDrag) { 192 mInitialDragDirectionXYRatio = (deltaY == 0) ? Float.MAX_VALUE : 193 Math.abs(deltaX / deltaY); 194 } 195 } 196 } else if (action == MotionEvent.ACTION_CANCEL) { 197 mLastDownMillis = 0; 198 mLastUpMillis = 0; 199 mMultiTapStatus = MultiTapStatus.NONE; 200 mMultiTapInSameArea = false; 201 mMovedEnoughForDrag = false; 202 mInitialDragDirectionXYRatio = 0.0f; 203 } 204 } 205 206 /** 207 * Returns true if the distance between the given coordinates is <= to the specified max. 208 * This is useful to be able to determine e.g. when the user's touch has moved enough in 209 * order to be considered a drag (no longer within touch slop). 210 */ isDistanceWithin(float x1, float y1, float x2, float y2, int maxDistance)211 public static boolean isDistanceWithin(float x1, float y1, float x2, float y2, 212 int maxDistance) { 213 float deltaX = x2 - x1; 214 float deltaY = y2 - y1; 215 float distanceSquared = (deltaX * deltaX) + (deltaY * deltaY); 216 return distanceSquared <= maxDistance * maxDistance; 217 } 218 219 /** 220 * Returns the x/y ratio corresponding to the given angle relative to vertical. Smaller angle 221 * values (ie, closer to vertical) will result in a smaller x/y ratio. For example: 222 * <ul> 223 * <li>if the angle is 45 deg, the ratio is 1 224 * <li>if the angle is 30 deg, the ratio is 0.58 (x delta is smaller than y delta) 225 * <li>if the angle is 60 deg, the ratio is 1.73 (x delta is bigger than y delta) 226 * </ul> 227 * If the passed-in value is <= 0, this function returns 0. If the passed-in value is >= 90, 228 * this function returns {@link Float#MAX_VALUE}. 229 * 230 * @see #getInitialDragDirectionXYRatio() 231 */ getXYRatio(int angleFromVerticalInDegrees)232 public static float getXYRatio(int angleFromVerticalInDegrees) { 233 if (angleFromVerticalInDegrees <= 0) { 234 return 0.0f; 235 } 236 if (angleFromVerticalInDegrees >= 90) { 237 return Float.MAX_VALUE; 238 } 239 return (float) Math.tan(Math.toRadians(angleFromVerticalInDegrees)); 240 } 241 } 242