blob: 2e362be42f6b2610961300e571096129e32b0813 [file] [log] [blame]
// Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// @dart = 2.9
part of touch;
/**
* Touch Handler. Class that handles all touch events and
* uses them to interpret higher level gestures and behaviors. TouchEvent is a
* built in mobile safari type:
* [http://developer.apple.com/safari/library/documentation/UserExperience/Reference/TouchEventClassReference/TouchEvent/TouchEvent.html].
*
* Examples of higher level gestures this class is intended to support
* - click, double click, long click
* - dragging, swiping, zooming
*
* Touch Behavior:
* Use this class to make your elements 'touchable' (see Touchable.dart).
* Intended to work with all webkit browsers.
*
* Drag Behavior:
* Use this class to make your elements 'draggable' (see draggable.js).
* This behavior will handle all of the required events and report the
* properties of the drag to you while the touch is happening and at the
* end of the drag sequence. This behavior will NOT perform the actual
* dragging (redrawing the element) for you, this responsibility is left to
* the client code. This behavior contains a work around for a mobile
* safari bug where the 'touchend' event is not dispatched when the touch
* goes past the bottom of the browser window.
* This is intended to work well in iframes.
* Intended to work with all webkit browsers, tested only on iPhone 3.x so
* far.
*
* Click Behavior:
* Not yet implemented.
*
* Zoom Behavior:
* Not yet implemented.
*
* Swipe Behavior:
* Not yet implemented.
*/
class TouchHandler {
Touchable _touchable;
Element _element;
/** The absolute sum of all touch y deltas. */
int _totalMoveY;
/** The absolute sum of all touch x deltas. */
int _totalMoveX;
/**
* A list of tuples where the first item is the horizontal component of a
* recent relevant touch and the second item is the touch's time stamp. Old
* touches are removed based on the max tracking time and when direction
* changes.
*/
List<int> _recentTouchesX;
/**
* A list of tuples where the first item is the vertical component of a
* recent relevant touch and the second item is the touch's time stamp. Old
* touches are removed based on the max tracking time and when direction
* changes.
*/
List<int> _recentTouchesY;
// TODO(jacobr): make customizable by passing optional parameters to the
// TouchHandler constructor.
/**
* Minimum movement of touch required to be considered a drag.
*/
static const _MIN_TRACKING_FOR_DRAG = 2;
/**
* The maximum number of ms to track a touch event. After an event is older
* than this value, it will be ignored in velocity calculations.
*/
static const _MAX_TRACKING_TIME = 250;
/** The maximum number of touches to track. */
static const _MAX_TRACKING_TOUCHES = 5;
/**
* The maximum velocity to return, in pixels per millisecond, that is used to
* guard against errors in calculating end velocity of a drag. This is a very
* fast drag velocity.
*/
static const _MAXIMUM_VELOCITY = 5;
/**
* The velocity to return, in pixel per millisecond, when the time stamps on
* the events are erroneous. The browser can return bad time stamps if the
* thread is blocked for the duration of the drag. This is a low velocity to
* prevent the content from moving quickly after a slow drag. It is less
* jarring if the content moves slowly after a fast drag.
*/
static const _VELOCITY_FOR_INCORRECT_EVENTS = 1;
Draggable _draggable;
bool _tracking;
bool _dragging;
bool _touching;
int _startTouchX;
int _startTouchY;
int _startTime;
TouchEvent _lastEvent;
int _lastTouchX;
int _lastTouchY;
int _lastMoveX;
int _lastMoveY;
int _endTime;
int _endTouchX;
int _endTouchY;
TouchHandler(Touchable touchable, [Element element = null])
: _touchable = touchable,
_totalMoveY = 0,
_totalMoveX = 0,
_recentTouchesX = new List<int>(),
_recentTouchesY = new List<int>(),
// TODO(jmesserly): I don't like having to initialize all booleans here
// See b/5045736
_dragging = false,
_tracking = false,
_touching = false {
_element = element != null ? element : touchable.getElement();
}
/**
* Begin tracking the touchable element, it is eligible for dragging.
*/
void _beginTracking() {
_tracking = true;
}
/**
* Stop tracking the touchable element, it is no longer dragging.
*/
void _endTracking() {
_tracking = false;
_dragging = false;
_totalMoveY = 0;
_totalMoveX = 0;
}
/**
* Correct erroneous velocities by capping the velocity if we think it's too
* high, or setting it to a default velocity if know that the event data is
* bad. Returns the corrected velocity.
*/
num _correctVelocity(num velocity) {
num absVelocity = velocity.abs();
if (absVelocity > _MAXIMUM_VELOCITY) {
absVelocity = _recentTouchesY.length < 6
? _VELOCITY_FOR_INCORRECT_EVENTS
: _MAXIMUM_VELOCITY;
}
return absVelocity * (velocity < 0 ? -1 : 1);
}
/**
* Start listenting for events.
* If [capture] is True the TouchHandler should listen during the capture
* phase.
*/
void enable([bool capture = false]) {
Function onEnd = (e) {
_onEnd(e.timeStamp.toInt(), e);
};
_addEventListeners(_element, (e) {
_onStart(e);
}, (e) {
_onMove(e);
}, onEnd, onEnd, capture);
}
/**
* Get the current horizontal drag delta. Drag delta is defined as the deltaX
* of the start touch position and the last touch position.
*/
int getDragDeltaX() {
return _lastTouchX - _startTouchX;
}
/**
* Get the current vertical drag delta. Drag delta is defined as the deltaY of
* the start touch position and the last touch position.
*/
int getDragDeltaY() {
return _lastTouchY - _startTouchY;
}
/**
* Get end velocity of the drag. This method is specific to drag behavior, so
* if touch behavior and drag behavior is split then this should go with drag
* behavior. End velocity is defined as deltaXY / deltaTime where deltaXY is
* the difference between endPosition and the oldest recent position, and
* deltaTime is the difference between endTime and the oldest recent time
* stamp.
*/
Coordinate getEndVelocity() {
num velocityX = 0;
num velocityY = 0;
if (_recentTouchesX.length > 0) {
num timeDeltaX = Math.max(1, _endTime - _recentTouchesX[1]);
velocityX = (_endTouchX - _recentTouchesX[0]) / timeDeltaX;
}
if (_recentTouchesY.length > 0) {
num timeDeltaY = Math.max(1, _endTime - _recentTouchesY[1]);
velocityY = (_endTouchY - _recentTouchesY[0]) / timeDeltaY;
}
velocityX = _correctVelocity(velocityX);
velocityY = _correctVelocity(velocityY);
return new Coordinate(velocityX, velocityY);
}
/**
* Return the touch of the last event.
*/
Touch _getLastTouch() {
assert(_lastEvent != null); // Last event not set
return _lastEvent.touches[0];
}
/**
* Is the touch manager currently tracking touch moves to detect a drag?
*/
bool isTracking() {
return _tracking;
}
/**
* Touch end handler.
*/
void _onEnd(int timeStamp, [TouchEvent e = null]) {
_touching = false;
_touchable.onTouchEnd();
if (!_tracking || _draggable == null) {
return;
}
Touch touch = _getLastTouch();
int clientX = touch.client.x;
int clientY = touch.client.y;
if (_dragging) {
_endTime = timeStamp;
_endTouchX = clientX;
_endTouchY = clientY;
_recentTouchesX = _removeOldTouches(_recentTouchesX, timeStamp);
_recentTouchesY = _removeOldTouches(_recentTouchesY, timeStamp);
_draggable.onDragEnd();
if (e != null) {
e.preventDefault();
}
ClickBuster.preventGhostClick(_startTouchX, _startTouchY);
}
_endTracking();
}
/**
* Touch move handler.
*/
void _onMove(TouchEvent e) {
if (!_tracking || _draggable == null) {
return;
}
final touch = e.touches[0];
int clientX = touch.client.x;
int clientY = touch.client.y;
int moveX = _lastTouchX - clientX;
int moveY = _lastTouchY - clientY;
int timeStamp = e.timeStamp.toInt();
_totalMoveX += moveX.abs();
_totalMoveY += moveY.abs();
_lastTouchX = clientX;
_lastTouchY = clientY;
if (!_dragging &&
((_totalMoveY > _MIN_TRACKING_FOR_DRAG && _draggable.verticalEnabled) ||
(_totalMoveX > _MIN_TRACKING_FOR_DRAG &&
_draggable.horizontalEnabled))) {
_dragging = _draggable.onDragStart(e);
if (!_dragging) {
_endTracking();
} else {
_startTouchX = clientX;
_startTouchY = clientY;
_startTime = timeStamp;
}
}
if (_dragging) {
_draggable.onDragMove();
_lastEvent = e;
e.preventDefault();
_recentTouchesX =
_removeTouchesInWrongDirection(_recentTouchesX, _lastMoveX, moveX);
_recentTouchesY =
_removeTouchesInWrongDirection(_recentTouchesY, _lastMoveY, moveY);
_recentTouchesX = _removeOldTouches(_recentTouchesX, timeStamp);
_recentTouchesY = _removeOldTouches(_recentTouchesY, timeStamp);
_recentTouchesX.add(clientX);
_recentTouchesX.add(timeStamp);
_recentTouchesY.add(clientY);
_recentTouchesY.add(timeStamp);
}
_lastMoveX = moveX;
_lastMoveY = moveY;
}
/**
* Touch start handler.
*/
void _onStart(TouchEvent e) {
if (_touching) {
return;
}
_touching = true;
if (!_touchable.onTouchStart(e) || _draggable == null) {
return;
}
final touch = e.touches[0];
_startTouchX = _lastTouchX = touch.client.x;
_startTouchY = _lastTouchY = touch.client.y;
int timeStamp = e.timeStamp.toInt();
_startTime = timeStamp;
// TODO(jacobr): why don't we just clear the lists?
_recentTouchesX = new List<int>();
_recentTouchesY = new List<int>();
_recentTouchesX.add(touch.client.x);
_recentTouchesX.add(timeStamp);
_recentTouchesY.add(touch.client.y);
_recentTouchesY.add(timeStamp);
_lastEvent = e;
_beginTracking();
}
/**
* Filters the provided recent touches list to remove all touches older than
* the max tracking time or the 5th most recent touch.
* [recentTouches] specifies a list of tuples where the first item is the x
* or y component of the recent touch and the second item is the touch time
* stamp. The time of the most recent event is specified by [recentTime].
*/
List<int> _removeOldTouches(List<int> recentTouches, int recentTime) {
int count = 0;
final len = recentTouches.length;
assert(len % 2 == 0);
while (count < len &&
recentTime - recentTouches[count + 1] > _MAX_TRACKING_TIME ||
(len - count) > _MAX_TRACKING_TOUCHES * 2) {
count += 2;
}
return count == 0 ? recentTouches : _removeFirstN(recentTouches, count);
}
static List<int> _removeFirstN(List<int> list, int n) {
return list.sublist(n);
}
/**
* Filters the provided recent touches list to remove all touches except the
* last if the move direction has changed.
* [recentTouches] specifies a list of tuples where the first item is the x
* or y component of the recent touch and the second item is the touch time
* stamp. The x or y component of the most recent move is specified by
* [recentMove].
*/
List<int> _removeTouchesInWrongDirection(
List<int> recentTouches, int lastMove, int recentMove) {
if (lastMove != 0 &&
recentMove != 0 &&
recentTouches.length > 2 &&
_xor(lastMove > 0, recentMove > 0)) {
return _removeFirstN(recentTouches, recentTouches.length - 2);
}
return recentTouches;
}
// TODO(jacobr): why doesn't bool implement the xor operator directly?
static bool _xor(bool a, bool b) => a != b;
/**
* Reset the touchable element.
*/
void reset() {
_endTracking();
_touching = false;
}
/**
* Call this method to enable drag behavior on a draggable delegate.
* The [draggable] object can be the same as the [_touchable] object, they are
* assigned to different members to allow for strong typing with interfaces.
*/
void setDraggable(Draggable draggable) {
_draggable = draggable;
}
}