| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file | |
| 2 // for details. All rights reserved. Use of this source code is governed by a | |
| 3 // BSD-style license that can be found in the LICENSE file. | |
| 4 | |
| 5 /** | |
| 6 * Touch Handler. Class that handles all touch events and | |
| 7 * uses them to interpret higher level gestures and behaviors. TouchEvent is a | |
| 8 * built in mobile safari type: | |
| 9 * [http://developer.apple.com/safari/library/documentation/UserExperience/Refer
ence/TouchEventClassReference/TouchEvent/TouchEvent.html]. | |
| 10 * | |
| 11 * Examples of higher level gestures this class is intended to support | |
| 12 * - click, double click, long click | |
| 13 * - dragging, swiping, zooming | |
| 14 * | |
| 15 * Touch Behavior: | |
| 16 * Use this class to make your elements 'touchable' (see Touchable.dart). | |
| 17 * Intended to work with all webkit browsers. | |
| 18 * | |
| 19 * Drag Behavior: | |
| 20 * Use this class to make your elements 'draggable' (see draggable.js). | |
| 21 * This behavior will handle all of the required events and report the | |
| 22 * properties of the drag to you while the touch is happening and at the | |
| 23 * end of the drag sequence. This behavior will NOT perform the actual | |
| 24 * dragging (redrawing the element) for you, this responsibility is left to | |
| 25 * the client code. This behavior contains a work around for a mobile | |
| 26 * safari bug where the 'touchend' event is not dispatched when the touch | |
| 27 * goes past the bottom of the browser window. | |
| 28 * This is intended to work well in iframes. | |
| 29 * Intended to work with all webkit browsers, tested only on iPhone 3.x so | |
| 30 * far. | |
| 31 * | |
| 32 * Click Behavior: | |
| 33 * Not yet implemented. | |
| 34 * | |
| 35 * Zoom Behavior: | |
| 36 * Not yet implemented. | |
| 37 * | |
| 38 * Swipe Behavior: | |
| 39 * Not yet implemented. | |
| 40 */ | |
| 41 class TouchHandler { | |
| 42 Touchable _touchable; | |
| 43 Element _element; | |
| 44 | |
| 45 /** The absolute sum of all touch y deltas. */ | |
| 46 int _totalMoveY; | |
| 47 | |
| 48 /** The absolute sum of all touch x deltas. */ | |
| 49 int _totalMoveX; | |
| 50 | |
| 51 /** | |
| 52 * A list of tuples where the first item is the horizontal component of a | |
| 53 * recent relevant touch and the second item is the touch's time stamp. Old | |
| 54 * touches are removed based on the max tracking time and when direction | |
| 55 * changes. | |
| 56 */ | |
| 57 List<int> _recentTouchesX; | |
| 58 | |
| 59 /** | |
| 60 * A list of tuples where the first item is the vertical component of a | |
| 61 * recent relevant touch and the second item is the touch's time stamp. Old | |
| 62 * touches are removed based on the max tracking time and when direction | |
| 63 * changes. | |
| 64 */ | |
| 65 List<int> _recentTouchesY; | |
| 66 | |
| 67 // TODO(jacobr): make customizable by passing optional parameters to the | |
| 68 // TouchHandler constructor. | |
| 69 /** | |
| 70 * Minimum movement of touch required to be considered a drag. | |
| 71 */ | |
| 72 static final _MIN_TRACKING_FOR_DRAG = 2; | |
| 73 | |
| 74 /** | |
| 75 * The maximum number of ms to track a touch event. After an event is older | |
| 76 * than this value, it will be ignored in velocity calculations. | |
| 77 */ | |
| 78 static final _MAX_TRACKING_TIME = 250; | |
| 79 | |
| 80 /** The maximum number of touches to track. */ | |
| 81 static final _MAX_TRACKING_TOUCHES = 5; | |
| 82 | |
| 83 /** | |
| 84 * The maximum velocity to return, in pixels per millisecond, that is used to | |
| 85 * guard against errors in calculating end velocity of a drag. This is a very | |
| 86 * fast drag velocity. | |
| 87 */ | |
| 88 static final _MAXIMUM_VELOCITY = 5; | |
| 89 | |
| 90 /** | |
| 91 * The velocity to return, in pixel per millisecond, when the time stamps on | |
| 92 * the events are erroneous. The browser can return bad time stamps if the | |
| 93 * thread is blocked for the duration of the drag. This is a low velocity to | |
| 94 * prevent the content from moving quickly after a slow drag. It is less | |
| 95 * jarring if the content moves slowly after a fast drag. | |
| 96 */ | |
| 97 static final _VELOCITY_FOR_INCORRECT_EVENTS = 1; | |
| 98 | |
| 99 Draggable _draggable; | |
| 100 bool _tracking; | |
| 101 bool _dragging; | |
| 102 bool _touching; | |
| 103 int _startTouchX; | |
| 104 int _startTouchY; | |
| 105 int _startTime; | |
| 106 TouchEvent _lastEvent; | |
| 107 int _lastTouchX; | |
| 108 int _lastTouchY; | |
| 109 int _lastMoveX; | |
| 110 int _lastMoveY; | |
| 111 int _endTime; | |
| 112 int _endTouchX; | |
| 113 int _endTouchY; | |
| 114 | |
| 115 TouchHandler(Touchable touchable, [Element element = null]) | |
| 116 : _touchable = touchable, | |
| 117 _totalMoveY = 0, | |
| 118 _totalMoveX = 0, | |
| 119 _recentTouchesX = new List<int>(), | |
| 120 _recentTouchesY = new List<int>(), | |
| 121 // TODO(jmesserly): I don't like having to initialize all booleans here | |
| 122 // See b/5045736 | |
| 123 _dragging = false, | |
| 124 _tracking = false, | |
| 125 _touching = false { | |
| 126 _element = element != null ? element : touchable.getElement(); | |
| 127 } | |
| 128 | |
| 129 /** | |
| 130 * Begin tracking the touchable element, it is eligible for dragging. | |
| 131 */ | |
| 132 void _beginTracking() { | |
| 133 _tracking = true; | |
| 134 } | |
| 135 | |
| 136 /** | |
| 137 * Stop tracking the touchable element, it is no longer dragging. | |
| 138 */ | |
| 139 void _endTracking() { | |
| 140 _tracking = false; | |
| 141 _dragging = false; | |
| 142 _totalMoveY = 0; | |
| 143 _totalMoveX = 0; | |
| 144 } | |
| 145 | |
| 146 /** | |
| 147 * Correct erroneous velocities by capping the velocity if we think it's too | |
| 148 * high, or setting it to a default velocity if know that the event data is | |
| 149 * bad. Returns the corrected velocity. | |
| 150 */ | |
| 151 num _correctVelocity(num velocity) { | |
| 152 num absVelocity = velocity.abs(); | |
| 153 if (absVelocity > _MAXIMUM_VELOCITY) { | |
| 154 absVelocity = _recentTouchesY.length < 6 ? | |
| 155 _VELOCITY_FOR_INCORRECT_EVENTS : _MAXIMUM_VELOCITY; | |
| 156 } | |
| 157 return absVelocity * (velocity < 0 ? -1 : 1); | |
| 158 } | |
| 159 | |
| 160 /** | |
| 161 * Start listenting for events. | |
| 162 * If [capture] is True the TouchHandler should listen during the capture | |
| 163 * phase. | |
| 164 */ | |
| 165 void enable([bool capture = false]) { | |
| 166 Function onEnd = (e) { _onEnd(e.timeStamp, e); }; | |
| 167 _addEventListeners( | |
| 168 _element, | |
| 169 (e) { _onStart(e); }, | |
| 170 (e) { _onMove(e); }, onEnd, onEnd, capture); | |
| 171 } | |
| 172 | |
| 173 /** | |
| 174 * Get the current horizontal drag delta. Drag delta is defined as the deltaX | |
| 175 * of the start touch position and the last touch position. | |
| 176 */ | |
| 177 int getDragDeltaX() { | |
| 178 return _lastTouchX - _startTouchX; | |
| 179 } | |
| 180 | |
| 181 /** | |
| 182 * Get the current vertical drag delta. Drag delta is defined as the deltaY of | |
| 183 * the start touch position and the last touch position. | |
| 184 */ | |
| 185 int getDragDeltaY() { | |
| 186 return _lastTouchY - _startTouchY; | |
| 187 } | |
| 188 | |
| 189 /** | |
| 190 * Get end velocity of the drag. This method is specific to drag behavior, so | |
| 191 * if touch behavior and drag behavior is split then this should go with drag | |
| 192 * behavior. End velocity is defined as deltaXY / deltaTime where deltaXY is | |
| 193 * the difference between endPosition and the oldest recent position, and | |
| 194 * deltaTime is the difference between endTime and the oldest recent time | |
| 195 * stamp. | |
| 196 */ | |
| 197 Coordinate getEndVelocity() { | |
| 198 num velocityX = 0; | |
| 199 num velocityY = 0; | |
| 200 | |
| 201 if (_recentTouchesX.length > 0) { | |
| 202 num timeDeltaX = Math.max(1, _endTime - _recentTouchesX[1]); | |
| 203 velocityX = (_endTouchX - _recentTouchesX[0]) / timeDeltaX; | |
| 204 } | |
| 205 | |
| 206 if (_recentTouchesY.length > 0) { | |
| 207 num timeDeltaY = Math.max(1, _endTime - _recentTouchesY[1]); | |
| 208 velocityY = (_endTouchY - _recentTouchesY[0]) / timeDeltaY; | |
| 209 } | |
| 210 velocityX = _correctVelocity(velocityX); | |
| 211 velocityY = _correctVelocity(velocityY); | |
| 212 return new Coordinate(velocityX, velocityY); | |
| 213 } | |
| 214 | |
| 215 /** | |
| 216 * Return the touch of the last event. | |
| 217 */ | |
| 218 Touch _getLastTouch() { | |
| 219 assert (_lastEvent != null); // Last event not set | |
| 220 return _lastEvent.touches[0]; | |
| 221 } | |
| 222 | |
| 223 /** | |
| 224 * Is the touch manager currently tracking touch moves to detect a drag? | |
| 225 */ | |
| 226 bool isTracking() { | |
| 227 return _tracking; | |
| 228 } | |
| 229 | |
| 230 /** | |
| 231 * Touch end handler. | |
| 232 */ | |
| 233 void _onEnd(int timeStamp, [TouchEvent e = null]) { | |
| 234 _touching = false; | |
| 235 _touchable.onTouchEnd(); | |
| 236 if (!_tracking || _draggable === null) { | |
| 237 return; | |
| 238 } | |
| 239 Touch touch = _getLastTouch(); | |
| 240 int clientX = touch.clientX; | |
| 241 int clientY = touch.clientY; | |
| 242 if (_dragging) { | |
| 243 _endTime = timeStamp; | |
| 244 _endTouchX = clientX; | |
| 245 _endTouchY = clientY; | |
| 246 _recentTouchesX = _removeOldTouches(_recentTouchesX, timeStamp); | |
| 247 _recentTouchesY = _removeOldTouches(_recentTouchesY, timeStamp); | |
| 248 _draggable.onDragEnd(); | |
| 249 if (e !== null) { | |
| 250 e.preventDefault(); | |
| 251 } | |
| 252 ClickBuster.preventGhostClick(_startTouchX, _startTouchY); | |
| 253 } | |
| 254 _endTracking(); | |
| 255 } | |
| 256 | |
| 257 /** | |
| 258 * Touch move handler. | |
| 259 */ | |
| 260 void _onMove(TouchEvent e) { | |
| 261 if (!_tracking || _draggable === null) { | |
| 262 return; | |
| 263 } | |
| 264 final touch = e.touches[0]; | |
| 265 int clientX = touch.clientX; | |
| 266 int clientY = touch.clientY; | |
| 267 int moveX = _lastTouchX - clientX; | |
| 268 int moveY = _lastTouchY - clientY; | |
| 269 _totalMoveX += moveX.abs(); | |
| 270 _totalMoveY += moveY.abs(); | |
| 271 _lastTouchX = clientX; | |
| 272 _lastTouchY = clientY; | |
| 273 if (!_dragging && | |
| 274 ((_totalMoveY > _MIN_TRACKING_FOR_DRAG && _draggable.verticalEnabled) || | |
| 275 (_totalMoveX > _MIN_TRACKING_FOR_DRAG && | |
| 276 _draggable.horizontalEnabled))) { | |
| 277 _dragging = _draggable.onDragStart(e); | |
| 278 if (!_dragging) { | |
| 279 _endTracking(); | |
| 280 } else { | |
| 281 _startTouchX = clientX; | |
| 282 _startTouchY = clientY; | |
| 283 _startTime = e.timeStamp; | |
| 284 } | |
| 285 } | |
| 286 if (_dragging) { | |
| 287 _draggable.onDragMove(); | |
| 288 _lastEvent = e; | |
| 289 e.preventDefault(); | |
| 290 _recentTouchesX = | |
| 291 _removeTouchesInWrongDirection(_recentTouchesX, _lastMoveX, moveX); | |
| 292 _recentTouchesY = | |
| 293 _removeTouchesInWrongDirection(_recentTouchesY, _lastMoveY, moveY); | |
| 294 _recentTouchesX = _removeOldTouches(_recentTouchesX, e.timeStamp); | |
| 295 _recentTouchesY = _removeOldTouches(_recentTouchesY, e.timeStamp); | |
| 296 _recentTouchesX.add(clientX); | |
| 297 _recentTouchesX.add(e.timeStamp); | |
| 298 _recentTouchesY.add(clientY); | |
| 299 _recentTouchesY.add(e.timeStamp); | |
| 300 } | |
| 301 _lastMoveX = moveX; | |
| 302 _lastMoveY = moveY; | |
| 303 } | |
| 304 | |
| 305 /** | |
| 306 * Touch start handler. | |
| 307 */ | |
| 308 void _onStart(TouchEvent e) { | |
| 309 if (_touching) { | |
| 310 return; | |
| 311 } | |
| 312 _touching = true; | |
| 313 if (!_touchable.onTouchStart(e) || _draggable === null) { | |
| 314 return; | |
| 315 } | |
| 316 final touch = e.touches[0]; | |
| 317 _startTouchX = _lastTouchX = touch.clientX; | |
| 318 _startTouchY = _lastTouchY = touch.clientY; | |
| 319 _startTime = e.timeStamp; | |
| 320 // TODO(jacobr): why don't we just clear the lists? | |
| 321 _recentTouchesX = new List<int>(); | |
| 322 _recentTouchesY = new List<int>(); | |
| 323 _recentTouchesX.add(touch.clientX); | |
| 324 _recentTouchesX.add(e.timeStamp); | |
| 325 _recentTouchesY.add(touch.clientY); | |
| 326 _recentTouchesY.add(e.timeStamp); | |
| 327 _lastEvent = e; | |
| 328 _beginTracking(); | |
| 329 } | |
| 330 | |
| 331 /** | |
| 332 * Filters the provided recent touches list to remove all touches older than | |
| 333 * the max tracking time or the 5th most recent touch. | |
| 334 * [recentTouches] specifies a list of tuples where the first item is the x | |
| 335 * or y component of the recent touch and the second item is the touch time | |
| 336 * stamp. The time of the most recent event is specified by [recentTime]. | |
| 337 */ | |
| 338 List<int> _removeOldTouches(List<int> recentTouches, | |
| 339 int recentTime) { | |
| 340 int count = 0; | |
| 341 final len = recentTouches.length; | |
| 342 assert (len % 2 == 0); | |
| 343 while (count < len && | |
| 344 recentTime - recentTouches[count + 1] > _MAX_TRACKING_TIME || | |
| 345 (len - count) > _MAX_TRACKING_TOUCHES * 2) { | |
| 346 count += 2; | |
| 347 } | |
| 348 return count == 0 ? recentTouches : _removeFirstN(recentTouches, count); | |
| 349 } | |
| 350 | |
| 351 static List<int> _removeFirstN(List<int> list, int n) { | |
| 352 return list.getRange(n, list.length - n); | |
| 353 } | |
| 354 | |
| 355 /** | |
| 356 * Filters the provided recent touches list to remove all touches except the | |
| 357 * last if the move direction has changed. | |
| 358 * [recentTouches] specifies a list of tuples where the first item is the x | |
| 359 * or y component of the recent touch and the second item is the touch time | |
| 360 * stamp. The x or y component of the most recent move is specified by | |
| 361 * [recentMove]. | |
| 362 */ | |
| 363 List<int> _removeTouchesInWrongDirection(List<int> recentTouches, | |
| 364 int lastMove, int recentMove) { | |
| 365 if (lastMove !=0 && recentMove != 0 && recentTouches.length > 2 && | |
| 366 _xor(lastMove > 0, recentMove > 0)) { | |
| 367 return _removeFirstN(recentTouches, recentTouches.length - 2); | |
| 368 } | |
| 369 return recentTouches; | |
| 370 } | |
| 371 | |
| 372 // TODO(jacobr): why doesn't bool implement the xor operator directly? | |
| 373 static bool _xor(bool a, bool b) { | |
| 374 return (a === true || b === true) && !(a === true && b === true); | |
| 375 } | |
| 376 | |
| 377 /** | |
| 378 * Reset the touchable element. | |
| 379 */ | |
| 380 void reset() { | |
| 381 _endTracking(); | |
| 382 _touching = false; | |
| 383 } | |
| 384 | |
| 385 /** | |
| 386 * Call this method to enable drag behavior on a draggable delegate. | |
| 387 * The [draggable] object can be the same as the [_touchable] object, they are | |
| 388 * assigned to different members to allow for strong typing with interfaces. | |
| 389 */ | |
| 390 void setDraggable(Draggable draggable) { | |
| 391 _draggable = draggable; | |
| 392 } | |
| 393 } | |
| OLD | NEW |