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