Revision: 9988
Author:   jlaba...@google.com
Date:     Wed Apr 13 10:30:02 2011
Log: Improving TouchScroller to allow native document level scrolling when appropriate. If the scrollable widget is scrolled as far as it can go in a direction, and the user tries to scroll in that direction, then we defer to document level scrolling. For example, in the Showcase app (which is not a mobile specific app), if you scroll the menu bar to the bottom, then scrolling it again allows the document to scroll down, revealing the rest of the page. It isn't perfect because we cannot disable native scrolling in one direction (as in, allow native horizontal but disable native vertical), but its a drastic improvement and make Showcase usable on mobile.

This change also fixes a bug in TouchScroller where it always busts the next click, even if Momentum has finished. The next click should only be busted if the user interupts momentum to stop it. Also, we cancel momentum on WindowResize (and by extension, orientation change) to account for the fact that resizing the scrollable widget will cause the contents to reflow, and the old scroll positions become stale.

Review at http://gwt-code-reviews.appspot.com/1410803

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

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 Mon Mar 21 12:22:19 2011 +++ /trunk/user/src/com/google/gwt/touch/client/TouchScroller.java Wed Apr 13 10:30:02 2011
@@ -30,11 +30,14 @@
 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.ResizeEvent;
+import com.google.gwt.event.logical.shared.ResizeHandler;
 import com.google.gwt.event.shared.HandlerRegistration;
 import com.google.gwt.touch.client.Momentum.State;
 import com.google.gwt.user.client.Event;
 import com.google.gwt.user.client.Event.NativePreviewEvent;
 import com.google.gwt.user.client.Event.NativePreviewHandler;
+import com.google.gwt.user.client.Window;
 import com.google.gwt.user.client.ui.HasScrolling;

 import java.util.ArrayList;
@@ -99,6 +102,7 @@
     private final Point initialPosition = getWidgetScrollPosition();
     private int lastElapsedMillis = 0;
     private State state;
+    private HandlerRegistration windowResizeHandler;

     /**
      * Construct a {@link MomentumCommand}.
@@ -107,6 +111,18 @@
      */
     public MomentumCommand(Point endVelocity) {
       state = momentum.createState(initialPosition, endVelocity);
+
+      /**
+ * If the user resizes the window (which happens on orientation change of + * a mobile device), cancel the momentum. The scrollable widget may be + * resized, which will cause its content to reflow and invalidates the
+       * current scrolling position.
+       */
+      windowResizeHandler = Window.addResizeHandler(new ResizeHandler() {
+        public void onResize(ResizeEvent event) {
+          finish();
+        }
+      });
     }

     public boolean execute() {
@@ -115,6 +131,7 @@
        * disabled.
        */
       if (this != momentumCommand) {
+        finish();
         return false;
       }

@@ -127,9 +144,9 @@
       // Calculate the new state.
       boolean notDone = momentum.updateState(state);

-      // Momementum is finished, so the user is free to click.
+      // Momentum is finished, so the user is free to click.
       if (!notDone) {
-        setBustNextClick(false);
+        finish();
       }

       /*
@@ -139,6 +156,20 @@
       setWidgetScrollPosition(state.getPosition());
       return notDone;
     }
+
+    /**
+     * Finish and cleanup this momentum command.
+     */
+    private void finish() {
+      if (windowResizeHandler != null) {
+        windowResizeHandler.removeHandler();
+        windowResizeHandler = null;
+      }
+      if (this == momentumCommand) {
+        momentumCommand = null;
+        setBustNextClick(false);
+      }
+    }
   }

   /**
@@ -492,9 +523,6 @@
     if (!touching) {
       return;
     }
-
-    // Prevent native scrolling.
-    event.preventDefault();

     // Check if we should start dragging.
     Touch touch = getTouchFromEvent(event);
@@ -506,11 +534,60 @@
       double absDiffX = Math.abs(diff.getX());
       double absDiffY = Math.abs(diff.getY());
if (absDiffX > MIN_TRACKING_FOR_DRAG || absDiffY > MIN_TRACKING_FOR_DRAG) {
+        /*
+         * 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
+         * to prevent scrolling of the document.
+         *
+         * We cannot prevent native scrolling in only one direction (ie. we
+ * cannot allow native horizontal scrolling but prevent native vertical + * scrolling), so we make a best guess based on the direction of the
+         * drag.
+         */
+        if (absDiffX > absDiffY) {
+          /*
+ * The user scrolled primarily in the horizontal direction, so check
+           * if we should defer left/right scrolling to the document.
+           */
+          int hPosition = widget.getHorizontalScrollPosition();
+          int hMin = widget.getMinimumHorizontalScrollPosition();
+          int hMax = widget.getMaximumHorizontalScrollPosition();
+          if (diff.getX() < 0 && hMax <= hPosition) {
+            // Already scrolled to the right.
+            cancelAll();
+            return;
+          } else if (diff.getX() > 0 && hMin >= hPosition) {
+            // Already scrolled to the left.
+            cancelAll();
+            return;
+          }
+        } else {
+          /*
+ * The user scrolled primarily in the vertical direction, so check if
+           * we should defer up/down scrolling to the document.
+           */
+          int vPosition = widget.getVerticalScrollPosition();
+          int vMin = widget.getMinimumVerticalScrollPosition();
+          int vMax = widget.getMaximumVerticalScrollPosition();
+          if (diff.getY() < 0 && vMax <= vPosition) {
+            // Already scrolled to the bottom.
+            cancelAll();
+            return;
+          } else if (diff.getY() > 0 && vMin >= vPosition) {
+            // Already scrolled to the top.
+            cancelAll();
+            return;
+          }
+        }
+
         // Start dragging.
         dragging = true;
         onDragStart(event);
       }
     }
+
+    // Prevent native document level scrolling.
+    event.preventDefault();

     if (dragging) {
       // Continue dragging.
@@ -647,8 +724,6 @@

   /**
    * Get the scroll position of the widget.
-   *
-   * @param position the current scroll position
    */
   private Point getWidgetScrollPosition() {
return new Point(widget.getHorizontalScrollPosition(), widget.getVerticalScrollPosition());
@@ -668,7 +743,7 @@
             event.getNativeEvent().preventDefault();
             setBustNextClick(false);
           }
-        };
+        }
       });
     } else if (!doBust && bustClickHandler != null) {
       bustClickHandler.removeHandler();
=======================================
--- /trunk/user/test/com/google/gwt/touch/client/TouchScrollTest.java Mon Mar 14 09:18:30 2011 +++ /trunk/user/test/com/google/gwt/touch/client/TouchScrollTest.java Wed Apr 13 10:30:02 2011
@@ -16,6 +16,7 @@
 package com.google.gwt.touch.client;

 import com.google.gwt.core.client.Duration;
+import com.google.gwt.core.client.Scheduler.RepeatingCommand;
 import com.google.gwt.dom.client.NativeEvent;
 import com.google.gwt.dom.client.Touch;
 import com.google.gwt.event.dom.client.TouchEvent;
@@ -56,9 +57,9 @@
      */
     public CustomScrollPanel() {
       this.minVerticalScrollPosition = 0;
-      this.maxVerticalScrollPosition = Integer.MAX_VALUE;
+      this.maxVerticalScrollPosition = 5000;
       this.minHorizontalScrollPosition = 0;
-      this.maxHorizontalScrollPosition = Integer.MAX_VALUE;
+      this.maxHorizontalScrollPosition = 5000;
     }

     @Override
@@ -206,7 +207,7 @@
     CustomTouchEvent event = new CustomTouchEvent();
     event.setNativeEvent(createNativeTouchEvent());
     return event;
-  };
+  }

   /**
    * Create a mock TouchMoveEvent for the specified x and y coordinate.
@@ -218,7 +219,7 @@
   private static TouchEvent<?> createTouchMoveEvent(int x, int y) {
     // TouchScroller doesn't care about the actual event subclass.
     return createTouchStartEvent(x, y);
-  };
+  }

   /**
* Create a mock {@link TouchStartEvent} for the specified x and y coordinate.
@@ -233,7 +234,7 @@
     nativeEvent.getTouches().push(createTouch(x, y));
     event.setNativeEvent(nativeEvent);
     return event;
-  };
+  }

   private CustomTouchScroller scroller;
   private CustomScrollPanel scrollPanel;
@@ -276,6 +277,22 @@
assertNull("TouchScroll created, but touch is not supported", scroller);
     }
   }
+
+  public void testDeferToNativeScrollingBottom() {
+ testDeferToNativeScrolling(0, scrollPanel.getMaximumVerticalScrollPosition(), 0, -100);
+  }
+
+  public void testDeferToNativeScrollingLeft() {
+    testDeferToNativeScrolling(0, 0, 100, 0);
+  }
+
+  public void testDeferToNativeScrollingRight() {
+ testDeferToNativeScrolling(scrollPanel.getMaximumHorizontalScrollPosition(), 0, -100, 0);
+  }
+
+  public void testDeferToNativeScrollingTop() {
+    testDeferToNativeScrolling(0, 0, 0, 100);
+  }

   /**
    * Test that touch events correctly initiate drag events.
@@ -295,21 +312,21 @@
     assertFalse(scroller.isDragging());

     // Move, but not enough to drag.
-    scroller.onTouchMove(createTouchMoveEvent(1, 0));
+    scroller.onTouchMove(createTouchMoveEvent(-1, 0));
     scroller.assertOnDragStartCalled(false);
     scroller.assertOnDragMoveCalled(false);
     assertTrue(scroller.isTouching());
     assertFalse(scroller.isDragging());

     // Move.
-    scroller.onTouchMove(createTouchMoveEvent(100, 0));
+    scroller.onTouchMove(createTouchMoveEvent(-100, 0));
     scroller.assertOnDragStartCalled(true);
     scroller.assertOnDragMoveCalled(true);
     assertTrue(scroller.isTouching());
     assertTrue(scroller.isDragging());

     // Move again.
-    scroller.onTouchMove(createTouchMoveEvent(200, 0));
+    scroller.onTouchMove(createTouchMoveEvent(-200, 0));
     scroller.assertOnDragStartCalled(false); // drag already started.
     scroller.assertOnDragMoveCalled(true);
     assertTrue(scroller.isTouching());
@@ -323,6 +340,39 @@
     assertFalse(scroller.isTouching());
     assertFalse(scroller.isDragging());
   }
+
+  /**
+   * Test that when momentum ends, the momentum command is set to null (and
+   * isMomentumActive() returns false).
+   */
+  public void testMomentumEnd() {
+    // Use a short lived momentum.
+    scroller.setMomentum(new DefaultMomentum() {
+      @Override
+      public boolean updateState(State state) {
+        // Immediately end momentum.
+        return false;
+      }
+    });
+
+    // Start a drag sequence.
+    double millis = Duration.currentTimeMillis();
+ scroller.getRecentTouchPosition().setTemporalPoint(new Point(0, 0), millis); + scroller.getLastTouchPosition().setTemporalPoint(new Point(100, 100), millis + 100);
+
+    // End the drag sequence.
+    scroller.onDragEnd(createTouchEndEvent());
+    scroller.assertOnDragEndCalled(true);
+    assertFalse(scroller.isTouching());
+    assertFalse(scroller.isDragging());
+    assertTrue(scroller.isMomentumActive());
+
+    // Force momentum to run, which causes it to end.
+    getMomentumCommand(scroller).execute();
+    assertFalse(scroller.isTouching());
+    assertFalse(scroller.isDragging());
+    assertFalse(scroller.isMomentumActive());
+  }

   public void testOnDragEnd() {
     // Start a drag sequence.
@@ -486,12 +536,14 @@
   protected void gwtSetUp() throws Exception {
     // Create and attach a widget that has scrolling.
     scrollPanel = new CustomScrollPanel();
-    scrollPanel.setTouchScrollingDisabled(true);
     scrollPanel.setPixelSize(500, 500);
     Label content = new Label("Content");
     content.setPixelSize(10000, 10000);
     RootPanel.get().add(scrollPanel);

+    // Disabled touch scrolling because we will add our own scroller.
+    scrollPanel.setTouchScrollingDisabled(true);
+
     // Add scrolling support.
     scroller = new CustomTouchScroller(scrollPanel);
   }
@@ -511,4 +563,42 @@
     scrollPanel = null;
     scroller = null;
   }
-}
+
+  /**
+   * Get the momentum command from the specified {@link TouchScroller}.
+   */
+ private native RepeatingCommand getMomentumCommand(TouchScroller scroller) /*-{ + return scroll...@com.google.gwt.touch.client.TouchScroller::momentumCommand;
+  }-*/;
+
+  /**
+   * Test that {@link TouchScroller} defers to native scrolling if the
+   * scrollable widget is already scrolled as far as it can go.
+   *
+   * @param hStart the starting horizontal scroll position
+   * @param vStart the starting vertical scroll position
+   * @param xEnd the ending x touch coordinate
+   * @param yEnd the ending y touch coordinate
+   */
+ private void testDeferToNativeScrolling(int hStart, int vStart, int xEnd, int yEnd) {
+    // Disable momentum for this test.
+    scroller.setMomentum(null);
+
+    // Scroll to the left.
+    scrollPanel.setHorizontalScrollPosition(hStart);
+    scrollPanel.setVerticalScrollPosition(vStart);
+
+    // Start touching.
+    scroller.onTouchStart(createTouchStartEvent(0, 0));
+    scroller.assertOnDragStartCalled(false);
+    assertTrue(scroller.isTouching());
+    assertFalse(scroller.isDragging());
+
+    // Move to the left.
+    scroller.onTouchMove(createTouchMoveEvent(xEnd, yEnd));
+    scroller.assertOnDragStartCalled(false);
+    scroller.assertOnDragMoveCalled(false);
+    assertFalse(scroller.isTouching());
+    assertFalse(scroller.isDragging());
+  }
+}

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

Reply via email to