| /* | |
| * 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.AttachEvent; | |
| 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.Iterator; | |
| 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(); | |
| } | |
| }); | |
| } | |
| @Override | |
| 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()); | |
| /* | |
| * 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; | |
| } | |
| /** | |
| * Finish and cleanup this momentum command. | |
| */ | |
| private void finish() { | |
| if (windowResizeHandler != null) { | |
| windowResizeHandler.removeHandler(); | |
| windowResizeHandler = null; | |
| } | |
| if (this == momentumCommand) { | |
| momentumCommand = null; | |
| } | |
| } | |
| } | |
| /** | |
| * 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. | |
| */ | |
| 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 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. | |
| */ | |
| 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 bustClickHandlerReg; | |
| /** | |
| * The registration for the attach event handler. | |
| */ | |
| private HandlerRegistration attachHandlerReg; | |
| /** | |
| * 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 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. | |
| */ | |
| 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()}. | |
| * | |
| * @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(); | |
| // Release the old widget. | |
| 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); | |
| } | |
| }, TouchStartEvent.getType())); | |
| // Add touch move handler. | |
| handlerRegs.add(widget.asWidget().addDomHandler(new TouchMoveHandler() { | |
| @Override | |
| public void onTouchMove(TouchMoveEvent event) { | |
| TouchScroller.this.onTouchMove(event); | |
| } | |
| }, TouchMoveEvent.getType())); | |
| // Add touch end handler. | |
| handlerRegs.add(widget.asWidget().addDomHandler(new TouchEndHandler() { | |
| @Override | |
| public void onTouchEnd(TouchEndEvent event) { | |
| TouchScroller.this.onTouchEnd(event); | |
| } | |
| }, TouchEndEvent.getType())); | |
| // Add touch cancel handler. | |
| handlerRegs.add(widget.asWidget().addDomHandler(new TouchCancelHandler() { | |
| @Override | |
| 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) { | |
| 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 | |
| * 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) { | |
| // 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; | |
| } | |
| // 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; | |
| /** | |
| * 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; | |
| } | |
| /** | |
| * 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; | |
| } | |
| /** | |
| * 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. | |
| */ | |
| 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()); | |
| } | |
| /** | |
| * 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 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; | |
| } | |
| /** | |
| * 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 | |
| */ | |
| private void setWidgetScrollPosition(Point position) { | |
| widget.setHorizontalScrollPosition((int) position.getX()); | |
| widget.setVerticalScrollPosition((int) position.getY()); | |
| } | |
| } |