blob: 8b82c06cee233e7cc337099d3b83edb2e59716fa [file] [log] [blame]
Alan Viverette3da604b2020-06-10 18:34:39 +00001/*
2 * Copyright (C) 2015 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 android.os.SystemClock;
20import android.view.MotionEvent;
21import android.view.View;
22import android.view.ViewConfiguration;
23import android.view.ViewParent;
24
25import com.android.internal.view.menu.ShowableListMenu;
26
27/**
28 * Abstract class that forwards touch events to a {@link ShowableListMenu}.
29 *
30 * @hide
31 */
32public abstract class ForwardingListener
33 implements View.OnTouchListener, View.OnAttachStateChangeListener {
34
35 /** Scaled touch slop, used for detecting movement outside bounds. */
36 private final float mScaledTouchSlop;
37
38 /** Timeout before disallowing intercept on the source's parent. */
39 private final int mTapTimeout;
40
41 /** Timeout before accepting a long-press to start forwarding. */
42 private final int mLongPressTimeout;
43
44 /** Source view from which events are forwarded. */
45 private final View mSrc;
46
47 /** Runnable used to prevent conflicts with scrolling parents. */
48 private Runnable mDisallowIntercept;
49
50 /** Runnable used to trigger forwarding on long-press. */
51 private Runnable mTriggerLongPress;
52
53 /** Whether this listener is currently forwarding touch events. */
54 private boolean mForwarding;
55
56 /** The id of the first pointer down in the current event stream. */
57 private int mActivePointerId;
58
59 public ForwardingListener(View src) {
60 mSrc = src;
61 src.setLongClickable(true);
62 src.addOnAttachStateChangeListener(this);
63
64 mScaledTouchSlop = ViewConfiguration.get(src.getContext()).getScaledTouchSlop();
65 mTapTimeout = ViewConfiguration.getTapTimeout();
66
67 // Use a medium-press timeout. Halfway between tap and long-press.
68 mLongPressTimeout = (mTapTimeout + ViewConfiguration.getLongPressTimeout()) / 2;
69 }
70
71 /**
72 * Returns the popup to which this listener is forwarding events.
73 * <p>
74 * Override this to return the correct popup. If the popup is displayed
75 * asynchronously, you may also need to override
76 * {@link #onForwardingStopped} to prevent premature cancellation of
77 * forwarding.
78 *
79 * @return the popup to which this listener is forwarding events
80 */
81 public abstract ShowableListMenu getPopup();
82
83 @Override
84 public boolean onTouch(View v, MotionEvent event) {
85 final boolean wasForwarding = mForwarding;
86 final boolean forwarding;
87 if (wasForwarding) {
88 forwarding = onTouchForwarded(event) || !onForwardingStopped();
89 } else {
90 forwarding = onTouchObserved(event) && onForwardingStarted();
91
92 if (forwarding) {
93 // Make sure we cancel any ongoing source event stream.
94 final long now = SystemClock.uptimeMillis();
95 final MotionEvent e = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL,
96 0.0f, 0.0f, 0);
97 mSrc.onTouchEvent(e);
98 e.recycle();
99 }
100 }
101
102 mForwarding = forwarding;
103 return forwarding || wasForwarding;
104 }
105
106 @Override
107 public void onViewAttachedToWindow(View v) {
108 }
109
110 @Override
111 public void onViewDetachedFromWindow(View v) {
112 mForwarding = false;
113 mActivePointerId = MotionEvent.INVALID_POINTER_ID;
114
115 if (mDisallowIntercept != null) {
116 mSrc.removeCallbacks(mDisallowIntercept);
117 }
118 }
119
120 /**
121 * Called when forwarding would like to start.
122 * <p>
123 * By default, this will show the popup returned by {@link #getPopup()}.
124 * It may be overridden to perform another action, like clicking the
125 * source view or preparing the popup before showing it.
126 *
127 * @return true to start forwarding, false otherwise
128 */
129 protected boolean onForwardingStarted() {
130 final ShowableListMenu popup = getPopup();
131 if (popup != null && !popup.isShowing()) {
132 popup.show();
133 }
134 return true;
135 }
136
137 /**
138 * Called when forwarding would like to stop.
139 * <p>
140 * By default, this will dismiss the popup returned by
141 * {@link #getPopup()}. It may be overridden to perform some other
142 * action.
143 *
144 * @return true to stop forwarding, false otherwise
145 */
146 protected boolean onForwardingStopped() {
147 final ShowableListMenu popup = getPopup();
148 if (popup != null && popup.isShowing()) {
149 popup.dismiss();
150 }
151 return true;
152 }
153
154 /**
155 * Observes motion events and determines when to start forwarding.
156 *
157 * @param srcEvent motion event in source view coordinates
158 * @return true to start forwarding motion events, false otherwise
159 */
160 private boolean onTouchObserved(MotionEvent srcEvent) {
161 final View src = mSrc;
162 if (!src.isEnabled()) {
163 return false;
164 }
165
166 final int actionMasked = srcEvent.getActionMasked();
167 switch (actionMasked) {
168 case MotionEvent.ACTION_DOWN:
169 mActivePointerId = srcEvent.getPointerId(0);
170
171 if (mDisallowIntercept == null) {
172 mDisallowIntercept = new DisallowIntercept();
173 }
174 src.postDelayed(mDisallowIntercept, mTapTimeout);
175
176 if (mTriggerLongPress == null) {
177 mTriggerLongPress = new TriggerLongPress();
178 }
179 src.postDelayed(mTriggerLongPress, mLongPressTimeout);
180 break;
181 case MotionEvent.ACTION_MOVE:
182 final int activePointerIndex = srcEvent.findPointerIndex(mActivePointerId);
183 if (activePointerIndex >= 0) {
184 final float x = srcEvent.getX(activePointerIndex);
185 final float y = srcEvent.getY(activePointerIndex);
186
187 // Has the pointer moved outside of the view?
188 if (!src.pointInView(x, y, mScaledTouchSlop)) {
189 clearCallbacks();
190
191 // Don't let the parent intercept our events.
192 src.getParent().requestDisallowInterceptTouchEvent(true);
193 return true;
194 }
195 }
196 break;
197 case MotionEvent.ACTION_CANCEL:
198 case MotionEvent.ACTION_UP:
199 clearCallbacks();
200 break;
201 }
202
203 return false;
204 }
205
206 private void clearCallbacks() {
207 if (mTriggerLongPress != null) {
208 mSrc.removeCallbacks(mTriggerLongPress);
209 }
210
211 if (mDisallowIntercept != null) {
212 mSrc.removeCallbacks(mDisallowIntercept);
213 }
214 }
215
216 private void onLongPress() {
217 clearCallbacks();
218
219 final View src = mSrc;
220 if (!src.isEnabled() || src.isLongClickable()) {
221 // Ignore long-press if the view is disabled or has its own
222 // handler.
223 return;
224 }
225
226 if (!onForwardingStarted()) {
227 return;
228 }
229
230 // Don't let the parent intercept our events.
231 src.getParent().requestDisallowInterceptTouchEvent(true);
232
233 // Make sure we cancel any ongoing source event stream.
234 final long now = SystemClock.uptimeMillis();
235 final MotionEvent e = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0);
236 src.onTouchEvent(e);
237 e.recycle();
238
239 mForwarding = true;
240 }
241
242 /**
243 * Handles forwarded motion events and determines when to stop
244 * forwarding.
245 *
246 * @param srcEvent motion event in source view coordinates
247 * @return true to continue forwarding motion events, false to cancel
248 */
249 private boolean onTouchForwarded(MotionEvent srcEvent) {
250 final View src = mSrc;
251 final ShowableListMenu popup = getPopup();
252 if (popup == null || !popup.isShowing()) {
253 return false;
254 }
255
256 final DropDownListView dst = (DropDownListView) popup.getListView();
257 if (dst == null || !dst.isShown()) {
258 return false;
259 }
260
261 // Convert event to destination-local coordinates.
262 final MotionEvent dstEvent = MotionEvent.obtainNoHistory(srcEvent);
263 src.toGlobalMotionEvent(dstEvent);
264 dst.toLocalMotionEvent(dstEvent);
265
266 // Forward converted event to destination view, then recycle it.
267 final boolean handled = dst.onForwardedEvent(dstEvent, mActivePointerId);
268 dstEvent.recycle();
269
270 // Always cancel forwarding when the touch stream ends.
271 final int action = srcEvent.getActionMasked();
272 final boolean keepForwarding = action != MotionEvent.ACTION_UP
273 && action != MotionEvent.ACTION_CANCEL;
274
275 return handled && keepForwarding;
276 }
277
278 private class DisallowIntercept implements Runnable {
279 @Override
280 public void run() {
281 final ViewParent parent = mSrc.getParent();
282 if (parent != null) {
283 parent.requestDisallowInterceptTouchEvent(true);
284 }
285 }
286 }
287
288 private class TriggerLongPress implements Runnable {
289 @Override
290 public void run() {
291 onLongPress();
292 }
293 }
294}