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