// 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 _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 _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(), _recentTouchesY = new List(), // 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(); _recentTouchesY = new List(); _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 _removeOldTouches(List 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 _removeFirstN(List 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 _removeTouchesInWrongDirection( List 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; } }