Bug 775463: Recognize double tap gestures while still supporting single taps
☠☠ backed out by 02d63f48e752 ☠ ☠
authorDoug Sherk <dsherk2@mozilla.com>
Fri, 27 Jul 2012 17:23:51 -0700
changeset 100793 e0e33c1c7c17aeff411248076b5280011a138d26
parent 100792 5bff8785ab1b17476f6cb37c03522f460b94cb6b
child 100794 3d0fb7ac961a4bcd988c3b5aa2d3f0a70f8e1962
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
bugs775463
milestone17.0a1
Bug 775463: Recognize double tap gestures while still supporting single taps
gfx/layers/ipc/GestureEventListener.cpp
gfx/layers/ipc/GestureEventListener.h
--- a/gfx/layers/ipc/GestureEventListener.cpp
+++ b/gfx/layers/ipc/GestureEventListener.cpp
@@ -1,23 +1,34 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set sw=4 ts=8 et tw=80 : */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * 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)
 {
 }
 
 GestureEventListener::~GestureEventListener()
 {
 }
 
 nsEventStatus GestureEventListener::HandleInputEvent(const InputData& aEvent)
@@ -44,17 +55,23 @@ nsEventStatus GestureEventListener::Hand
       // If we didn't find a touch in our list that matches this, then add it.
       // If it already existed, we don't want to add it twice because that
       // messes with our touch move/end code.
       if (!foundAlreadyExistingTouch) {
         mTouches.AppendElement(event.mTouches[i]);
       }
     }
 
-    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);
     }
 
     break;
   }
   case MultiTouchInput::MULTITOUCH_MOVE: {
     // If we move at all, just bail out of the tap. We need to change this so
@@ -84,28 +101,47 @@ nsEventStatus GestureEventListener::Hand
           foundAlreadyExistingTouch = true;
           mTouches.RemoveElementAt(j);
         }
       }
     }
 
     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();
+
+        // 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 (HandleSingleTapConfirmedEvent(event) == nsEventStatus_eConsumeNoDefault) {
-        return nsEventStatus_eConsumeNoDefault;
-      }
+    if (mState == GESTURE_WAITING_SINGLE_TAP) {
+      mState = GESTURE_NONE;
     }
 
     break;
   }
   case MultiTouchInput::MULTITOUCH_CANCEL:
     // This gets called if there's a touch that has to bail for weird reasons
     // like pinching and then moving away from the window that the pinch was
     // started in without letting go of the screen.
@@ -125,84 +161,104 @@ nsEventStatus GestureEventListener::Hand
                       secondTouch = mTouches[mTouches.Length() - 1].mScreenPoint;
     nsIntPoint focusPoint =
       nsIntPoint((firstTouch.x + secondTouch.x)/2,
                  (firstTouch.y + secondTouch.y)/2);
     float currentSpan =
       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,
                                    currentSpan,
                                    currentSpan);
 
       mAsyncPanZoomController->HandleInputEvent(pinchEvent);
 
-      mState = InPinchGesture;
+      mState = GESTURE_PINCH;
     } else {
       PinchGestureInput pinchEvent(PinchGestureInput::PINCHGESTURE_SCALE,
                                    aEvent.mTime,
                                    focusPoint,
                                    currentSpan,
                                    mPreviousSpan);
 
       mAsyncPanZoomController->HandleInputEvent(pinchEvent);
     }
 
     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;
   }
 
   if (aClearTouches) {
     mTouches.Clear();
   }
 
   return rv;
 }
 
 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;
 }
 
 }
 }
--- a/gfx/layers/ipc/GestureEventListener.h
+++ b/gfx/layers/ipc/GestureEventListener.h
@@ -50,27 +50,29 @@ public:
   /**
    * Returns the AsyncPanZoomController stored on this class and used for
    * callbacks.
    */
   AsyncPanZoomController* GetAsyncPanZoomController();
 
 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.
    *
    * |aClearTouches| marks whether or not to terminate any pinch currently
    * happening.
    */
   nsEventStatus HandlePinchGestureEvent(const MultiTouchInput& aEvent, bool aClearTouches);
 
@@ -95,34 +97,75 @@ protected:
    * Attempts to handle a tap event cancellation. This happens when we think
    * something was a tap but it actually wasn't. 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 tap once it ends).
    */
   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;
 
   /**
    * Array containing all active touches. When a touch happens it, gets added to
    * 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;
 
   /**
    * Previous span calculated for the purposes of setting inside a
    * PinchGestureInput.
    */
   float mPreviousSpan;
 
   /**
    * 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;
 };
 
 }
 }
 
 #endif