1 // Copyright 2012 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.os.Handler; 9 import android.view.GestureDetector; 10 import android.view.MotionEvent; 11 import android.view.ViewConfiguration; 12 13 import org.chromium.chrome.browser.layouts.EventFilter; 14 15 /** 16 * Filters events that would trigger gestures like scroll and fling. 17 */ 18 public class GestureEventFilter extends EventFilter { 19 private final int mLongPressTimeoutMs; 20 private final GestureDetector mDetector; 21 private final GestureHandler mHandler; 22 private final boolean mUseDefaultLongPress; 23 private final int mScaledTouchSlop; 24 private boolean mSingleInput = true; 25 private boolean mInLongPress; 26 private boolean mSeenFirstScrollEvent; 27 private int mButtons; 28 private LongPressRunnable mLongPressRunnable = new LongPressRunnable(); 29 private Handler mLongPressHandler = new Handler(); 30 31 /** 32 * A runnable to send a delayed long press. 33 */ 34 private class LongPressRunnable implements Runnable { 35 private MotionEvent mInitialEvent; 36 private boolean mIsPending; 37 init(MotionEvent e)38 public void init(MotionEvent e) { 39 if (mInitialEvent != null) { 40 mInitialEvent.recycle(); 41 } 42 mInitialEvent = MotionEvent.obtain(e); 43 mIsPending = true; 44 } 45 46 @Override run()47 public void run() { 48 longPress(mInitialEvent); 49 mIsPending = false; 50 } 51 cancel()52 public void cancel() { 53 mIsPending = false; 54 } 55 isPending()56 public boolean isPending() { 57 return mIsPending; 58 } 59 getInitialEvent()60 public MotionEvent getInitialEvent() { 61 return mInitialEvent; 62 } 63 } 64 65 /** 66 * Creates a {@link GestureEventFilter} with offset touch events. 67 */ GestureEventFilter(Context context, GestureHandler handler)68 public GestureEventFilter(Context context, GestureHandler handler) { 69 this(context, handler, true); 70 } 71 72 /** 73 * Creates a {@link GestureEventFilter} with default long press behavior. 74 */ GestureEventFilter(Context context, GestureHandler handler, boolean autoOffset)75 public GestureEventFilter(Context context, GestureHandler handler, boolean autoOffset) { 76 this(context, handler, autoOffset, true); 77 } 78 79 /** 80 * Creates a {@link GestureEventFilter}. 81 * @param useDefaultLongPress If true, use Android's long press behavior which does not send 82 * any more events after a long press. If false, use a custom 83 * implementation that will send events after a long press. 84 */ GestureEventFilter(Context context, GestureHandler handler, boolean autoOffset, boolean useDefaultLongPress)85 public GestureEventFilter(Context context, GestureHandler handler, boolean autoOffset, 86 boolean useDefaultLongPress) { 87 super(context, autoOffset); 88 assert handler != null; 89 mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 90 mLongPressTimeoutMs = ViewConfiguration.getLongPressTimeout(); 91 mUseDefaultLongPress = useDefaultLongPress; 92 mHandler = handler; 93 context.getResources(); 94 95 mDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() { 96 97 private float mOnScrollBeginX; 98 private float mOnScrollBeginY; 99 100 @Override 101 public boolean onScroll(MotionEvent e1, MotionEvent e2, 102 float distanceX, float distanceY) { 103 if (!mSeenFirstScrollEvent) { 104 // Remove the touch slop region from the first scroll event to avoid a 105 // jump. 106 mSeenFirstScrollEvent = true; 107 float distance = (float) Math.sqrt( 108 distanceX * distanceX + distanceY * distanceY); 109 if (distance > 0.0f) { 110 float ratio = Math.max(0, distance - mScaledTouchSlop) / distance; 111 mOnScrollBeginX = e1.getX() + distanceX * (1.0f - ratio); 112 mOnScrollBeginY = e1.getY() + distanceY * (1.0f - ratio); 113 distanceX *= ratio; 114 distanceY *= ratio; 115 } 116 } 117 if (mSingleInput) { 118 // distanceX/Y only represent the distance since the last event, not the total 119 // distance for the full scroll. Calculate the total distance here. 120 float totalX = e2.getX() - mOnScrollBeginX; 121 float totalY = e2.getY() - mOnScrollBeginY; 122 mHandler.drag(e2.getX() * mPxToDp, e2.getY() * mPxToDp, 123 -distanceX * mPxToDp, -distanceY * mPxToDp, 124 totalX * mPxToDp, totalY * mPxToDp); 125 } 126 return true; 127 } 128 129 @Override 130 public boolean onSingleTapUp(MotionEvent e) { 131 /* Android's GestureDector calls listener.onSingleTapUp on MotionEvent.ACTION_UP 132 * during long press, so we need to explicitly not call handler.click if a 133 * long press has been detected. 134 */ 135 if (mSingleInput && !mInLongPress) { 136 mHandler.click(e.getX() * mPxToDp, e.getY() * mPxToDp, 137 e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE, 138 mButtons); 139 } 140 return true; 141 } 142 143 @Override 144 public boolean onFling(MotionEvent e1, MotionEvent e2, 145 float velocityX, float velocityY) { 146 if (mSingleInput) { 147 mHandler.fling(e1.getX() * mPxToDp, e1.getY() * mPxToDp, 148 velocityX * mPxToDp, velocityY * mPxToDp); 149 } 150 return true; 151 } 152 153 @Override 154 public boolean onDown(MotionEvent e) { 155 mButtons = e.getButtonState(); 156 mInLongPress = false; 157 mSeenFirstScrollEvent = false; 158 if (mSingleInput) { 159 mHandler.onDown(e.getX() * mPxToDp, 160 e.getY() * mPxToDp, 161 e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE, 162 mButtons); 163 } 164 return true; 165 } 166 167 @Override 168 public void onLongPress(MotionEvent e) { 169 longPress(e); 170 } 171 }); 172 173 mDetector.setIsLongpressEnabled(mUseDefaultLongPress); 174 } 175 longPress(MotionEvent e)176 private void longPress(MotionEvent e) { 177 if (mSingleInput) { 178 mInLongPress = true; 179 mHandler.onLongPress(e.getX() * mPxToDp, e.getY() * mPxToDp); 180 } 181 } 182 183 @Override onInterceptTouchEventInternal(MotionEvent e, boolean isKeyboardShowing)184 public boolean onInterceptTouchEventInternal(MotionEvent e, boolean isKeyboardShowing) { 185 return true; 186 } 187 cancelLongPress()188 private void cancelLongPress() { 189 mLongPressHandler.removeCallbacks(mLongPressRunnable); 190 mLongPressRunnable.cancel(); 191 } 192 193 @Override onTouchEventInternal(MotionEvent e)194 public boolean onTouchEventInternal(MotionEvent e) { 195 final int action = e.getActionMasked(); 196 197 // This path mimics the Android long press detection while still allowing 198 // other touch events to come through the gesture detector. 199 if (!mUseDefaultLongPress) { 200 if (e.getPointerCount() > 1) { 201 // If there's more than one pointer ignore the long press. 202 if (mLongPressRunnable.isPending()) { 203 cancelLongPress(); 204 } 205 } else if (action == MotionEvent.ACTION_DOWN) { 206 // If there was a pending event kill it off. 207 if (mLongPressRunnable.isPending()) { 208 cancelLongPress(); 209 } 210 mLongPressRunnable.init(e); 211 mLongPressHandler.postDelayed(mLongPressRunnable, mLongPressTimeoutMs); 212 } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { 213 cancelLongPress(); 214 } else if (mLongPressRunnable.isPending()) { 215 // Allow for a little bit of touch slop. 216 MotionEvent initialEvent = mLongPressRunnable.getInitialEvent(); 217 float distanceX = initialEvent.getX() - e.getX(); 218 float distanceY = initialEvent.getY() - e.getY(); 219 float distance = distanceX * distanceX + distanceY * distanceY; 220 221 // Save a square root here by comparing to squared touch slop 222 if (distance > mScaledTouchSlop * mScaledTouchSlop) { 223 cancelLongPress(); 224 } 225 } 226 } 227 228 // Sends the pinch event if two or more fingers touch the screen. According to test 229 // Android handles the fingers order pretty consistently so always requesting 230 // index 0 and 1 works here. 231 // This might need some rework if 3 fingers event are supported. 232 if (e.getPointerCount() > 1) { 233 mHandler.onPinch(e.getX(0) * mPxToDp, e.getY(0) * mPxToDp, 234 e.getX(1) * mPxToDp, e.getY(1) * mPxToDp, 235 action == MotionEvent.ACTION_POINTER_DOWN); 236 mDetector.setIsLongpressEnabled(false); 237 mSingleInput = false; 238 } else { 239 mDetector.setIsLongpressEnabled(mUseDefaultLongPress); 240 mSingleInput = true; 241 } 242 mDetector.onTouchEvent(e); 243 244 // Propagate the up event after any gesture events. 245 if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { 246 mHandler.onUpOrCancel(); 247 } 248 return true; 249 } 250 } 251