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