Revision: 10198
Author:   brianp...@google.com
Date:     Thu May 19 11:23:57 2011
Log:      Improving the user experience and fixing some bugs.

Review by: jlaba...@google.com
http://code.google.com/p/google-web-toolkit/source/detail?r=10198

Modified:
 /trunk/user/src/com/google/gwt/touch/client/TouchScroller.java
 /trunk/user/test/com/google/gwt/touch/client/TouchScrollTest.java

=======================================
--- /trunk/user/src/com/google/gwt/touch/client/TouchScroller.java Wed Apr 13 10:30:02 2011 +++ /trunk/user/src/com/google/gwt/touch/client/TouchScroller.java Thu May 19 11:23:57 2011
@@ -30,6 +30,7 @@
 import com.google.gwt.event.dom.client.TouchMoveHandler;
 import com.google.gwt.event.dom.client.TouchStartEvent;
 import com.google.gwt.event.dom.client.TouchStartHandler;
+import com.google.gwt.event.logical.shared.AttachEvent;
 import com.google.gwt.event.logical.shared.ResizeEvent;
 import com.google.gwt.event.logical.shared.ResizeHandler;
 import com.google.gwt.event.shared.HandlerRegistration;
@@ -41,6 +42,7 @@
 import com.google.gwt.user.client.ui.HasScrolling;

 import java.util.ArrayList;
+import java.util.Iterator;
 import java.util.List;

 /**
@@ -125,6 +127,7 @@
       });
     }

+    @Override
     public boolean execute() {
       /*
        * Stop the command if another touch event starts or if momentum is
@@ -154,6 +157,22 @@
        * want to respect the end position that the momentum returns.
        */
       setWidgetScrollPosition(state.getPosition());
+
+      /*
+       * If the scroll position reached the min/max of the scrollable area,
+       * finish the momentum command.
+       */
+      int hPos = (int) state.getPosition().getX();
+      int hMin = widget.getMinimumHorizontalScrollPosition();
+      int hMax = widget.getMaximumHorizontalScrollPosition();
+      int vMin = widget.getMinimumVerticalScrollPosition();
+      int vMax = widget.getMaximumVerticalScrollPosition();
+      int vPos = (int) state.getPosition().getY();
+ if ((vMax <= vPos || vMin >= vPos) && (hMax <= hPos || hMin >= hPos)) {
+        finish();
+        return false;
+      }
+
       return notDone;
     }

@@ -167,10 +186,29 @@
       }
       if (this == momentumCommand) {
         momentumCommand = null;
-        setBustNextClick(false);
       }
     }
   }
+
+  /**
+ * The command used to remove touch points that occurred while the scrolling
+   * momentum is active. Only the touch points that occurred before
+   * TIME_THRESHOLD get to be removed.
+   */
+  private class MomentumTouchRemovalCommand implements RepeatingCommand {
+    @Override
+    public boolean execute() {
+      double currentTime = Duration.currentTimeMillis();
+ Iterator<TemporalPoint> iter = touchPositionsDuringMomentum.iterator();
+      while (iter.hasNext()) {
+        TemporalPoint point = iter.next();
+        if (currentTime - point.getTime() >= TIME_THRESHOLD) {
+          iter.remove();
+        }
+      }
+      return !touchPositionsDuringMomentum.isEmpty();
+    }
+  }

   /**
    * The number of frames per second the animation should run at.
@@ -193,6 +231,20 @@
    */
   private static final double MIN_TRACKING_FOR_DRAG = 5;

+  /**
+ * The threshold for how close the time of a click event has to be to that of
+   * a touch event for us to consider those events to be related; that is,
+   * caused by the same user action (tap).
+   */
+  private static final int TIME_THRESHOLD = 2500;
+
+  /**
+ * The threshold for how close the coordinates of a click event has to be to
+   * those of a touch event for us to consider those events to be related;
+   * that is, caused by the same user action (tap).
+   */
+  private static final int DISTANCE_THRESHOLD = 25;
+
   /**
    * The number of milliseconds per animation frame.
    */
@@ -262,7 +314,12 @@
   /**
    * The registration for the preview handler used to bust click events.
    */
-  private HandlerRegistration bustClickHandler;
+  private HandlerRegistration bustClickHandlerReg;
+
+  /**
+   * The registration for the attach event handler.
+   */
+  private HandlerRegistration attachHandlerReg;

   /**
* A boolean indicating that we are in a drag sequence. Dragging occurs after
@@ -273,7 +330,8 @@
   /**
    * Registrations for the handlers added to the widget.
    */
- private final List<HandlerRegistration> handlerRegs = new ArrayList<HandlerRegistration>();
+  private final List<HandlerRegistration> handlerRegs =
+      new ArrayList<HandlerRegistration>();

   /**
* The last (most recent) touch position. We need to keep track of this when
@@ -312,6 +370,26 @@
    */
   private TemporalPoint recentTouchPositionOnDeck;

+  /**
+ * The coordinate of the most recent touch event that triggered scrolling.
+   */
+  private TemporalPoint recentScrollTriggeringTouchPosition =
+      new TemporalPoint();
+
+  /**
+ * The coordinates of the touch positions while the momentum is active. The
+   * points stored in this array will be removed after TIME_THRESHOLD.
+   */
+  private List<TemporalPoint> touchPositionsDuringMomentum =
+      new ArrayList<TemporalPoint>();
+
+  /**
+ * The repeating command used to remove the touch points that were added to
+   * touchPositionsDuringMomentum after TIME_THRESHOLD.
+   */
+  private RepeatingCommand momentumTouchRemovalCommand =
+      new MomentumTouchRemovalCommand();
+
   /**
    * The position of the scrollable when the first touch occured.
    */
@@ -387,21 +465,37 @@

     // Cancel drag and momentum.
     cancelAll();
-    setBustNextClick(false);

     // Release the old widget.
-    if (this.widget != null) {
-      for (HandlerRegistration reg : handlerRegs) {
-        reg.removeHandler();
-      }
-      handlerRegs.clear();
-    }
+    for (HandlerRegistration reg : handlerRegs) {
+      reg.removeHandler();
+    }
+    handlerRegs.clear();
+    removeAttachHandler();
+    removeBustClickHandler();

     // Attach to the new widget.
     this.widget = widget;
     if (widget != null) {
+      if (widget.asWidget().isAttached()) {
+        setupBustClickHandler();
+      }
+
+ // Make sure to add/remove the bust click handler if the widget is attached/detached. + attachHandlerReg = widget.asWidget().addAttachHandler(new AttachEvent.Handler() {
+        @Override
+        public void onAttachOrDetach(AttachEvent event) {
+          if (event.isAttached()) {
+            setupBustClickHandler();
+          } else {
+            removeBustClickHandler();
+          }
+        }
+      });
+
       // Add touch start handler.
handlerRegs.add(widget.asWidget().addDomHandler(new TouchStartHandler() {
+        @Override
         public void onTouchStart(TouchStartEvent event) {
           TouchScroller.this.onTouchStart(event);
         }
@@ -409,6 +503,7 @@

       // Add touch move handler.
handlerRegs.add(widget.asWidget().addDomHandler(new TouchMoveHandler() {
+        @Override
         public void onTouchMove(TouchMoveEvent event) {
           TouchScroller.this.onTouchMove(event);
         }
@@ -416,6 +511,7 @@

       // Add touch end handler.
handlerRegs.add(widget.asWidget().addDomHandler(new TouchEndHandler() {
+        @Override
         public void onTouchEnd(TouchEndEvent event) {
           TouchScroller.this.onTouchEnd(event);
         }
@@ -423,6 +519,7 @@

       // Add touch cancel handler.
handlerRegs.add(widget.asWidget().addDomHandler(new TouchCancelHandler() {
+        @Override
         public void onTouchCancel(TouchCancelEvent event) {
           TouchScroller.this.onTouchCancel(event);
         }
@@ -515,7 +612,7 @@

   /**
    * Called when the user moves a touch.
-   *
+   *
    * @param event the touch event
    */
   protected void onTouchMove(TouchEvent<?> event) {
@@ -534,6 +631,9 @@
       double absDiffX = Math.abs(diff.getX());
       double absDiffY = Math.abs(diff.getY());
if (absDiffX > MIN_TRACKING_FOR_DRAG || absDiffY > MIN_TRACKING_FOR_DRAG) {
+        recentScrollTriggeringTouchPosition.setTemporalPoint(
+            recentTouchPosition.getPoint(), recentTouchPosition.getTime());
+
         /*
          * Check if we should defer to native scrolling. If the scrollable
* widget is already scrolled as far as it will go, then we don't want
@@ -624,19 +724,16 @@
    * @param event the touch event
    */
   protected void onTouchStart(TouchEvent<?> event) {
+    // Touch start event always precedes the click event. So this
+    // recentScrollTriggeringTouchPosition gets reset before a click event
+ // occurs. That's why we don't need to compare the time of the ghost click
+    // and the recentScrollTriggeringTouchPosition.
+    recentScrollTriggeringTouchPosition.setTemporalPoint(null, 0);
+
     // Ignore the touch if there is already a touch happening.
     if (touching) {
       return;
     }
-
-    /*
- * If the user touches the screen while momentum is scrolling, bust the next - * click event. They probably want to pause the momentum, not click an item.
-     */
-    setBustNextClick(isMomentumActive());
-
-    cancelAll();
-    touching = true;

     // Record the starting touch position.
     Touch touch = getTouchFromEvent(event);
@@ -645,9 +742,25 @@
recentTouchPosition.setTemporalPoint(startTouchPosition, startTouchTime);
     lastTouchPosition.setTemporalPoint(startTouchPosition, startTouchTime);
     recentTouchPositionOnDeck = null;
+
+    /**
+ * If the user touches the screen while the scrolling momentum is active, + * add the touch point to an array so that the later click events can be
+     * compared to the points in the array and busted if it shouldn't be
+     * considered as click.
+     */
+    if (isMomentumActive()) {
+      touchPositionsDuringMomentum.add(
+          new TemporalPoint(startTouchPosition, startTouchTime));
+      Scheduler.get().scheduleFixedDelay(
+          momentumTouchRemovalCommand, TIME_THRESHOLD);
+    }

     // Record the starting scroll position.
     startScrollPosition = getWidgetScrollPosition();
+
+    cancelAll();
+    touching = true;
   }

   /**
@@ -712,6 +825,60 @@
   boolean isTouching() {
     return touching;
   }
+
+  /**
+   * Removes the attach handler. Visible for testing.
+   */
+  void removeAttachHandler() {
+    if (attachHandlerReg != null) {
+      attachHandlerReg.removeHandler();
+      attachHandlerReg = null;
+    }
+  }
+
+  /**
+   * Removes the bust click handler. Visible for testing.
+   */
+  void removeBustClickHandler() {
+    if (bustClickHandlerReg != null) {
+      bustClickHandlerReg.removeHandler();
+      bustClickHandlerReg = null;
+    }
+  }
+
+  /**
+   * Sets up the bust click handler. Visible for testing.
+   */
+  void setupBustClickHandler() {
+    removeBustClickHandler();
+ bustClickHandlerReg = Event.addNativePreviewHandler(new NativePreviewHandler() {
+      @Override
+      public void onPreviewNativeEvent(NativePreviewEvent event) {
+        if (Event.ONCLICK == event.getTypeInt()) {
+          Point clickPoint = new Point(
+              event.getNativeEvent().getClientX(),
+              event.getNativeEvent().getClientY());
+
+          /*
+           * In mobile browsers, when the user touches on the screen, drag
+           * his finger across and take his finger off, the browser fires a
+           * click event at the position where the touch start happen. This
+ * click event is called "ghost click". This click is not intended by
+           * the user and thus needs to be busted.
+ * Also, while the panel is scrolling, if the user taps on the screen + * we want to stop the scrolling and bust the click event that's fired
+           * because the user probably didn't mean to click on something.
+           */
+          if (isClickScrollTriggeringTouch(clickPoint) ||
+              isClickTouchPositionDuringMomentum(clickPoint)) {
+            event.cancel();
+            event.getNativeEvent().stopPropagation();
+            event.getNativeEvent().preventDefault();
+          }
+        }
+      }
+    });
+  }

   /**
    * Cancel all existing touch, drag, and momentum.
@@ -730,29 +897,60 @@
   }

   /**
-   * Set whether or not we should bust the next click.
-   *
-   * @param doBust true to bust the next click, false not to
+   * Tests if the given points fall within the threshold from each other.
+   *
+   * @param point1 first point
+   * @param point2 second point
+   * @return true if the points fall within the threshold from each other
    */
-  private void setBustNextClick(boolean doBust) {
-    if (doBust && bustClickHandler == null) {
- bustClickHandler = Event.addNativePreviewHandler(new NativePreviewHandler() {
-        public void onPreviewNativeEvent(NativePreviewEvent event) {
-          if (Event.ONCLICK == event.getTypeInt()) {
-            event.getNativeEvent().stopPropagation();
-            event.getNativeEvent().preventDefault();
-            setBustNextClick(false);
-          }
-        }
-      });
-    } else if (!doBust && bustClickHandler != null) {
-      bustClickHandler.removeHandler();
-      bustClickHandler = null;
-    }
+  private boolean hitTest(Point point1, Point point2) {
+    Point diff = point1.minus(point2);
+    double absDiffX = Math.abs(diff.getX());
+    double absDiffY = Math.abs(diff.getY());
+ return (absDiffX <= DISTANCE_THRESHOLD) && (absDiffY <= DISTANCE_THRESHOLD);
+  }
+
+  /**
+ * Checks if the click event is related to the touch event that has triggered + * scrolling. Such click events occur as a result of ghost click (i.e. click
+   * event that is fired at the touch start position after the touch end
+   * happens) and should be busted.
+   *
+   * @param clickPoint point of the click event
+   * @return true if clickPoint is the same as the recent scroll triggering
+   *     touch
+   */
+  private boolean isClickScrollTriggeringTouch(Point clickPoint) {
+    if (recentScrollTriggeringTouchPosition.getPoint() != null) {
+      return hitTest(
+          clickPoint, recentScrollTriggeringTouchPosition.getPoint());
+    }
+    return false;
   }

   /**
-   * Set the scroll position of the widget.
+   * Checks if the click event point is related to the touch positions
+   * during the recent momentum scrolling.
+   *
+   * @param clickPoint point of the click event
+ * @return true if clickPoint is the same as one of the touch positions during
+   *     the recent momentum scrolling
+   */
+  private boolean isClickTouchPositionDuringMomentum(Point clickPoint) {
+    double currentTime = Duration.currentTimeMillis();
+    boolean same = false;
+    for (TemporalPoint point : touchPositionsDuringMomentum) {
+      if (currentTime - point.getTime() <= TIME_THRESHOLD &&
+          hitTest(clickPoint, point.getPoint())) {
+        same = true;
+        break;
+      }
+    }
+    return same;
+  }
+
+  /**
+   * Sets the scroll position of the widget.
    *
    * @param position the new position
    */
=======================================
--- /trunk/user/test/com/google/gwt/touch/client/TouchScrollTest.java Wed Apr 13 10:30:02 2011 +++ /trunk/user/test/com/google/gwt/touch/client/TouchScrollTest.java Thu May 19 11:23:57 2011
@@ -116,6 +116,9 @@
    */
   private static class CustomTouchScroller extends TouchScroller {

+    private boolean setupBustClickHandlerCalled;
+    private boolean removeBustClickHandlerCalled;
+    private boolean removeAttachHandlerCalled;
     private boolean onDragEndCalled;
     private boolean onDragMoveCalled;
     private boolean onDragStartCalled;
@@ -139,6 +142,21 @@
       assertEquals(expected, onDragStartCalled);
       onDragStartCalled = false;
     }
+
+    public void assertSetupBustClickHandlerCalled(boolean expected) {
+      assertEquals(expected, setupBustClickHandlerCalled);
+      setupBustClickHandlerCalled = false;
+    }
+
+    public void assertRemoveBustClickHandlerCalled(boolean expected) {
+      assertEquals(expected, removeBustClickHandlerCalled);
+      removeBustClickHandlerCalled = false;
+    }
+
+    public void assertRemoveAttachHandlerCalled(boolean expected) {
+      assertEquals(expected, removeAttachHandlerCalled);
+      removeAttachHandlerCalled = false;
+    }

     @Override
     protected void onDragEnd(TouchEvent<?> event) {
@@ -160,6 +178,24 @@
       super.onDragStart(event);
       onDragStartCalled = true;
     }
+
+    @Override
+    protected void setupBustClickHandler() {
+      super.setupBustClickHandler();
+      setupBustClickHandlerCalled = true;
+    }
+
+    @Override
+    protected void removeBustClickHandler() {
+      super.removeBustClickHandler();
+      removeBustClickHandlerCalled = true;
+    }
+
+    @Override
+    protected void removeAttachHandler() {
+      super.removeAttachHandler();
+      removeAttachHandlerCalled = true;
+    }
   }

   /**
@@ -340,6 +376,32 @@
     assertFalse(scroller.isTouching());
     assertFalse(scroller.isDragging());
   }
+
+  /**
+   * Test that the bust click/attach event handler is removed from the old
+   * widget.
+   */
+  public void testHandlersRemovedFromOldWidget() {
+    CustomScrollPanel newScrollPanel = new CustomScrollPanel();
+
+    // Initial state.
+    scroller.assertRemoveBustClickHandlerCalled(true);
+    scroller.assertRemoveAttachHandlerCalled(true);
+
+ // Replace the old widget (scrollPanel) with the new widget (newScrollPanel)
+    scroller.setTargetWidget(newScrollPanel);
+
+ // Verify that the bust click handler and attach event handler are removed.
+    scroller.assertRemoveBustClickHandlerCalled(true);
+    scroller.assertRemoveAttachHandlerCalled(true);
+
+    // Remove the old widget (scrollPanel) from the root panel.
+    RootPanel.get().remove(scrollPanel);
+
+ // Verify that removing the old widget doesn't cause removeBustClickHandler
+    // from being called.
+    scroller.assertRemoveBustClickHandlerCalled(false);
+  }

   /**
    * Test that when momentum ends, the momentum command is set to null (and
@@ -531,6 +593,27 @@
     assertTrue(scroller.isTouching());
     assertFalse(scroller.isDragging());
   }
+
+  /**
+ * Test that the setupBustClickHandler is called when the widget is detached
+   * and re-attached.
+   */
+  public void testSetupBustClickHandler() {
+    // Initial state.
+    scroller.assertRemoveBustClickHandlerCalled(true);
+    scroller.assertRemoveAttachHandlerCalled(true);
+    scroller.assertSetupBustClickHandlerCalled(true);
+
+    RootPanel.get().remove(scrollPanel);
+
+    // Verify that the bust click handler is removed.
+    scroller.assertRemoveBustClickHandlerCalled(true);
+
+    RootPanel.get().add(scrollPanel);
+
+    // Verify that the bust click handler is setup.
+    scroller.assertSetupBustClickHandlerCalled(true);
+  }

   @Override
   protected void gwtSetUp() throws Exception {

--
http://groups.google.com/group/Google-Web-Toolkit-Contributors

Reply via email to