Package com.google.gwt.touch.client

Source Code of com.google.gwt.touch.client.TouchScroller

/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.gwt.touch.client;

import com.google.gwt.core.client.Duration;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.RepeatingCommand;
import com.google.gwt.dom.client.PartialSupport;
import com.google.gwt.dom.client.Touch;
import com.google.gwt.event.dom.client.TouchCancelEvent;
import com.google.gwt.event.dom.client.TouchCancelHandler;
import com.google.gwt.event.dom.client.TouchEndEvent;
import com.google.gwt.event.dom.client.TouchEndHandler;
import com.google.gwt.event.dom.client.TouchEvent;
import com.google.gwt.event.dom.client.TouchMoveEvent;
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;
import java.util.List;

/**
* Adds touch based scrolling to a scroll panel.
*
* <p>
* Touch based scrolling is only supported on devices that support touch events
* and do not implement native touch based scrolling.
* </p>
*/
@PartialSupport
public class TouchScroller {

  /**
   * A point associated with a time.
   *
   * Visible for testing.
   */
  static class TemporalPoint {
    private Point point;
    private double time;

    public TemporalPoint() {
    }

    /**
     * Construct a new {@link TemporalPoint} for the specified point and time.
     */
    public TemporalPoint(Point point, double time) {
      setTemporalPoint(point, time);
    }

    public Point getPoint() {
      return point;
    }

    public double getTime() {
      return time;
    }

    /**
     * Update the point and time.
     *
     * @param point the new point
     * @param time the new time
     */
    public void setTemporalPoint(Point point, double time) {
      this.point = point;
      this.time = time;
    }
  }

  /**
   * The command used to apply momentum.
   */
  private class MomentumCommand implements RepeatingCommand {

    private final Duration duration = new Duration();
    private final Point initialPosition = getWidgetScrollPosition();
    private int lastElapsedMillis = 0;
    private State state;
    private HandlerRegistration windowResizeHandler;

    /**
     * Construct a {@link MomentumCommand}.
     *
     * @param endVelocity the final velocity of the user drag
     */
    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() {
      /*
       * Stop the command if another touch event starts or if momentum is
       * disabled.
       */
      if (this != momentumCommand) {
        finish();
        return false;
      }

      // Get the current position from the momentum.
      int cumulativeElapsedMillis = duration.elapsedMillis();
      state.setElapsedMillis(cumulativeElapsedMillis - lastElapsedMillis);
      lastElapsedMillis = cumulativeElapsedMillis;
      state.setCumulativeElapsedMillis(cumulativeElapsedMillis);

      // Calculate the new state.
      boolean notDone = momentum.updateState(state);

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

      /*
       * Apply the new position. Even if there is no additional momentum, we
       * want to respect the end position that the momentum returns.
       */
      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);
      }
    }
  }

  /**
   * The number of frames per second the animation should run at.
   */
  private static final double FRAMES_PER_SECOND = 60;

  /**
   * The number of ms to wait during a drag before updating the reported start
   * position of the drag.
   */
  private static final double MAX_TRACKING_TIME = 200;

  /**
   * The number of ms to wait before putting a position on deck.
   */
  private static final double MAX_TRACKING_TIME_ON_DECK = MAX_TRACKING_TIME / 2;

  /**
   * Minimum movement of touch required to be considered a drag.
   */
  private static final double MIN_TRACKING_FOR_DRAG = 5;

  /**
   * The number of milliseconds per animation frame.
   */
  private static final int MS_PER_FRAME = (int) (1000 / FRAMES_PER_SECOND);

  /**
   * A cached boolean indicating whether or not touch scrolling is supported.
   * Set to a non-null value the first time {@link #isSupported()} is called.
   */
  private static Boolean isSupported;

  /**
   * Return a new {@link TouchScroller}.
   *
   * @return a new {@link TouchScroller} if supported, and null otherwise
   */
  public static TouchScroller createIfSupported() {
    return isSupported() ? new TouchScroller() : null;
  }

  /**
   * Return a new {@link TouchScroller} that augments the specified scrollable
   * widget if supported, and null otherwise.
   *
   * @param widget the scrollable widget
   * @return a new {@link TouchScroller} if supported, and null otherwise
   */
  public static TouchScroller createIfSupported(HasScrolling widget) {
    TouchScroller scroller = createIfSupported();
    if (scroller != null) {
      scroller.setTargetWidget(widget);
    }
    return scroller;
  }

  /**
   * Runtime check for whether touch scrolling is supported in this browser.
   * Returns true if touch events are supported but touch based scrolling is not
   * natively supported.
   *
   * @return true if touch scrolling is supported, false if not
   */
  public static boolean isSupported() {
    if (isSupported == null) {
      /*
       * Android 3.0 devices support touch scrolling natively.
       *
       * TODO(jlabanca): Find a more reliable way to detect if native touch
       * scrolling is supported.
       */
      isSupported = TouchEvent.isSupported() && !isAndroid3();
    }
    return isSupported;
  }

  /**
   * Check if the user agent is android 3.0 or greater.
   *
   * @return true if android 3.0+
   *
   */
  private static native boolean isAndroid3() /*-{
    var ua = navigator.userAgent.toLowerCase();
    return /android ([3-9]+)\.([0-9]+)/.exec(ua) != null;
  }-*/;

  /**
   * The registration for the preview handler used to bust click events.
   */
  private HandlerRegistration bustClickHandler;

  /**
   * A boolean indicating that we are in a drag sequence. Dragging occurs after
   * the user moves beyond a threshold distance.
   */
  private boolean dragging;

  /**
   * Registrations for the handlers added to the widget.
   */
  private final List<HandlerRegistration> handlerRegs = new ArrayList<HandlerRegistration>();

  /**
   * The last (most recent) touch position. We need to keep track of this when
   * we handle touch move events because the Touch is already destroyed before
   * the touch end event fires.
   */
  private final TemporalPoint lastTouchPosition = new TemporalPoint();

  /**
   * The momentum that determines how the widget scrolls after the user
   * completes a gesture. Can be null if momentum is not supported.
   */
  private Momentum momentum;

  /**
   * The repeating command used to continue momentum after the gesture ends. The
   * command is instantiated after the user finishes a drag sequence. A non null
   * value indicates that momentum is occurring.
   */
  private RepeatingCommand momentumCommand;

  /**
   * The coordinate of the most recent relevant touch event. For most drag
   * sequences this will be the same as the startCoordinate. If the touch
   * gesture changes direction significantly or pauses for a while this
   * coordinate will be updated to the coordinate of the on deck touchmove
   * event.
   */
  private final TemporalPoint recentTouchPosition = new TemporalPoint();

  /**
   * If the gesture takes too long, we update the recentTouchPosition to the
   * position on deck, which occurred halfway through the max tracking time. We
   * do this so that we don't base the velocity on two touch events that
   * occurred very close to each other at the end of a long gesture.
   */
  private TemporalPoint recentTouchPositionOnDeck;

  /**
   * The position of the scrollable when the first touch occured.
   */
  private Point startScrollPosition;

  /**
   * The position of the first touch.
   */
  private Point startTouchPosition;

  /**
   * A boolean indicating that we are in a touch sequence.
   */
  private boolean touching;

  /**
   * The widget being augmented.
   */
  private HasScrolling widget;

  /**
   * Construct a new {@link TouchScroller}. This constructor should be called
   * using the static method {@link #createIfSupported()}.
   *
   * @param widget the widget to augment
   * @see #createIfSupported()
   */
  protected TouchScroller() {
    setMomentum(new DefaultMomentum());
  }

  /**
   * Get the {@link Momentum} that controls scrolling after the user completes a
   * gesture.
   *
   * @return the scrolling {@link Momentum}, or null if disabled
   */
  public Momentum getMomentum() {
    return momentum;
  }

  /**
   * Get the target {@link HasScrolling} widget that this scroller affects.
   *
   * @return the target widget
   */
  public HasScrolling getTargetWidget() {
    return widget;
  }

  /**
   * Set the {@link Momentum} that controls scrolling after the user completes a
   * gesture.
   *
   * @param momentum the scrolling {@link Momentum}, or null to disable
   */
  public void setMomentum(Momentum momentum) {
    this.momentum = momentum;
    if (momentum == null) {
      // Cancel the current momentum.
      momentumCommand = null;
    }
  }

  /**
   * Set the target {@link HasScrolling} widget that this scroller affects.
   *
   * @param widget the target widget, or null to disbale
   */
  public void setTargetWidget(HasScrolling widget) {
    if (this.widget == widget) {
      return;
    }

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

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

    // Attach to the new widget.
    this.widget = widget;
    if (widget != null) {
      // Add touch start handler.
      handlerRegs.add(widget.asWidget().addDomHandler(new TouchStartHandler() {
        public void onTouchStart(TouchStartEvent event) {
          TouchScroller.this.onTouchStart(event);
        }
      }, TouchStartEvent.getType()));

      // Add touch move handler.
      handlerRegs.add(widget.asWidget().addDomHandler(new TouchMoveHandler() {
        public void onTouchMove(TouchMoveEvent event) {
          TouchScroller.this.onTouchMove(event);
        }
      }, TouchMoveEvent.getType()));

      // Add touch end handler.
      handlerRegs.add(widget.asWidget().addDomHandler(new TouchEndHandler() {
        public void onTouchEnd(TouchEndEvent event) {
          TouchScroller.this.onTouchEnd(event);
        }
      }, TouchEndEvent.getType()));

      // Add touch cancel handler.
      handlerRegs.add(widget.asWidget().addDomHandler(new TouchCancelHandler() {
        public void onTouchCancel(TouchCancelEvent event) {
          TouchScroller.this.onTouchCancel(event);
        }
      }, TouchCancelEvent.getType()));
    }
  }

  /**
   * Get touch from event.
   *
   * @param event the event
   * @return the touch object
   */
  protected Touch getTouchFromEvent(TouchEvent<?> event) {
    JsArray<Touch> touches = event.getTouches();
    return (touches.length() > 0) ? touches.get(0) : null;
  }

  /**
   * Called when the object's drag sequence is complete.
   *
   * @param event the touch event
   */
  protected void onDragEnd(TouchEvent<?> event) {
    // There is no momentum or it isn't supported.
    if (momentum == null) {
      return;
    }

    // Schedule the momentum.
    Point endVelocity = calculateEndVelocity(recentTouchPosition, lastTouchPosition);
    if (endVelocity != null) {
      momentumCommand = new MomentumCommand(endVelocity);
      Scheduler.get().scheduleFixedDelay(momentumCommand, MS_PER_FRAME);
    }
  }

  /**
   * Called when the object has been dragged to a new position.
   *
   * @param event the touch event
   */
  protected void onDragMove(TouchEvent<?> event) {
    /*
     * Scroll to the new position. Touch scrolling moves in the same direction
     * as the finger dragging, whereas scrolling is inverted with traditional
     * scrollbars.
     */
    Point diff = startTouchPosition.minus(lastTouchPosition.getPoint());
    Point curScrollPosition = startScrollPosition.plus(diff);
    setWidgetScrollPosition(curScrollPosition);
  }

  /**
   * Called when the object has started dragging.
   *
   * @param event the touch event
   */
  protected void onDragStart(TouchEvent<?> event) {
  }

  /**
   * Called when the user cancels a touch. This can happen if the user touches
   * the screen with too many fingers.
   *
   * @param event the touch event
   */
  protected void onTouchCancel(TouchEvent<?> event) {
    onTouchEnd(event);
  }

  /**
   * Called when the user releases a touch.
   *
   * @param event the touch event
   */
  protected void onTouchEnd(TouchEvent<?> event) {
    // Ignore the touch if we didn't catch a touch start event.
    if (!touching) {
      return;
    }
    touching = false;

    // Stop dragging.
    if (dragging) {
      dragging = false;
      onDragEnd(event);
    }
  }

  /**
   * Called when the user moves a touch.
   *
   * @param event the touch event
   */
  protected void onTouchMove(TouchEvent<?> event) {
    // Ignore the touch if we never caught a touch start event.
    if (!touching) {
      return;
    }

    // Check if we should start dragging.
    Touch touch = getTouchFromEvent(event);
    Point touchPoint = new Point(touch.getPageX(), touch.getPageY());
    double touchTime = Duration.currentTimeMillis();
    lastTouchPosition.setTemporalPoint(touchPoint, touchTime);
    if (!dragging) {
      Point diff = touchPoint.minus(startTouchPosition);
      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.
      onDragMove(event);

      /*
       * Update the recent position. This happens when they are dragging slowly.
       * If they are dragging slowly then we should reset the start time and
       * position to where they are now. This will be important during the drag
       * end when we report to the draggable delegate what kind of drag just
       * happened.
       */
      double trackingTime = touchTime - recentTouchPosition.getTime();
      if (trackingTime > MAX_TRACKING_TIME && recentTouchPositionOnDeck != null) {
        // See comment below.
        recentTouchPosition.setTemporalPoint(recentTouchPositionOnDeck.getPoint(),
            recentTouchPositionOnDeck.getTime());
        recentTouchPositionOnDeck = null;
      } else if (trackingTime > MAX_TRACKING_TIME_ON_DECK && recentTouchPositionOnDeck == null) {
        /*
         * When we are halfway to the max tracking time, put the current touch
         * on deck. When we switch the recent touch position, we use the on deck
         * position. That prevents us from calculating the velocity from two
         * points that are too close in time (or the same time).
         */
        recentTouchPositionOnDeck = new TemporalPoint(touchPoint, touchTime);
      }
    }
  }

  /**
   * Called when the user starts a touch.
   *
   * @param event the touch event
   */
  protected void onTouchStart(TouchEvent<?> event) {
    // 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);
    startTouchPosition = new Point(touch.getPageX(), touch.getPageY());
    double startTouchTime = Duration.currentTimeMillis();
    recentTouchPosition.setTemporalPoint(startTouchPosition, startTouchTime);
    lastTouchPosition.setTemporalPoint(startTouchPosition, startTouchTime);
    recentTouchPositionOnDeck = null;

    // Record the starting scroll position.
    startScrollPosition = getWidgetScrollPosition();
  }

  /**
   * Calculate the end velocity. Visible for testing.
   *
   * @param from the starting point
   * @param to the ending point
   * @return the end velocity, or null if it cannot be calculated
   */
  Point calculateEndVelocity(TemporalPoint from, TemporalPoint to) {
    /*
     * Calculate the time since the recent touch. The time can be zero if the
     * user pauses for too long, which updates the recentTouchPosition, then
     * lets go without moving again.
     */
    double time = to.getTime() - from.getTime();
    if (time <= 0) {
      return null;
    }

    /*
     * Calculate the end velocities. The velocity is inverted from the direction
     * of the gesture.
     */
    Point dist = from.getPoint().minus(to.getPoint());
    return new Point(dist.getX() / time, dist.getY() / time);
  }

  /**
   * Visible for testing.
   */
  TemporalPoint getLastTouchPosition() {
    return lastTouchPosition;
  }

  /**
   * Visible for testing.
   */
  TemporalPoint getRecentTouchPosition() {
    return recentTouchPosition;
  }

  /**
   * Visible for testing.
   */
  boolean isDragging() {
    return dragging;
  }

  /**
   * Check if momentum is currently active. Visible for testing.
   *
   * @return true if active, false if not
   */
  boolean isMomentumActive() {
    return (momentumCommand != null);
  }

  /**
   * Visible for testing.
   */
  boolean isTouching() {
    return touching;
  }

  /**
   * Cancel all existing touch, drag, and momentum.
   */
  private void cancelAll() {
    touching = false;
    dragging = false;
    momentumCommand = null;
  }

  /**
   * Get the scroll position of the widget.
   */
  private Point getWidgetScrollPosition() {
    return new Point(widget.getHorizontalScrollPosition(), widget.getVerticalScrollPosition());
  }

  /**
   * Set whether or not we should bust the next click.
   *
   * @param doBust true to bust the next click, false not to
   */
  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;
    }
  }

  /**
   * Set the scroll position of the widget.
   *
   * @param position the new position
   */
  private void setWidgetScrollPosition(Point position) {
    widget.setHorizontalScrollPosition((int) position.getX());
    widget.setVerticalScrollPosition((int) position.getY());
  }
}
TOP

Related Classes of com.google.gwt.touch.client.TouchScroller

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.