// Copyright 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.compositor.layouts.eventfilter;
import android.content.Context;
import android.os.Handler;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
/**
* Filters events that would trigger gestures like scroll and fling.
*/
public class GestureEventFilter extends EventFilter {
private final int mLongPressTimeoutMs;
private final GestureDetector mDetector;
private final GestureHandler mHandler;
private final boolean mUseDefaultLongPress;
private final int mScaledTouchSlop;
private boolean mSingleInput = true;
private boolean mInLongPress;
private boolean mSeenFirstScrollEvent;
private int mButtons = 0;
private LongPressRunnable mLongPressRunnable = new LongPressRunnable();
private Handler mLongPressHandler = new Handler();
/**
* A runnable to send a delayed long press.
*/
private class LongPressRunnable implements Runnable {
private MotionEvent mInitialEvent;
private boolean mIsPending;
public void init(MotionEvent e) {
if (mInitialEvent != null) {
mInitialEvent.recycle();
}
mInitialEvent = MotionEvent.obtain(e);
mIsPending = true;
}
@Override
public void run() {
longPress(mInitialEvent);
mIsPending = false;
}
public void cancel() {
mIsPending = false;
}
public boolean isPending() {
return mIsPending;
}
public MotionEvent getInitialEvent() {
return mInitialEvent;
}
}
/**
* Creates a {@link GestureEventFilter} with offset touch events.
*/
public GestureEventFilter(Context context, EventFilterHost host, GestureHandler handler) {
this(context, host, handler, true);
}
/**
* Creates a {@link GestureEventFilter} with default long press behavior.
*/
public GestureEventFilter(Context context, EventFilterHost host, GestureHandler handler,
boolean autoOffset) {
this(context, host, handler, autoOffset, true);
}
/**
* Creates a {@link GestureEventFilter}.
* @param useDefaultLongPress If true, use Android's long press behavior which does not send
* any more events after a long press. If false, use a custom
* implementation that will send events after a long press.
*/
public GestureEventFilter(Context context, EventFilterHost host, GestureHandler handler,
boolean autoOffset, boolean useDefaultLongPress) {
super(context, host, autoOffset);
mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
mLongPressTimeoutMs = ViewConfiguration.getLongPressTimeout();
mUseDefaultLongPress = useDefaultLongPress;
mHandler = handler;
context.getResources();
mDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
private float mOnScrollBeginX;
private float mOnScrollBeginY;
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2,
float distanceX, float distanceY) {
if (!mSeenFirstScrollEvent) {
// Remove the touch slop region from the first scroll event to avoid a
// jump.
mSeenFirstScrollEvent = true;
float distance = (float) Math.sqrt(
distanceX * distanceX + distanceY * distanceY);
if (distance > 0.0f) {
float ratio = Math.max(0, distance - mScaledTouchSlop) / distance;
mOnScrollBeginX = e1.getX() + distanceX * (1.0f - ratio);
mOnScrollBeginY = e1.getY() + distanceY * (1.0f - ratio);
distanceX *= ratio;
distanceY *= ratio;
}
}
if (mHandler != null && mSingleInput) {
// distanceX/Y only represent the distance since the last event, not the total
// distance for the full scroll. Calculate the total distance here.
float totalX = e2.getX() - mOnScrollBeginX;
float totalY = e2.getY() - mOnScrollBeginY;
mHandler.drag(e2.getX() * mPxToDp, e2.getY() * mPxToDp,
-distanceX * mPxToDp, -distanceY * mPxToDp,
totalX * mPxToDp, totalY * mPxToDp);
}
return true;
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
/* Android's GestureDector calls listener.onSingleTapUp on MotionEvent.ACTION_UP
* during long press, so we need to explicitly not call handler.click if a
* long press has been detected.
*/
if (mHandler != null && mSingleInput && !mInLongPress) {
mHandler.click(e.getX() * mPxToDp, e.getY() * mPxToDp,
e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE,
mButtons);
}
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2,
float velocityX, float velocityY) {
if (mHandler != null && mSingleInput) {
mHandler.fling(e1.getX() * mPxToDp, e1.getY() * mPxToDp,
velocityX * mPxToDp, velocityY * mPxToDp);
}
return true;
}
@Override
public boolean onDown(MotionEvent e) {
mButtons = e.getButtonState();
mInLongPress = false;
mSeenFirstScrollEvent = false;
if (mHandler != null && mSingleInput) {
mHandler.onDown(e.getX() * mPxToDp,
e.getY() * mPxToDp,
e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE,
mButtons);
}
return true;
}
@Override
public void onLongPress(MotionEvent e) {
longPress(e);
}
});
mDetector.setIsLongpressEnabled(mUseDefaultLongPress);
}
private void longPress(MotionEvent e) {
if (mHandler != null && mSingleInput) {
mInLongPress = true;
mHandler.onLongPress(e.getX() * mPxToDp, e.getY() * mPxToDp);
}
}
@Override
public boolean onInterceptTouchEventInternal(MotionEvent e, boolean isKeyboardShowing) {
return mHandler != null;
}
private void cancelLongPress() {
mLongPressHandler.removeCallbacks(mLongPressRunnable);
mLongPressRunnable.cancel();
}
@Override
public boolean onTouchEventInternal(MotionEvent e) {
final int action = e.getActionMasked();
// This path mimics the Android long press detection while still allowing
// other touch events to come through the gesture detector.
if (!mUseDefaultLongPress) {
if (e.getPointerCount() > 1) {
// If there's more than one pointer ignore the long press.
if (mLongPressRunnable.isPending()) {
cancelLongPress();
}
} else if (action == MotionEvent.ACTION_DOWN) {
// If there was a pending event kill it off.
if (mLongPressRunnable.isPending()) {
cancelLongPress();
}
mLongPressRunnable.init(e);
mLongPressHandler.postDelayed(mLongPressRunnable, mLongPressTimeoutMs);
} else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
cancelLongPress();
} else if (mLongPressRunnable.isPending()) {
// Allow for a little bit of touch slop.
MotionEvent initialEvent = mLongPressRunnable.getInitialEvent();
float distanceX = initialEvent.getX() - e.getX();
float distanceY = initialEvent.getY() - e.getY();
float distance = distanceX * distanceX + distanceY * distanceY;
// Save a square root here by comparing to squared touch slop
if (distance > mScaledTouchSlop * mScaledTouchSlop) {
cancelLongPress();
}
}
}
// Sends the pinch event if two or more fingers touch the screen. According to test
// Android handles the fingers order pretty consistently so always requesting
// index 0 and 1 works here.
// This might need some rework if 3 fingers event are supported.
if (e.getPointerCount() > 1) {
mHandler.onPinch(e.getX(0) * mPxToDp, e.getY(0) * mPxToDp,
e.getX(1) * mPxToDp, e.getY(1) * mPxToDp,
action == MotionEvent.ACTION_POINTER_DOWN);
mDetector.setIsLongpressEnabled(false);
mSingleInput = false;
} else {
mDetector.setIsLongpressEnabled(mUseDefaultLongPress);
mSingleInput = true;
}
mDetector.onTouchEvent(e);
// Propagate the up event after any gesture events.
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
mHandler.onUpOrCancel();
}
return true;
}
}