745f112ef6
Opt an initial batch of files under samples, samples-dev, utils, and runtime/tools/dartfuzz out of null safety in preparation for switching the flag on by default. Change-Id: Icdfd52a5a969e678a7205903332f73fe3841c223 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/166960 Reviewed-by: Daco Harkes <dacoharkes@google.com> Commit-Queue: Leaf Petersen <leafp@google.com>
403 lines
12 KiB
Dart
403 lines
12 KiB
Dart
// 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;
|
|
}
|
|
}
|