• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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