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