blob: 3855c3d85c0210565681c00a91a05c66e7b57494 [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 {
final 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])
: _touchable = touchable,
_totalMoveY = 0,
_totalMoveX = 0,
_recentTouchesX = <int>[],
_recentTouchesY = <int>[],
// TODO(jmesserly): I don't like having to initialize all booleans here
// See b/5045736
_dragging = false,
_tracking = false,
_touching = false {
_element = 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]) {
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.isNotEmpty) {
num timeDeltaX = Math.max(1, _endTime - _recentTouchesX[1]);
velocityX = (_endTouchX - _recentTouchesX[0]) / timeDeltaX;
}
if (_recentTouchesY.isNotEmpty) {
num timeDeltaY = Math.max(1, _endTime - _recentTouchesY[1]);
velocityY = (_endTouchY - _recentTouchesY[0]) / timeDeltaY;
}
velocityX = _correctVelocity(velocityX);
velocityY = _correctVelocity(velocityY);
return 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]) {
_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 = <int>[];
_recentTouchesY = <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;
}
}