Bug 639179 Part 3: Tapping during kinetic panning stops panning r=mbrubeck
authorBenjamin Stover <bstover@mozilla.com>
Mon, 11 Apr 2011 13:52:43 -0700
changeset 67857 ba45ce3791cc8b4d664b50208f2befc7e50cdcf4
parent 67856 d42f0af7b2f76017a6dbb7590dcbdf097fd7c787
child 67858 171b42492ec326dd8a33f148bfec2a251e613aa3
push id1
push userroot
push dateTue, 26 Apr 2011 22:38:44 +0000
treeherdermozilla-beta@bfdb6e623a36 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmbrubeck
bugs639179
milestone2.2a1pre
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 639179 Part 3: Tapping during kinetic panning stops panning r=mbrubeck
mobile/chrome/content/input.js
--- a/mobile/chrome/content/input.js
+++ b/mobile/chrome/content/input.js
@@ -55,16 +55,23 @@ const kOverTapWait = 150;
 const kLongTapWait = 500;
 
 // maximum drag distance in inches while axis locking can still be reverted
 const kAxisLockRevertThreshold = 0.8;
 
 // Same as NS_EVENT_STATE_ACTIVE from nsIEventStateManager.h
 const kStateActive = 0x00000001;
 
+// After a drag begins, kinetic panning is stopped if the drag doesn't become
+// a pan in 300 milliseconds.
+const kStopKineticPanOnDragTimeout = 300;
+
+// Max velocity of a pan. This is in pixels/millisecond.
+const kMaxVelocity = 6;
+
 /**
  * MouseModule
  *
  * Handles all touch-related input such as dragging and tapping.
  *
  * The Fennec chrome DOM tree has elements that are augmented dynamically with
  * custom JS properties that tell the MouseModule they have custom support for
  * either dragging or clicking.  These JS properties are JS objects that expose
@@ -200,29 +207,31 @@ MouseModule.prototype = {
     // stop kinetic panning if targetScrollbox has changed
     if (this._kinetic.isActive() && this._dragger != dragger)
       this._kinetic.end();
 
     this._targetScrollbox = targetScrollInterface ? targetScrollInterface.element : targetScrollbox;
     this._targetScrollInterface = targetScrollInterface;
 
     // Do tap
-    let event = document.createEvent("Events");
-    event.initEvent("TapDown", true, true);
-    event.clientX = aEvent.clientX;
-    event.clientY = aEvent.clientY;
-    let success = aEvent.target.dispatchEvent(event);
-    if (success) {
-      this._recordEvent(aEvent);
-      this._target = aEvent.target;
-      this._mouseOverTimeout.once(kOverTapWait);
-      this._longClickTimeout.once(kLongTapWait);
-    } else {
-      // cancel all pending content clicks
-      this._cleanClickBuffer();
+    if (!this._kinetic.isActive()) {
+      let event = document.createEvent("Events");
+      event.initEvent("TapDown", true, true);
+      event.clientX = aEvent.clientX;
+      event.clientY = aEvent.clientY;
+      let success = aEvent.target.dispatchEvent(event);
+      if (success) {
+        this._recordEvent(aEvent);
+        this._target = aEvent.target;
+        this._mouseOverTimeout.once(kOverTapWait);
+        this._longClickTimeout.once(kLongTapWait);
+      } else {
+        // cancel all pending content clicks
+        this._cleanClickBuffer();
+      }
     }
 
     // Do pan
     if (dragger) {
       let draggable = dragger.isDraggable(targetScrollbox, targetScrollInterface);
       dragData.locked = !draggable.x || !draggable.y;
       if (draggable.x || draggable.y) {
         this._dragger = dragger;
@@ -353,60 +362,67 @@ MouseModule.prototype = {
 
   /**
    * Inform our dragger of a dragStart.
    */
   _doDragStart: function _doDragStart(aEvent, aDraggable) {
     let dragData = this._dragData;
     dragData.setDragStart(aEvent.screenX, aEvent.screenY, aDraggable);
     this._kinetic.addData(0, 0);
+    this._dragStartTime = Date.now();
     if (!this._kinetic.isActive())
       this._dragger.dragStart(aEvent.clientX, aEvent.clientY, aEvent.target, this._targetScrollInterface);
   },
 
   /** Finish a drag. */
   _doDragStop: function _doDragStop() {
     let dragData = this._dragData;
     if (!dragData.dragging)
       return;
 
     dragData.endDrag();
 
     // Note: it is possible for kinetic scrolling to be active from a
     // mousedown/mouseup event previous to this one. In this case, we
     // want the kinetic panner to tell our drag interface to stop.
 
-    if (!dragData.isPan() && !this._kinetic.isActive()) {
-      // There was no pan and no kinetic scrolling, so just stop dragger.
+    if (dragData.isPan()) {
+      if (Date.now() - this._dragStartTime > kStopKineticPanOnDragTimeout)
+        this._kinetic._velocity.set(0, 0);
+      // Start kinetic pan.
+      this._kinetic.start();
+    } else {
+      this._kinetic.end();
       this._dragger.dragStop(0, 0, this._targetScrollInterface);
       this._dragger = null;
-    } else if (dragData.isPan()) {
-      // Start kinetic pan.
-      this._kinetic.start();
     }
   },
 
   /**
    * Used by _onMouseMove() above and by KineticController's timer to do the
    * actual dragMove signalling to the dragger.  We'd put this in _onMouseMove()
    * but then KineticController would be adding to its own data as it signals
    * the dragger of dragMove()s.
    */
   _dragBy: function _dragBy(dX, dY, aIsKinetic) {
     let dragged = true;
+    let dragData = this._dragData;
     if (!this._waitingForPaint || aIsKinetic) {
       let dragData = this._dragData;
       dragged = this._dragger.dragMove(dX, dY, this._targetScrollInterface, aIsKinetic);
       if (dragged && !this._waitingForPaint) {
         this._waitingForPaint = true;
         mozRequestAnimationFrame(this);
       }
       this.dX = 0;
       this.dY = 0;
     }
+    if (!dragData.isPan())
+      this._kinetic.pause();
+
     return dragged;
   },
 
   /** Callback for kinetic scroller. */
   _kineticStop: function _kineticStop() {
     // Kinetic panning could finish while user is panning, so don't finish
     // the pan just yet.
     let dragData = this._dragData;
@@ -826,16 +842,17 @@ function KineticController(aPanBy, aEndC
   this._swipeLength = Services.prefs.getIntPref("browser.ui.kinetic.swipeLength");
 
   this._reset();
 }
 
 KineticController.prototype = {
   _reset: function _reset() {
     this._active = false;
+    this._paused = false;
     this.momentumBuffer = [];
     this._velocity.set(0, 0);
   },
 
   isActive: function isActive() {
     return this._active;
   },
 
@@ -867,17 +884,19 @@ KineticController.prototype = {
 
     // Temporary "bins" so that we don't create new objects during pan.
     let aBin = new Point(0, 0);
     let v0Bin = new Point(0, 0);
     let self = this;
 
     let callback = {
       onBeforePaint: function kineticHandleEvent(timeStamp) {
-        if (!self.isActive())  // someone called end() on us between timer intervals
+        // Someone called end() on us between timer intervals
+        // or we are paused.
+        if (!self.isActive() || self._paused)
           return;
 
         // To make animation end fast enough but to keep smoothness, average the ideal
         // time frame (smooth animation) with the actual time lapse (end fast enough).
         // Animation will never take longer than 2 times the ideal length of time.
         let realt = timeStamp - self._initialTime;
         self._time += self._updateInterval;
         let t = (self._time + realt) / 2;
@@ -918,16 +937,17 @@ KineticController.prototype = {
             self.end();
           else
             mozRequestAnimationFrame(this);
         }
       }
     };
 
     this._active = true;
+    this._paused = false;
     mozRequestAnimationFrame(callback);
   },
 
   start: function start() {
     function sign(x) {
       return x ? ((x > 0) ? 1 : -1) : 0;
     }
 
@@ -944,34 +964,51 @@ KineticController.prototype = {
     for (let i = 0; i < mblen; i++) {
       me = mb[i];
       if (lastTime - me.t < swipeLength) {
         distanceX += me.dx;
         distanceY += me.dy;
       }
     }
 
-    // Only allow kinetic scrolling to speed up if kinetic scrolling is active.
-    this._velocity.x = (distanceX < 0 ? Math.min : Math.max)((distanceX / swipeLength) * this._speedSensitivity, this._velocity.x);
-    this._velocity.y = (distanceY < 0 ? Math.min : Math.max)((distanceY / swipeLength) * this._speedSensitivity, this._velocity.y);
+    let currentVelocityX = 0;
+    let currentVelocityY = 0;
+
+    if (this.isActive()) {
+      let currentTime = Date.now() - this._initialTime;
+      currentVelocityX = Util.clamp(this._velocity.x + this._acceleration.x * currentTime, -kMaxVelocity, kMaxVelocity);
+      currentVelocityY = Util.clamp(this._velocity.y + this._acceleration.y * currentTime, -kMaxVelocity, kMaxVelocity);
+    }
+
+    if (currentVelocityX * this._velocity.x <= 0)
+      currentVelocityX = 0;
+    if (currentVelocityY * this._velocity.y <= 0)
+      currentVelocityY = 0;
+
+    this._velocity.x = clampFromZero((distanceX / swipeLength) + currentVelocityX, Math.abs(currentVelocityX), kMaxVelocity);
+    this._velocity.y = clampFromZero((distanceY / swipeLength) + currentVelocityY, Math.abs(currentVelocityY), kMaxVelocity);
 
     // Set acceleration vector to opposite signs of velocity
     this._acceleration.set(this._velocity.clone().map(sign).scale(-this._decelerationRate));
 
     this._position.set(0, 0);
     this._initialTime = mozAnimationStartTime;
     this._time = 0;
     this.momentumBuffer = [];
 
-    if (!this.isActive())
+    if (!this.isActive() || this._paused)
       this._startTimer();
 
     return true;
   },
 
+  pause: function pause() {
+    this._paused = true;
+  },
+
   end: function end() {
     if (this.isActive()) {
       if (this._beforeEnd)
         this._beforeEnd();
       this._reset();
     }
   },