Bug 775463: Recognize double tap gestures while still supporting single taps

This commit is contained in:
Doug Sherk 2012-07-27 17:23:51 -07:00
parent 001d00e5ff
commit 42ecca33c0
2 changed files with 137 additions and 38 deletions

View File

@ -4,15 +4,26 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "base/basictypes.h"
#include "base/thread.h"
#include "GestureEventListener.h"
#include "AsyncPanZoomController.h"
namespace mozilla {
namespace layers {
/**
* Maximum time for a touch on the screen and corresponding lift of the finger
* to be considered a tap. This also applies to double taps, except that it is
* used twice.
*/
static const int MAX_TAP_TIME = 300;
GestureEventListener::GestureEventListener(AsyncPanZoomController* aAsyncPanZoomController)
: mAsyncPanZoomController(aAsyncPanZoomController),
mState(NoGesture)
mState(GESTURE_NONE),
mLastTouchInput(MultiTouchInput::MULTITOUCH_START, 0)
{
}
@ -49,7 +60,13 @@ nsEventStatus GestureEventListener::HandleInputEvent(const InputData& aEvent)
}
}
if (mTouches.Length() == 2) {
size_t length = mTouches.Length();
if (length == 1) {
mTapStartTime = event.mTime;
if (mState == GESTURE_NONE) {
mState = GESTURE_WAITING_SINGLE_TAP;
}
} else if (length == 2) {
// Another finger has been added; it can't be a tap anymore.
HandleTapCancel(event);
}
@ -89,20 +106,39 @@ nsEventStatus GestureEventListener::HandleInputEvent(const InputData& aEvent)
NS_WARN_IF_FALSE(foundAlreadyExistingTouch, "Touch ended, but not in list");
if (event.mTime - mTouchStartTime <= MAX_TAP_TIME) {
// XXX: Incorrect use of the tap event. In the future, we want to send this
// on NS_TOUCH_END, then have a short timer afterwards which sends
// SingleTapConfirmed. Since we don't have double taps yet, this is fine for
// now.
if (HandleSingleTapUpEvent(event) == nsEventStatus_eConsumeNoDefault) {
return nsEventStatus_eConsumeNoDefault;
}
if (event.mTime - mTapStartTime <= MAX_TAP_TIME) {
if (mState == GESTURE_WAITING_DOUBLE_TAP) {
mDoubleTapTimeoutTask->Cancel();
if (HandleSingleTapConfirmedEvent(event) == nsEventStatus_eConsumeNoDefault) {
return nsEventStatus_eConsumeNoDefault;
// We were waiting for a double tap and it has arrived.
HandleDoubleTap(event);
mState = GESTURE_NONE;
} else if (mState == GESTURE_WAITING_SINGLE_TAP) {
HandleSingleTapUpEvent(event);
// We were not waiting for anything but a single tap has happened that
// may turn into a double tap. Wait a while and if it doesn't turn into
// a double tap, send a single tap instead.
mState = GESTURE_WAITING_DOUBLE_TAP;
// Cache the current event since it may become the single tap that we
// send.
mLastTouchInput = event;
mDoubleTapTimeoutTask =
NewRunnableMethod(this, &GestureEventListener::TimeoutDoubleTap);
MessageLoop::current()->PostDelayedTask(
FROM_HERE,
mDoubleTapTimeoutTask,
MAX_TAP_TIME);
}
}
if (mState == GESTURE_WAITING_SINGLE_TAP) {
mState = GESTURE_NONE;
}
break;
}
case MultiTouchInput::MULTITOUCH_CANCEL:
@ -130,7 +166,7 @@ nsEventStatus GestureEventListener::HandlePinchGestureEvent(const MultiTouchInpu
float(NS_hypot(firstTouch.x - secondTouch.x,
firstTouch.y - secondTouch.y));
if (mState == NoGesture) {
if (mState == GESTURE_NONE) {
PinchGestureInput pinchEvent(PinchGestureInput::PINCHGESTURE_START,
aEvent.mTime,
focusPoint,
@ -139,7 +175,7 @@ nsEventStatus GestureEventListener::HandlePinchGestureEvent(const MultiTouchInpu
mAsyncPanZoomController->HandleInputEvent(pinchEvent);
mState = InPinchGesture;
mState = GESTURE_PINCH;
} else {
PinchGestureInput pinchEvent(PinchGestureInput::PINCHGESTURE_SCALE,
aEvent.mTime,
@ -153,16 +189,16 @@ nsEventStatus GestureEventListener::HandlePinchGestureEvent(const MultiTouchInpu
mPreviousSpan = currentSpan;
rv = nsEventStatus_eConsumeNoDefault;
} else if (mState == InPinchGesture) {
} else if (mState == GESTURE_PINCH) {
PinchGestureInput pinchEvent(PinchGestureInput::PINCHGESTURE_END,
aEvent.mTime,
mTouches[0].mScreenPoint,
1.0f,
1.0f);
mAsyncPanZoomController->HandleInputEvent(pinchEvent);
mState = NoGesture;
mState = GESTURE_NONE;
rv = nsEventStatus_eConsumeNoDefault;
}
@ -177,29 +213,49 @@ nsEventStatus GestureEventListener::HandlePinchGestureEvent(const MultiTouchInpu
nsEventStatus GestureEventListener::HandleSingleTapUpEvent(const MultiTouchInput& aEvent)
{
TapGestureInput tapEvent(TapGestureInput::TAPGESTURE_UP, aEvent.mTime, aEvent.mTouches[0].mScreenPoint);
mAsyncPanZoomController->HandleInputEvent(tapEvent);
return nsEventStatus_eConsumeDoDefault;
return mAsyncPanZoomController->HandleInputEvent(tapEvent);
}
nsEventStatus GestureEventListener::HandleSingleTapConfirmedEvent(const MultiTouchInput& aEvent)
{
TapGestureInput tapEvent(TapGestureInput::TAPGESTURE_CONFIRMED, aEvent.mTime, aEvent.mTouches[0].mScreenPoint);
mAsyncPanZoomController->HandleInputEvent(tapEvent);
return nsEventStatus_eConsumeDoDefault;
return mAsyncPanZoomController->HandleInputEvent(tapEvent);
}
nsEventStatus GestureEventListener::HandleTapCancel(const MultiTouchInput& aEvent)
{
// XXX: In the future we will have to actually send a cancel notification to
// Gecko, but for now since we're doing both the "SingleUp" and
// "SingleConfirmed" notifications together, there's no need to cancel either
// one.
mTouchStartTime = 0;
mTapStartTime = 0;
switch (mState)
{
case GESTURE_WAITING_SINGLE_TAP:
case GESTURE_WAITING_DOUBLE_TAP:
mState = GESTURE_NONE;
break;
default:
break;
}
return nsEventStatus_eConsumeDoDefault;
}
nsEventStatus GestureEventListener::HandleDoubleTap(const MultiTouchInput& aEvent)
{
TapGestureInput tapEvent(TapGestureInput::TAPGESTURE_DOUBLE, aEvent.mTime, aEvent.mTouches[0].mScreenPoint);
return mAsyncPanZoomController->HandleInputEvent(tapEvent);
}
void GestureEventListener::TimeoutDoubleTap()
{
// If we haven't gotten another tap by now, reset the state and treat it as a
// single tap. It couldn't have been a double tap.
if (mState == GESTURE_WAITING_DOUBLE_TAP) {
mState = GESTURE_NONE;
HandleSingleTapConfirmedEvent(mLastTouchInput);
}
}
AsyncPanZoomController* GestureEventListener::GetAsyncPanZoomController() {
return mAsyncPanZoomController;
}

View File

@ -55,16 +55,18 @@ public:
protected:
enum GestureState {
NoGesture = 0,
InPinchGesture
// There's no gesture going on, and we don't think we're about to enter one.
GESTURE_NONE,
// There's a pinch happening, which occurs when there are two touch inputs.
GESTURE_PINCH,
// A touch start has happened and it may turn into a tap. We use this
// because, if we put down two fingers and then lift them very quickly, this
// may be mistaken for a tap.
GESTURE_WAITING_SINGLE_TAP,
// A single tap has happened for sure, and we're waiting for a second tap.
GESTURE_WAITING_DOUBLE_TAP
};
/**
* Maximum time for a touch on the screen and corresponding lift of the finger
* to be considered a tap.
*/
enum { MAX_TAP_TIME = 500 };
/**
* Attempts to handle the event as a pinch event. If it is not a pinch event,
* then we simply tell the next consumer to consume the event instead.
@ -100,6 +102,24 @@ protected:
*/
nsEventStatus HandleTapCancel(const MultiTouchInput& aEvent);
/**
* Attempts to handle a double tap. This happens when we get two single taps
* within a short time. In general, this will not attempt to block the touch
* event from being passed along to AsyncPanZoomController since APZC needs to
* know about touches ending (and we only know if a touch was a double tap
* once it ends).
*/
nsEventStatus HandleDoubleTap(const MultiTouchInput& aEvent);
/**
* Times out a single tap we think may be turned into a double tap. This will
* also send a single tap if we're still in the "WaitingDoubleTap" state when
* this is called. This should be called a short time after a single tap is
* detected, and the delay on it should be enough that the user has time to
* tap again (to make a double tap).
*/
void TimeoutDoubleTap();
nsRefPtr<AsyncPanZoomController> mAsyncPanZoomController;
/**
@ -107,6 +127,10 @@ protected:
* this array, even if we choose not to handle it. When it ends, we remove it.
*/
nsTArray<SingleTouchData> mTouches;
/**
* Current gesture we're dealing with.
*/
GestureState mState;
/**
@ -117,9 +141,28 @@ protected:
/**
* Stores the time a touch started, used for detecting a tap gesture. Only
* valid when there's exactly one touch in mTouches.
* valid when there's exactly one touch in mTouches. This is the time that the
* first touch was inserted into the array. This is a PRUint64 because it is
* initialized from interactions with InputData, which stores its timestamps as
* a PRUint64.
*/
PRUint64 mTouchStartTime;
PRUint64 mTapStartTime;
/**
* Cached copy of the last touch input, only valid when in the
* "WaitingDoubleTap" state. This is used to forward along to
* AsyncPanZoomController if a single tap needs to be sent (since it is sent
* shortly after the user actually taps, since we need to wait for a double
* tap).
*/
MultiTouchInput mLastTouchInput;
/**
* Task used to timeout a double tap. This gets posted to the UI thread such
* that it runs a short time after a single tap happens. We cache it so that
* we can cancel it if a double tap actually comes in.
*/
CancelableTask *mDoubleTapTimeoutTask;
};
}