blob: 3798d0095f2d23ab20df0f8d21f18944bb545c11 [file] [log] [blame]
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -08001/*
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
17package android.widget;
18
19import static android.widget.Editor.logCursor;
20
21import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
22
23import android.annotation.IntDef;
24import android.view.InputDevice;
25import android.view.MotionEvent;
26import android.view.ViewConfiguration;
27
28import com.android.internal.annotations.VisibleForTesting;
29
30import java.lang.annotation.Retention;
31import java.lang.annotation.RetentionPolicy;
32
33/**
34 * Helper class used by {@link Editor} to track state for touch events.
35 *
36 * @hide
37 */
38@VisibleForTesting(visibility = PACKAGE)
39public class EditorTouchState {
40 private float mLastDownX, mLastDownY;
41 private float mLastUpX, mLastUpY;
42 private long mLastUpMillis;
43
44 @IntDef({MultiTapStatus.NONE, MultiTapStatus.FIRST_TAP, MultiTapStatus.DOUBLE_TAP,
45 MultiTapStatus.TRIPLE_CLICK})
46 @Retention(RetentionPolicy.SOURCE)
47 @VisibleForTesting
48 public @interface MultiTapStatus {
49 int NONE = 0;
50 int FIRST_TAP = 1;
51 int DOUBLE_TAP = 2;
52 int TRIPLE_CLICK = 3; // Only for mouse input.
53 }
54 @MultiTapStatus
55 private int mMultiTapStatus = MultiTapStatus.NONE;
56 private boolean mMultiTapInSameArea;
57
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -080058 private boolean mMovedEnoughForDrag;
59
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -080060 public float getLastDownX() {
61 return mLastDownX;
62 }
63
64 public float getLastDownY() {
65 return mLastDownY;
66 }
67
68 public float getLastUpX() {
69 return mLastUpX;
70 }
71
72 public float getLastUpY() {
73 return mLastUpY;
74 }
75
76 public boolean isDoubleTap() {
77 return mMultiTapStatus == MultiTapStatus.DOUBLE_TAP;
78 }
79
80 public boolean isTripleClick() {
81 return mMultiTapStatus == MultiTapStatus.TRIPLE_CLICK;
82 }
83
84 public boolean isMultiTap() {
85 return mMultiTapStatus == MultiTapStatus.DOUBLE_TAP
86 || mMultiTapStatus == MultiTapStatus.TRIPLE_CLICK;
87 }
88
89 public boolean isMultiTapInSameArea() {
90 return isMultiTap() && mMultiTapInSameArea;
91 }
92
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -080093 public boolean isMovedEnoughForDrag() {
94 return mMovedEnoughForDrag;
95 }
96
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -080097 /**
98 * Updates the state based on the new event.
99 */
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -0800100 public void update(MotionEvent event, ViewConfiguration config) {
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -0800101 final int action = event.getActionMasked();
102 if (action == MotionEvent.ACTION_DOWN) {
103 final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
104 final long millisSinceLastUp = event.getEventTime() - mLastUpMillis;
105 // Detect double tap and triple click.
106 if (millisSinceLastUp <= ViewConfiguration.getDoubleTapTimeout()
107 && (mMultiTapStatus == MultiTapStatus.FIRST_TAP
108 || (mMultiTapStatus == MultiTapStatus.DOUBLE_TAP && isMouse))) {
109 if (mMultiTapStatus == MultiTapStatus.FIRST_TAP) {
110 mMultiTapStatus = MultiTapStatus.DOUBLE_TAP;
111 } else {
112 mMultiTapStatus = MultiTapStatus.TRIPLE_CLICK;
113 }
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -0800114 mMultiTapInSameArea = isDistanceWithin(mLastDownX, mLastDownY,
115 event.getX(), event.getY(), config.getScaledDoubleTapSlop());
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -0800116 if (TextView.DEBUG_CURSOR) {
117 String status = isDoubleTap() ? "double" : "triple";
118 String inSameArea = mMultiTapInSameArea ? "in same area" : "not in same area";
119 logCursor("EditorTouchState", "ACTION_DOWN: %s tap detected, %s",
120 status, inSameArea);
121 }
122 } else {
123 mMultiTapStatus = MultiTapStatus.FIRST_TAP;
124 mMultiTapInSameArea = false;
125 if (TextView.DEBUG_CURSOR) {
126 logCursor("EditorTouchState", "ACTION_DOWN: first tap detected");
127 }
128 }
129 mLastDownX = event.getX();
130 mLastDownY = event.getY();
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -0800131 mMovedEnoughForDrag = false;
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -0800132 } else if (action == MotionEvent.ACTION_UP) {
133 if (TextView.DEBUG_CURSOR) {
134 logCursor("EditorTouchState", "ACTION_UP");
135 }
136 mLastUpX = event.getX();
137 mLastUpY = event.getY();
138 mLastUpMillis = event.getEventTime();
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -0800139 mMovedEnoughForDrag = false;
140 } else if (action == MotionEvent.ACTION_MOVE) {
141 mMovedEnoughForDrag = !isDistanceWithin(mLastDownX, mLastDownY,
142 event.getX(), event.getY(), config.getScaledTouchSlop());
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -0800143 }
144 }
Nikita Dubrovsky9a1369b2019-12-06 09:25:20 -0800145
146 /**
147 * Returns true if the distance between the given coordinates is <= to the specified max.
148 * This is useful to be able to determine e.g. when the user's touch has moved enough in
149 * order to be considered a drag (no longer within touch slop).
150 */
151 public static boolean isDistanceWithin(float x1, float y1, float x2, float y2,
152 int maxDistance) {
153 float deltaX = x2 - x1;
154 float deltaY = y2 - y1;
155 float distanceSquared = (deltaX * deltaX) + (deltaY * deltaY);
156 return distanceSquared <= maxDistance * maxDistance;
157 }
Nikita Dubrovskyd63f2032019-11-20 14:33:21 -0800158}