1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.chrome.browser.compositor.layouts.eventfilter;
6 
7 import android.content.Context;
8 import android.view.GestureDetector;
9 import android.view.MotionEvent;
10 import android.view.ViewConfiguration;
11 import android.view.ViewGroup;
12 
13 import androidx.annotation.IntDef;
14 import androidx.annotation.VisibleForTesting;
15 
16 import org.chromium.chrome.browser.compositor.bottombar.OverlayPanel;
17 import org.chromium.chrome.browser.compositor.bottombar.OverlayPanel.PanelState;
18 import org.chromium.chrome.browser.compositor.bottombar.OverlayPanelManager;
19 import org.chromium.chrome.browser.contextualsearch.SwipeRecognizer;
20 import org.chromium.content_public.browser.WebContents;
21 
22 import java.lang.annotation.Retention;
23 import java.lang.annotation.RetentionPolicy;
24 import java.util.ArrayList;
25 
26 /**
27  * The {@link GestureEventFilter} used when an overlay panel is being shown. It filters
28  * events that happen in the Content View area and propagates them to the appropriate
29  * WebContents.
30  */
31 public class OverlayPanelEventFilter extends GestureEventFilter {
32     /**
33      * The targets that can handle MotionEvents.
34      */
35     @IntDef({EventTarget.UNDETERMINED, EventTarget.PANEL, EventTarget.CONTENT_VIEW})
36     @Retention(RetentionPolicy.SOURCE)
37     private @interface EventTarget {
38         int UNDETERMINED = 0;
39         int PANEL = 1;
40         int CONTENT_VIEW = 2;
41     }
42 
43     /**
44      * The direction of the gesture.
45      */
46     @IntDef({GestureOrientation.UNDETERMINED, GestureOrientation.HORIZONTAL,
47             GestureOrientation.VERTICAL})
48     @Retention(RetentionPolicy.SOURCE)
49     private @interface GestureOrientation {
50         int UNDETERMINED = 0;
51         int HORIZONTAL = 1;
52         int VERTICAL = 2;
53     }
54 
55     /**
56      * The boost factor that can be applied to prioritize vertical movements over horizontal ones.
57      */
58     private static final float VERTICAL_DETERMINATION_BOOST = 1.25f;
59 
60     /** The OverlayPanel that this filter is for. */
61     private final OverlayPanel mPanel;
62 
63     /** The {@link GestureDetector} used to distinguish tap and scroll gestures. */
64     private final GestureDetector mGestureDetector;
65 
66     /** The @{link SwipeRecognizer} that recognizes directional swipe gestures. */
67     private final SwipeRecognizer mSwipeRecognizer;
68 
69     /**
70      * The square of ViewConfiguration.getScaledTouchSlop() in pixels used to calculate whether
71      * the finger has moved beyond the established threshold.
72      */
73     private final float mTouchSlopSquarePx;
74 
75     /** The target to propagate events to. */
76     private @EventTarget int mEventTarget;
77 
78     /** Whether the code is in the middle of the process of determining the event target. */
79     private boolean mIsDeterminingEventTarget;
80 
81     /** Whether the event target has been determined. */
82     private boolean mHasDeterminedEventTarget;
83 
84     /** The previous target the events were propagated to. */
85     private @EventTarget int mPreviousEventTarget;
86 
87     /** Whether the event target has changed since the last touch event. */
88     private boolean mHasChangedEventTarget;
89 
90     /**
91      * Whether the event target might change. This will be true in cases we know the overscroll
92      * and/or underscroll might happen, which means we'll have to constantly monitor the event
93      * targets in order to determine the exact moment the target has changed.
94      */
95     private boolean mMayChangeEventTarget;
96 
97     /** Whether the gesture orientation has been determined. */
98     private boolean mHasDeterminedGestureOrientation;
99 
100     /** The current gesture orientation. */
101     private @GestureOrientation int mGestureOrientation;
102 
103     /** Whether the events are being recorded. */
104     private boolean mIsRecordingEvents;
105 
106     /** Whether the ACTION_DOWN that initiated the MotionEvent's stream was synthetic. */
107     private boolean mWasActionDownEventSynthetic;
108 
109     /** The X coordinate of the synthetic ACTION_DOWN MotionEvent. */
110     private float mSyntheticActionDownX;
111 
112     /** The Y coordinate of the synthetic ACTION_DOWN MotionEvent. */
113     private float mSyntheticActionDownY;
114 
115     /** The list of recorded events. */
116     private final ArrayList<MotionEvent> mRecordedEvents = new ArrayList<MotionEvent>();
117 
118     /** The initial Y position of the current gesture. */
119     private float mInitialEventY;
120 
121     /** Whether or not the superclass has seen a down event. */
122     private boolean mFilterHadDownEvent;
123 
124     private class SwipeRecognizerImpl extends SwipeRecognizer {
SwipeRecognizerImpl(Context context)125         public SwipeRecognizerImpl(Context context) {
126             super(context);
127             setSwipeHandler(mPanel);
128         }
129 
130         @Override
onSingleTapUp(MotionEvent event)131         public boolean onSingleTapUp(MotionEvent event) {
132             mPanel.handleClick(event.getX() * mPxToDp, event.getY() * mPxToDp);
133             return true;
134         }
135     }
136 
137     /**
138      * Creates a {@link GestureEventFilter} with offset touch events.
139      * @param context The {@link Context} for Android.
140      * @param panelManager The {@link OverlayPanelManager} responsible for showing panels.
141      */
OverlayPanelEventFilter(Context context, OverlayPanel panel)142     public OverlayPanelEventFilter(Context context, OverlayPanel panel) {
143         super(context, panel, false, false);
144 
145         mGestureDetector = new GestureDetector(context, new InternalGestureDetector());
146         mPanel = panel;
147 
148         mSwipeRecognizer = new SwipeRecognizerImpl(context);
149 
150         // Store the square of the platform touch slop in pixels to use in the scroll detection.
151         // See {@link OverlayPanelEventFilter#isDistanceGreaterThanTouchSlop}.
152         float touchSlopPx = ViewConfiguration.get(context).getScaledTouchSlop();
153         mTouchSlopSquarePx = touchSlopPx * touchSlopPx;
154 
155         reset();
156     }
157 
158     /**
159      * Gets the Content View's vertical scroll position. If the Content View
160      * is not available it returns -1.
161      * @return The Content View scroll position.
162      */
163     @VisibleForTesting
getContentViewVerticalScroll()164     protected float getContentViewVerticalScroll() {
165         return mPanel.getContentVerticalScroll();
166     }
167 
168     @Override
onInterceptTouchEventInternal(MotionEvent e, boolean isKeyboardShowing)169     public boolean onInterceptTouchEventInternal(MotionEvent e, boolean isKeyboardShowing) {
170         if (mPanel.isShowing()
171                 && (mPanel.isCoordinateInsideOverlayPanel(e.getX() * mPxToDp, e.getY() * mPxToDp)
172                 // When the Panel is opened, all events should be forwarded to the Panel,
173                 // even those who are not inside the Panel. This is to prevent any events
174                 // being forward to the base page when the Panel is expanded.
175                 || mPanel.isPanelOpened())) {
176             return super.onInterceptTouchEventInternal(e, isKeyboardShowing);
177         }
178 
179         // The event filter will have been recording events before the event target was
180         // determined. Clear this cache if the panel is not showing to prevent sending
181         // motion events that would start a target's stream with something other than
182         // ACTION_DOWN.
183         mRecordedEvents.clear();
184         reset();
185 
186         return false;
187     }
188 
189     @Override
onTouchEventInternal(MotionEvent e)190     public boolean onTouchEventInternal(MotionEvent e) {
191         final int action = e.getActionMasked();
192 
193         if (mPanel.getPanelState() == PanelState.PEEKED) {
194             if (action == MotionEvent.ACTION_DOWN) {
195                 // To avoid a gray flash of empty content, show the search content
196                 // view immediately on tap rather than waiting for panel expansion.
197                 // TODO(pedrosimonetti): Once we implement "side-swipe to dismiss"
198                 // we'll have to revisit this because we don't want to set the
199                 // Content View visibility to true when the side-swipe is detected.
200                 mPanel.notifyBarTouched(e.getX() * mPxToDp);
201             }
202             mSwipeRecognizer.onTouchEvent(e);
203             mGestureDetector.onTouchEvent(e);
204             return true;
205         }
206 
207         if (!mIsDeterminingEventTarget && action == MotionEvent.ACTION_DOWN) {
208             mInitialEventY = e.getY();
209             if (mPanel.isCoordinateInsideContent(e.getX() * mPxToDp, mInitialEventY * mPxToDp)) {
210                 // If the DOWN event happened inside the Content View, we'll need
211                 // to wait until the user has moved the finger beyond a certain threshold,
212                 // so we can determine the gesture's orientation and consequently be able
213                 // to tell if the Content View will accept the gesture.
214                 mIsDeterminingEventTarget = true;
215                 mMayChangeEventTarget = true;
216             } else {
217                 // If the DOWN event happened outside the Content View, then we know
218                 // that the Panel will start handling the event right away.
219                 setEventTarget(EventTarget.PANEL);
220                 mMayChangeEventTarget = false;
221             }
222         }
223 
224         // Send the event to the GestureDetector so we can distinguish between scroll and tap.
225         mGestureDetector.onTouchEvent(e);
226 
227         if (mHasDeterminedEventTarget) {
228             // If the event target has been determined, resume pending events, then propagate
229             // the current event to the appropriate target.
230             resumeAndPropagateEvent(e);
231         } else {
232             // If the event target has not been determined, we need to record a copy of the event
233             // until we are able to determine the event target.
234             MotionEvent event = MotionEvent.obtain(e);
235             mRecordedEvents.add(event);
236             mIsRecordingEvents = true;
237         }
238 
239         if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
240             reset();
241         }
242 
243         return true;
244     }
245 
246     /**
247      * Resets the current and previous {@link EventTarget} as well the {@link GestureOrientation}
248      * to the UNDETERMINED state.
249      */
reset()250     private void reset() {
251         mEventTarget = EventTarget.UNDETERMINED;
252         mIsDeterminingEventTarget = false;
253         mHasDeterminedEventTarget = false;
254 
255         mPreviousEventTarget = EventTarget.UNDETERMINED;
256         mHasChangedEventTarget = false;
257         mMayChangeEventTarget = false;
258 
259         mWasActionDownEventSynthetic = false;
260 
261         mGestureOrientation = GestureOrientation.UNDETERMINED;
262         mHasDeterminedGestureOrientation = false;
263     }
264 
265     /**
266      * Resumes pending events then propagates the given event to the current {@link EventTarget}.
267      *
268      * Resuming events might consist in simply propagating previously recorded events if the
269      * EventTarget was UNDETERMINED when the gesture started.
270      *
271      * For the case where the EventTarget has changed during the course of the gesture, we'll
272      * need to simulate a gesture end in the previous target (by simulating an ACTION_CANCEL
273      * event) and a gesture start in the new target (by simulating an ACTION_DOWN event).
274      *
275      * @param e The {@link MotionEvent} to be propagated after resuming the pending events.
276      */
resumeAndPropagateEvent(MotionEvent e)277     private void resumeAndPropagateEvent(MotionEvent e) {
278         if (mIsRecordingEvents) {
279             resumeRecordedEvents();
280         }
281 
282         if (mHasChangedEventTarget) {
283             // If the event target has changed since the beginning of the gesture, then we need
284             // to send a ACTION_CANCEL to the previous event target to make sure it no longer
285             // expects events.
286             propagateAndRecycleEvent(copyEvent(e, MotionEvent.ACTION_CANCEL), mPreviousEventTarget);
287 
288             // Similarly we need to send an ACTION_DOWN to the new event target so subsequent
289             // events can be analyzed properly by the Gesture Detector.
290             MotionEvent syntheticActionDownEvent = copyEvent(e, MotionEvent.ACTION_DOWN);
291 
292             // Store the synthetic ACTION_DOWN coordinates to prevent unwanted taps from
293             // happening. See {@link OverlayPanelEventFilter#propagateEventToContent}.
294             mWasActionDownEventSynthetic = true;
295             mSyntheticActionDownX = syntheticActionDownEvent.getX();
296             mSyntheticActionDownY =
297                     syntheticActionDownEvent.getY() - mPanel.getContentY() / mPxToDp;
298 
299             propagateAndRecycleEvent(syntheticActionDownEvent, mEventTarget);
300 
301             mHasChangedEventTarget = false;
302         }
303 
304         propagateEvent(e, mEventTarget);
305     }
306 
307     /**
308      * Resumes recorded events by propagating all of them to the current {@link EventTarget}.
309      */
resumeRecordedEvents()310     private void resumeRecordedEvents() {
311         for (int i = 0, size = mRecordedEvents.size(); i < size; i++) {
312             propagateAndRecycleEvent(mRecordedEvents.get(i), mEventTarget);
313         }
314 
315         mRecordedEvents.clear();
316         mIsRecordingEvents = false;
317     }
318 
319     /**
320      * Propagates the given {@link MotionEvent} to the given {@link EventTarget}, recycling it
321      * afterwards. This is intended for synthetic events only, those create by
322      * {@link MotionEvent#obtain} or the helper methods
323      * {@link OverlayPanelEventFilter#lockEventHorizontallty} and
324      * {@link OverlayPanelEventFilter#copyEvent}.
325      *
326      * @param e The {@link MotionEvent} to be propagated.
327      * @param target The {@link EventTarget} to propagate events to.
328      */
propagateAndRecycleEvent(MotionEvent e, @EventTarget int target)329     private void propagateAndRecycleEvent(MotionEvent e, @EventTarget int target) {
330         propagateEvent(e, target);
331         e.recycle();
332     }
333 
334     /**
335      * Propagates the given {@link MotionEvent} to the given {@link EventTarget}.
336      * @param e The {@link MotionEvent} to be propagated.
337      * @param target The {@link EventTarget} to propagate events to.
338      */
propagateEvent(MotionEvent e, @EventTarget int target)339     private void propagateEvent(MotionEvent e, @EventTarget int target) {
340         if (target == EventTarget.PANEL) {
341             // Make sure the internal gesture detector has seen at least on down event.
342             if (e.getActionMasked() == MotionEvent.ACTION_DOWN) mFilterHadDownEvent = true;
343             if (!mFilterHadDownEvent) {
344                 MotionEvent down = MotionEvent.obtain(e);
345                 down.setAction(MotionEvent.ACTION_DOWN);
346                 super.onTouchEventInternal(down);
347                 mFilterHadDownEvent = true;
348             }
349             super.onTouchEventInternal(e);
350         } else if (target == EventTarget.CONTENT_VIEW) {
351             propagateEventToContent(e);
352         }
353     }
354 
355     /**
356      * Propagates the given {@link MotionEvent} to the {@link WebContents}.
357      * @param e The {@link MotionEvent} to be propagated.
358      */
propagateEventToContent(MotionEvent e)359     protected void propagateEventToContent(MotionEvent e) {
360         MotionEvent event = e;
361         int action = event.getActionMasked();
362         boolean isSyntheticEvent = false;
363         if (mGestureOrientation == GestureOrientation.HORIZONTAL && !mPanel.isMaximized()) {
364             // Ignores multitouch events to prevent the Content View from scrolling.
365             if (action == MotionEvent.ACTION_POINTER_UP
366                     || action == MotionEvent.ACTION_POINTER_DOWN) {
367                 return;
368             }
369 
370             // NOTE(pedrosimonetti): Lock horizontal motion, ignoring all vertical changes,
371             // when the Panel is not maximized. This is to prevent the Content View
372             // from scrolling when side swiping on the expanded Panel. Also, note that the
373             // method {@link OverlayPanelEventFilter#lockEventHorizontallty} will always
374             // return an event with a single pointer, which is necessary to prevent
375             // the app from crashing when the motion involves multiple pointers.
376             // See: crbug.com/486901
377             event = MotionEvent.obtain(
378                     e.getDownTime(),
379                     e.getEventTime(),
380                     // NOTE(pedrosimonetti): Use getActionMasked() to make sure we're not
381                     // send any pointer information to the event, given that getAction()
382                     // may have the pointer Id associated to it.
383                     e.getActionMasked(),
384                     e.getX(),
385                     mInitialEventY,
386                     e.getMetaState());
387 
388             isSyntheticEvent = true;
389         }
390 
391         final float contentViewOffsetXPx = mPanel.getContentX() / mPxToDp;
392         final float contentViewOffsetYPx = mPanel.getContentY() / mPxToDp;
393 
394         // Adjust the offset to be relative to the Content View.
395         event.offsetLocation(-contentViewOffsetXPx, -contentViewOffsetYPx);
396 
397         // Get the container view to propagate the event to.
398         WebContents webContents = mPanel.getWebContents();
399         ViewGroup containerView = mPanel.getContainerView();
400 
401         boolean wasEventCanceled = false;
402         if (mWasActionDownEventSynthetic && action == MotionEvent.ACTION_UP) {
403             float deltaX = event.getX() - mSyntheticActionDownX;
404             float deltaY = event.getY() - mSyntheticActionDownY;
405             // NOTE(pedrosimonetti): If the ACTION_DOWN event was synthetic and the distance
406             // between it and the ACTION_UP event was short, then we should synthesize an
407             // ACTION_CANCEL event to prevent a Tap gesture from being triggered on the
408             // Content View. See crbug.com/408654
409             if (!isDistanceGreaterThanTouchSlop(deltaX, deltaY)) {
410                 event.setAction(MotionEvent.ACTION_CANCEL);
411                 if (containerView != null) containerView.dispatchTouchEvent(event);
412                 wasEventCanceled = true;
413             }
414         } else if (action == MotionEvent.ACTION_DOWN) {
415             mPanel.onTouchSearchContentViewAck();
416         }
417 
418         if (!wasEventCanceled && containerView != null) containerView.dispatchTouchEvent(event);
419 
420         // Synthetic events should be recycled.
421         if (isSyntheticEvent) event.recycle();
422     }
423 
424     /**
425      * Creates a {@link MotionEvent} inheriting from a given |e| event.
426      * @param e The {@link MotionEvent} to inherit properties from.
427      * @param action The MotionEvent's Action to be used.
428      * @return A new {@link MotionEvent}.
429      */
copyEvent(MotionEvent e, int action)430     private MotionEvent copyEvent(MotionEvent e, int action) {
431         MotionEvent event = MotionEvent.obtain(e);
432         event.setAction(action);
433         return event;
434     }
435 
436     /**
437      * Handles the tap event, determining the event target.
438      * @param e The tap {@link MotionEvent}.
439      * @return Whether the event has been consumed.
440      */
handleSingleTapUp(MotionEvent e)441     protected boolean handleSingleTapUp(MotionEvent e) {
442         // If the panel is peeking then the panel was already notified in #onTouchEventInternal().
443         if (mPanel.getPanelState() == PanelState.PEEKED) return false;
444 
445         setEventTarget(mPanel.isCoordinateInsideContent(
446                 e.getX() * mPxToDp, e.getY() * mPxToDp)
447                 ? EventTarget.CONTENT_VIEW : EventTarget.PANEL);
448         return false;
449     }
450 
451     /**
452      * Handles the scroll event, determining the gesture orientation and event target,
453      * when appropriate.
454      * @param e1 The first down {@link MotionEvent} that started the scrolling.
455      * @param e2 The move {@link MotionEvent} that triggered the current scroll.
456      * @param distanceY The distance along the Y axis that has been scrolled since the last call
457      *                  to handleScroll.
458      * @return Whether the event has been consumed.
459      */
handleScroll(MotionEvent e1, MotionEvent e2, float distanceY)460     protected boolean handleScroll(MotionEvent e1, MotionEvent e2, float distanceY) {
461         // If the panel is peeking then the swipe recognizer will handle the scroll event.
462         if (mPanel.getPanelState() == PanelState.PEEKED) return false;
463 
464         // Only determines the gesture orientation if it hasn't been determined yet,
465         // affectively "locking" the orientation once the gesture has started.
466         if (!mHasDeterminedGestureOrientation && isDistanceGreaterThanTouchSlop(e1, e2)) {
467             determineGestureOrientation(e1, e2);
468         }
469 
470         // Only determines the event target after determining the gesture orientation and
471         // if it hasn't been determined yet or if changing the event target during the
472         // middle of the gesture is supported. This will allow a smooth transition from
473         // swiping the Panel and scrolling the Content View.
474         final boolean mayChangeEventTarget = mMayChangeEventTarget && e2.getPointerCount() == 1;
475         if (mHasDeterminedGestureOrientation
476                 && (!mHasDeterminedEventTarget || mayChangeEventTarget)) {
477             determineEventTarget(distanceY);
478         }
479 
480         return false;
481     }
482 
483     /**
484      * Determines the gesture orientation.
485      * @param e1 The first down {@link MotionEvent} that started the scrolling.
486      * @param e2 The move {@link MotionEvent} that triggered the current scroll.
487      */
determineGestureOrientation(MotionEvent e1, MotionEvent e2)488     private void determineGestureOrientation(MotionEvent e1, MotionEvent e2) {
489         float deltaX = Math.abs(e2.getX() - e1.getX());
490         float deltaY = Math.abs(e2.getY() - e1.getY());
491         mGestureOrientation = deltaY * VERTICAL_DETERMINATION_BOOST > deltaX
492                 ? GestureOrientation.VERTICAL : GestureOrientation.HORIZONTAL;
493         mHasDeterminedGestureOrientation = true;
494     }
495 
496     /**
497      * Determines the target to propagate events to. This will not only update the
498      * {@code mEventTarget} but also save the previous target and determine whether the
499      * target has changed.
500      * @param distanceY The distance along the Y axis that has been scrolled since the last call
501      *                  to handleScroll.
502      */
determineEventTarget(float distanceY)503     private void determineEventTarget(float distanceY) {
504         boolean isVertical = mGestureOrientation == GestureOrientation.VERTICAL;
505 
506         boolean shouldPropagateEventsToPanel;
507         if (mPanel.isMaximized()) {
508             // Allow overscroll in the Content View to move the Panel.
509             boolean isMovingDown = distanceY < 0;
510             shouldPropagateEventsToPanel = isVertical
511                     && isMovingDown
512                     && getContentViewVerticalScroll() == 0;
513         } else {
514             // Only allow horizontal movements to be propagated to the Content View
515             // when the Panel is expanded (that is, not maximized).
516             shouldPropagateEventsToPanel = isVertical;
517 
518             // If the gesture is horizontal, then we know that the event target won't change.
519             if (!isVertical) mMayChangeEventTarget = false;
520         }
521 
522         @EventTarget
523         int target = shouldPropagateEventsToPanel ? EventTarget.PANEL : EventTarget.CONTENT_VIEW;
524 
525         if (target != mEventTarget) {
526             mPreviousEventTarget = mEventTarget;
527             setEventTarget(target);
528 
529             mHasChangedEventTarget = mEventTarget != mPreviousEventTarget
530                     && mPreviousEventTarget != EventTarget.UNDETERMINED;
531         }
532     }
533 
534     /**
535      * Sets the {@link EventTarget}.
536      * @param target The {@link EventTarget} to be set.
537      */
538     private void setEventTarget(@EventTarget int target) {
539         mEventTarget = target;
540 
541         mIsDeterminingEventTarget = false;
542         mHasDeterminedEventTarget = true;
543     }
544 
545     /**
546      * @param e1 The first down {@link MotionEvent} that started the scrolling.
547      * @param e2 The move {@link MotionEvent} that triggered the current scroll.
548      * @return Whether the distance is greater than the touch slop threshold.
549      */
550     private boolean isDistanceGreaterThanTouchSlop(MotionEvent e1, MotionEvent e2) {
551         float deltaX = e2.getX() - e1.getX();
552         float deltaY = e2.getY() - e1.getY();
553         // Check if the distance between the events |e1| and |e2| is greater than the touch slop.
554         return isDistanceGreaterThanTouchSlop(deltaX, deltaY);
555     }
556 
557     /**
558      * @param deltaX The delta X in pixels.
559      * @param deltaY The delta Y in pixels.
560      * @return Whether the distance is greater than the touch slop threshold.
561      */
562     private boolean isDistanceGreaterThanTouchSlop(float deltaX, float deltaY) {
563         return deltaX * deltaX + deltaY * deltaY > mTouchSlopSquarePx;
564     }
565 
566     /**
567      * Internal GestureDetector class that is responsible for determining the event target.
568      */
569     private class InternalGestureDetector extends GestureDetector.SimpleOnGestureListener {
570         @Override
onShowPress(MotionEvent e)571         public void onShowPress(MotionEvent e) {
572             mPanel.onShowPress(e.getX() * mPxToDp, e.getY() * mPxToDp);
573         }
574 
575         @Override
onSingleTapUp(MotionEvent e)576         public boolean onSingleTapUp(MotionEvent e) {
577             return handleSingleTapUp(e);
578         }
579 
580         @Override
onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)581         public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
582             return handleScroll(e1, e2, distanceY);
583         }
584     }
585 }
586