/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Modified for aFreeRDP by Martin Fleisz (martin.fleisz@thincast.com)
*/
package com.freerdp.freerdpcore.utils;
import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
public class GestureDetector
{
private static final int TAP_TIMEOUT = 100;
private static final int DOUBLE_TAP_TIMEOUT = 200;
// Distance a touch can wander before we think the user is the first touch in a sequence of
// double tap
private static final int LARGE_TOUCH_SLOP = 18;
// Distance between the first touch and second touch to still be considered a double tap
private static final int DOUBLE_TAP_SLOP = 100;
// constants for Message.what used by GestureHandler below
private static final int SHOW_PRESS = 1;
private static final int LONG_PRESS = 2;
private static final int TAP = 3;
private final Handler mHandler;
private final OnGestureListener mListener;
private int mTouchSlopSquare;
private int mLargeTouchSlopSquare;
private int mDoubleTapSlopSquare;
private int mLongpressTimeout = 100;
private OnDoubleTapListener mDoubleTapListener;
private boolean mStillDown;
private boolean mInLongPress;
private boolean mAlwaysInTapRegion;
private boolean mAlwaysInBiggerTapRegion;
private MotionEvent mCurrentDownEvent;
private MotionEvent mPreviousUpEvent;
/**
* True when the user is still touching for the second tap (down, move, and
* up events). Can only be true if there is a double tap listener attached.
*/
private boolean mIsDoubleTapping;
private float mLastMotionY;
private float mLastMotionX;
private boolean mIsLongpressEnabled;
/**
* True if we are at a target API level of >= Froyo or the developer can
* explicitly set it. If true, input events with > 1 pointer will be ignored
* so we can work side by side with multitouch gesture detectors.
*/
private boolean mIgnoreMultitouch;
/**
* Creates a GestureDetector with the supplied listener.
* You may only use this constructor from a UI thread (this is the usual situation).
*
* @param context the application's context
* @param listener the listener invoked for all the callbacks, this must
* not be null.
* @throws NullPointerException if {@code listener} is null.
* @see android.os.Handler#Handler()
*/
public GestureDetector(Context context, OnGestureListener listener)
{
this(context, listener, null);
}
/**
* Creates a GestureDetector with the supplied listener.
* You may only use this constructor from a UI thread (this is the usual situation).
*
* @param context the application's context
* @param listener the listener invoked for all the callbacks, this must
* not be null.
* @param handler the handler to use
* @throws NullPointerException if {@code listener} is null.
* @see android.os.Handler#Handler()
*/
public GestureDetector(Context context, OnGestureListener listener, Handler handler)
{
this(context, listener, handler,
context != null &&
context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.FROYO);
}
/**
* Creates a GestureDetector with the supplied listener.
* You may only use this constructor from a UI thread (this is the usual situation).
*
* @param context the application's context
* @param listener the listener invoked for all the callbacks, this must
* not be null.
* @param handler the handler to use
* @param ignoreMultitouch whether events involving more than one pointer should
* be ignored.
* @throws NullPointerException if {@code listener} is null.
* @see android.os.Handler#Handler()
*/
public GestureDetector(Context context, OnGestureListener listener, Handler handler,
boolean ignoreMultitouch)
{
if (handler != null)
{
mHandler = new GestureHandler(handler);
}
else
{
mHandler = new GestureHandler();
}
mListener = listener;
if (listener instanceof OnDoubleTapListener)
{
setOnDoubleTapListener((OnDoubleTapListener)listener);
}
init(context, ignoreMultitouch);
}
private void init(Context context, boolean ignoreMultitouch)
{
if (mListener == null)
{
throw new NullPointerException("OnGestureListener must not be null");
}
mIsLongpressEnabled = true;
mIgnoreMultitouch = ignoreMultitouch;
// Fallback to support pre-donuts releases
int touchSlop, largeTouchSlop, doubleTapSlop;
if (context == null)
{
// noinspection deprecation
touchSlop = ViewConfiguration.getTouchSlop();
largeTouchSlop = touchSlop + 2;
doubleTapSlop = DOUBLE_TAP_SLOP;
}
else
{
final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
final float density = metrics.density;
final ViewConfiguration configuration = ViewConfiguration.get(context);
touchSlop = configuration.getScaledTouchSlop();
largeTouchSlop = (int)(density * LARGE_TOUCH_SLOP + 0.5f);
doubleTapSlop = configuration.getScaledDoubleTapSlop();
}
mTouchSlopSquare = touchSlop * touchSlop;
mLargeTouchSlopSquare = largeTouchSlop * largeTouchSlop;
mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop;
}
/**
* Sets the listener which will be called for double-tap and related
* gestures.
*
* @param onDoubleTapListener the listener invoked for all the callbacks, or
* null to stop listening for double-tap gestures.
*/
public void setOnDoubleTapListener(OnDoubleTapListener onDoubleTapListener)
{
mDoubleTapListener = onDoubleTapListener;
}
/**
* Set whether longpress is enabled, if this is enabled when a user
* presses and holds down you get a longpress event and nothing further.
* If it's disabled the user can press and hold down and then later
* moved their finger and you will get scroll events. By default
* longpress is enabled.
*
* @param isLongpressEnabled whether longpress should be enabled.
*/
public void setIsLongpressEnabled(boolean isLongpressEnabled)
{
mIsLongpressEnabled = isLongpressEnabled;
}
/**
* @return true if longpress is enabled, else false.
*/
public boolean isLongpressEnabled()
{
return mIsLongpressEnabled;
}
public void setLongPressTimeout(int timeout)
{
mLongpressTimeout = timeout;
}
/**
* Analyzes the given motion event and if applicable triggers the
* appropriate callbacks on the {@link OnGestureListener} supplied.
*
* @param ev The current motion event.
* @return true if the {@link OnGestureListener} consumed the event,
* else false.
*/
public boolean onTouchEvent(MotionEvent ev)
{
final int action = ev.getAction();
final float y = ev.getY();
final float x = ev.getX();
boolean handled = false;
switch (action & MotionEvent.ACTION_MASK)
{
case MotionEvent.ACTION_POINTER_DOWN:
if (mIgnoreMultitouch)
{
// Multitouch event - abort.
cancel();
}
break;
case MotionEvent.ACTION_POINTER_UP:
// Ending a multitouch gesture and going back to 1 finger
if (mIgnoreMultitouch && ev.getPointerCount() == 2)
{
int index = (((action & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
MotionEvent.ACTION_POINTER_INDEX_SHIFT) == 0)
? 1
: 0;
mLastMotionX = ev.getX(index);
mLastMotionY = ev.getY(index);
}
break;
case MotionEvent.ACTION_DOWN:
if (mDoubleTapListener != null)
{
boolean hadTapMessage = mHandler.hasMessages(TAP);
if (hadTapMessage)
mHandler.removeMessages(TAP);
if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null) &&
hadTapMessage &&
isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev))
{
// This is a second tap
mIsDoubleTapping = true;
// Give a callback with the first tap of the double-tap
handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent);
// Give a callback with down event of the double-tap
handled |= mDoubleTapListener.onDoubleTapEvent(ev);
}
else
{
// This is a first tap
mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT);
}
}
mLastMotionX = x;
mLastMotionY = y;
if (mCurrentDownEvent != null)
{
mCurrentDownEvent.recycle();
}
mCurrentDownEvent = MotionEvent.obtain(ev);
mAlwaysInTapRegion = true;
mAlwaysInBiggerTapRegion = true;
mStillDown = true;
mInLongPress = false;
if (mIsLongpressEnabled)
{
mHandler.removeMessages(LONG_PRESS);
mHandler.sendEmptyMessageAtTime(LONG_PRESS, mCurrentDownEvent.getDownTime() +
TAP_TIMEOUT +
mLongpressTimeout);
}
mHandler.sendEmptyMessageAtTime(SHOW_PRESS,
mCurrentDownEvent.getDownTime() + TAP_TIMEOUT);
handled |= mListener.onDown(ev);
break;
case MotionEvent.ACTION_MOVE:
if (mIgnoreMultitouch && ev.getPointerCount() > 1)
{
break;
}
final float scrollX = mLastMotionX - x;
final float scrollY = mLastMotionY - y;
if (mIsDoubleTapping)
{
// Give the move events of the double-tap
handled |= mDoubleTapListener.onDoubleTapEvent(ev);
}
else if (mAlwaysInTapRegion)
{
final int deltaX = (int)(x - mCurrentDownEvent.getX());
final int deltaY = (int)(y - mCurrentDownEvent.getY());
int distance = (deltaX * deltaX) + (deltaY * deltaY);
if (distance > mTouchSlopSquare)
{
mLastMotionX = x;
mLastMotionY = y;
mAlwaysInTapRegion = false;
mHandler.removeMessages(TAP);
mHandler.removeMessages(SHOW_PRESS);
mHandler.removeMessages(LONG_PRESS);
}
if (distance > mLargeTouchSlopSquare)
{
mAlwaysInBiggerTapRegion = false;
}
handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
}
else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1))
{
handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
mLastMotionX = x;
mLastMotionY = y;
}
break;
case MotionEvent.ACTION_UP:
mStillDown = false;
MotionEvent currentUpEvent = MotionEvent.obtain(ev);
if (mIsDoubleTapping)
{
// Finally, give the up event of the double-tap
handled |= mDoubleTapListener.onDoubleTapEvent(ev);
}
else if (mInLongPress)
{
mHandler.removeMessages(TAP);
mListener.onLongPressUp(ev);
mInLongPress = false;
}
else if (mAlwaysInTapRegion)
{
handled = mListener.onSingleTapUp(mCurrentDownEvent);
}
else
{
// A fling must travel the minimum tap distance
}
if (mPreviousUpEvent != null)
{
mPreviousUpEvent.recycle();
}
// Hold the event we obtained above - listeners may have changed the original.
mPreviousUpEvent = currentUpEvent;
mIsDoubleTapping = false;
mHandler.removeMessages(SHOW_PRESS);
mHandler.removeMessages(LONG_PRESS);
handled |= mListener.onUp(ev);
break;
case MotionEvent.ACTION_CANCEL:
cancel();
break;
}
return handled;
}
private void cancel()
{
mHandler.removeMessages(SHOW_PRESS);
mHandler.removeMessages(LONG_PRESS);
mHandler.removeMessages(TAP);
mAlwaysInTapRegion = false; // ensures that we won't receive an OnSingleTap notification
// when a 2-Finger tap is performed
mIsDoubleTapping = false;
mStillDown = false;
if (mInLongPress)
{
mInLongPress = false;
}
}
private boolean isConsideredDoubleTap(MotionEvent firstDown, MotionEvent firstUp,
MotionEvent secondDown)
{
if (!mAlwaysInBiggerTapRegion)
{
return false;
}
if (secondDown.getEventTime() - firstUp.getEventTime() > DOUBLE_TAP_TIMEOUT)
{
return false;
}
int deltaX = (int)firstDown.getX() - (int)secondDown.getX();
int deltaY = (int)firstDown.getY() - (int)secondDown.getY();
return (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare);
}
private void dispatchLongPress()
{
mHandler.removeMessages(TAP);
mInLongPress = true;
mListener.onLongPress(mCurrentDownEvent);
}
/**
* The listener that is used to notify when gestures occur.
* If you want to listen for all the different gestures then implement
* this interface. If you only want to listen for a subset it might
* be easier to extend {@link SimpleOnGestureListener}.
*/
public interface OnGestureListener {
/**
* Notified when a tap occurs with the down {@link MotionEvent}
* that triggered it. This will be triggered immediately for
* every down event. All other events should be preceded by this.
*
* @param e The down motion event.
*/
boolean onDown(MotionEvent e);
/**
* Notified when a tap finishes with the up {@link MotionEvent}
* that triggered it. This will be triggered immediately for
* every up event. All other events should be preceded by this.
*
* @param e The up motion event.
*/
boolean onUp(MotionEvent e);
/**
* The user has performed a down {@link MotionEvent} and not performed
* a move or up yet. This event is commonly used to provide visual
* feedback to the user to let them know that their action has been
* recognized i.e. highlight an element.
*
* @param e The down motion event
*/
void onShowPress(MotionEvent e);
/**
* Notified when a tap occurs with the up {@link MotionEvent}
* that triggered it.
*
* @param e The up motion event that completed the first tap
* @return true if the event is consumed, else false
*/
boolean onSingleTapUp(MotionEvent e);
/**
* Notified when a scroll occurs with the initial on down {@link MotionEvent} and the
* current move {@link MotionEvent}. The distance in x and y is also supplied for
* convenience.
*
* @param e1 The first down motion event that started the scrolling.
* @param e2 The move motion event that triggered the current onScroll.
* @param distanceX The distance along the X axis that has been scrolled since the last
* call to onScroll. This is NOT the distance between {@code e1}
* and {@code e2}.
* @param distanceY The distance along the Y axis that has been scrolled since the last
* call to onScroll. This is NOT the distance between {@code e1}
* and {@code e2}.
* @return true if the event is consumed, else false
*/
boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);
/**
* Notified when a long press occurs with the initial on down {@link MotionEvent}
* that trigged it.
*
* @param e The initial on down motion event that started the longpress.
*/
void onLongPress(MotionEvent e);
/**
* Notified when a long press ends with the final {@link MotionEvent}.
*
* @param e The up motion event that ended the longpress.
*/
void onLongPressUp(MotionEvent e);
}
/**
* The listener that is used to notify when a double-tap or a confirmed
* single-tap occur.
*/
public interface OnDoubleTapListener {
/**
* Notified when a single-tap occurs.
* <p>
* Unlike {@link OnGestureListener#onSingleTapUp(MotionEvent)}, this
* will only be called after the detector is confident that the user's
* first tap is not followed by a second tap leading to a double-tap
* gesture.
*
* @param e The down motion event of the single-tap.
* @return true if the event is consumed, else false
*/
boolean onSingleTapConfirmed(MotionEvent e);
/**
* Notified when a double-tap occurs.
*
* @param e The down motion event of the first tap of the double-tap.
* @return true if the event is consumed, else false
*/
boolean onDoubleTap(MotionEvent e);
/**
* Notified when an event within a double-tap gesture occurs, including
* the down, move, and up events.
*
* @param e The motion event that occurred during the double-tap gesture.
* @return true if the event is consumed, else false
*/
boolean onDoubleTapEvent(MotionEvent e);
}
/**
* A convenience class to extend when you only want to listen for a subset
* of all the gestures. This implements all methods in the
* {@link OnGestureListener} and {@link OnDoubleTapListener} but does
* nothing and return {@code false} for all applicable methods.
*/
public static class SimpleOnGestureListener implements OnGestureListener, OnDoubleTapListener
{
public boolean onSingleTapUp(MotionEvent e)
{
return false;
}
public void onLongPress(MotionEvent e)
{
}
public void onLongPressUp(MotionEvent e)
{
}
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)
{
return false;
}
public void onShowPress(MotionEvent e)
{
}
public boolean onDown(MotionEvent e)
{
return false;
}
public boolean onUp(MotionEvent e)
{
return false;
}
public boolean onDoubleTap(MotionEvent e)
{
return false;
}
public boolean onDoubleTapEvent(MotionEvent e)
{
return false;
}
public boolean onSingleTapConfirmed(MotionEvent e)
{
return false;
}
}
private class GestureHandler extends Handler
{
GestureHandler()
{
super();
}
GestureHandler(Handler handler)
{
super(handler.getLooper());
}
@Override public void handleMessage(Message msg)
{
switch (msg.what)
{
case SHOW_PRESS:
mListener.onShowPress(mCurrentDownEvent);
break;
case LONG_PRESS:
dispatchLongPress();
break;
case TAP:
// If the user's finger is still down, do not count it as a tap
if (mDoubleTapListener != null && !mStillDown)
{
mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent);
}
break;
default:
throw new RuntimeException("Unknown message " + msg); // never
}
}
}
}