OLD | NEW |
| (Empty) |
1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. | |
2 | |
3 // Use of this source code is governed by a BSD-style license that can be | |
4 // found in the LICENSE file. | |
5 | |
6 /** | |
7 * @fileoverview Touch Handler. Class that handles all touch events and | |
8 * uses them to interpret higher level gestures and behaviors. TouchEvent is a | |
9 * built in mobile safari type: | |
10 * http://goo.gl/ajOdc | |
11 * This class is intended to work with all webkit browsers, tested on Chrome and | |
12 * iOS. | |
13 * | |
14 * The following types of gestures are currently supported. See the definition | |
15 * of TouchHandler.EventType for details. | |
16 * | |
17 * Single Touch: | |
18 * This provides simple single-touch events. Any secondary touch is | |
19 * ignored. | |
20 * | |
21 * Drag: | |
22 * A single touch followed by some movement. This behavior will handle all | |
23 * of the required events and report the properties of the drag to you | |
24 * while the touch is happening and at the end of the drag sequence. This | |
25 * behavior will NOT perform the actual dragging (redrawing the element) | |
26 * for you, this responsibility is left to the client code. | |
27 * | |
28 * Long press: | |
29 * When your element is touched and held without any drag occuring, the | |
30 * LONG_PRESS event will fire. | |
31 */ | |
32 | |
33 // Use an anonymous function to enable strict mode just for this file (which | |
34 // will be concatenated with other files when embedded in Chrome) | |
35 var TouchHandler = (function() { | |
36 'use strict'; | |
37 | |
38 /** | |
39 * A TouchHandler attaches to an Element, listents for low-level touch (or | |
40 * mouse) events and dispatching higher-level events on the element. | |
41 * @param {!Element} element The element to listen on and fire events | |
42 * for. | |
43 * @constructor | |
44 */ | |
45 function TouchHandler(element) { | |
46 /** | |
47 * @type {!Element} | |
48 * @private | |
49 */ | |
50 this.element_ = element; | |
51 | |
52 /** | |
53 * The absolute sum of all touch y deltas. | |
54 * @type {number} | |
55 * @private | |
56 */ | |
57 this.totalMoveY_ = 0; | |
58 | |
59 /** | |
60 * The absolute sum of all touch x deltas. | |
61 * @type {number} | |
62 * @private | |
63 */ | |
64 this.totalMoveX_ = 0; | |
65 | |
66 /** | |
67 * An array of tuples where the first item is the horizontal component of a | |
68 * recent relevant touch and the second item is the touch's time stamp. Old | |
69 * touches are removed based on the max tracking time and when direction | |
70 * changes. | |
71 * @type {!Array.<number>} | |
72 * @private | |
73 */ | |
74 this.recentTouchesX_ = []; | |
75 | |
76 /** | |
77 * An array of tuples where the first item is the vertical component of a | |
78 * recent relevant touch and the second item is the touch's time stamp. Old | |
79 * touches are removed based on the max tracking time and when direction | |
80 * changes. | |
81 * @type {!Array.<number>} | |
82 * @private | |
83 */ | |
84 this.recentTouchesY_ = []; | |
85 | |
86 /** | |
87 * Used to keep track of all events we subscribe to so we can easily clean | |
88 * up | |
89 * @type {EventTracker} | |
90 * @private | |
91 */ | |
92 this.events_ = new EventTracker(); | |
93 } | |
94 | |
95 | |
96 /** | |
97 * DOM Events that may be fired by the TouchHandler at the element | |
98 */ | |
99 TouchHandler.EventType = { | |
100 // Fired whenever the element is touched as the only touch to the device. | |
101 // enableDrag defaults to false, set to true to permit dragging. | |
102 TOUCH_START: 'touchhandler:touch_start', | |
103 | |
104 // Fired when an element is held for a period of time. Prevents dragging | |
105 // from occuring (even if enableDrag was set to true). | |
106 LONG_PRESS: 'touchhandler:long_press', | |
107 | |
108 // If enableDrag was set to true at TOUCH_START, DRAG_START will fire when | |
109 // the touch first moves sufficient distance. enableDrag is set to true but | |
110 // can be reset to false to cancel the drag. | |
111 DRAG_START: 'touchhandler:drag_start', | |
112 | |
113 // If enableDrag was true after DRAG_START, DRAG_MOVE will fire whenever the | |
114 // touch is moved. | |
115 DRAG_MOVE: 'touchhandler:drag_move', | |
116 | |
117 // Fired just before TOUCH_END when a drag is released. Correlates 1:1 with | |
118 // a DRAG_START. | |
119 DRAG_END: 'touchhandler:drag_end', | |
120 | |
121 // Fired whenever a touch that is being tracked has been released. | |
122 // Correlates 1:1 with a TOUCH_START. | |
123 TOUCH_END: 'touchhandler:touch_end' | |
124 }; | |
125 | |
126 | |
127 /** | |
128 * The type of event sent by TouchHandler | |
129 * @constructor | |
130 * @param {string} type The type of event (one of Grabber.EventType). | |
131 * @param {boolean} bubbles Whether or not the event should bubble. | |
132 * @param {number} clientX The X location of the touch. | |
133 * @param {number} clientY The Y location of the touch. | |
134 * @param {!Element} touchedElement The element at the current location of the | |
135 * touch. | |
136 */ | |
137 TouchHandler.Event = function(type, bubbles, clientX, clientY, | |
138 touchedElement) { | |
139 var event = document.createEvent('Event'); | |
140 event.initEvent(type, bubbles, true); | |
141 event.__proto__ = TouchHandler.Event.prototype; | |
142 | |
143 /** | |
144 * The X location of the touch affected | |
145 * @type {number} | |
146 */ | |
147 event.clientX = clientX; | |
148 | |
149 /** | |
150 * The Y location of the touch affected | |
151 * @type {number} | |
152 */ | |
153 event.clientY = clientY; | |
154 | |
155 /** | |
156 * The element at the current location of the touch. | |
157 * @type {!Element} | |
158 */ | |
159 event.touchedElement = touchedElement; | |
160 | |
161 return event; | |
162 }; | |
163 | |
164 TouchHandler.Event.prototype = { | |
165 __proto__: Event.prototype, | |
166 | |
167 /** | |
168 * For TOUCH_START and DRAG START events, set to true to enable dragging or | |
169 * false to disable dragging. | |
170 * @type {boolean|undefined} | |
171 */ | |
172 enableDrag: undefined, | |
173 | |
174 /** | |
175 * For DRAG events, provides the horizontal component of the | |
176 * drag delta. Drag delta is defined as the delta of the start touch | |
177 * position and the current drag position. | |
178 * @type {number|undefined} | |
179 */ | |
180 dragDeltaX: undefined, | |
181 | |
182 /** | |
183 * For DRAG events, provides the vertical component of the | |
184 * drag delta. | |
185 * @type {number|undefined} | |
186 */ | |
187 dragDeltaY: undefined | |
188 }; | |
189 | |
190 /** | |
191 * Minimum movement of touch required to be considered a drag. | |
192 * @type {number} | |
193 * @private | |
194 */ | |
195 TouchHandler.MIN_TRACKING_FOR_DRAG_ = 8; | |
196 | |
197 | |
198 /** | |
199 * The maximum number of ms to track a touch event. After an event is older | |
200 * than this value, it will be ignored in velocity calculations. | |
201 * @type {number} | |
202 * @private | |
203 */ | |
204 TouchHandler.MAX_TRACKING_TIME_ = 250; | |
205 | |
206 | |
207 /** | |
208 * The maximum number of touches to track. | |
209 * @type {number} | |
210 * @private | |
211 */ | |
212 TouchHandler.MAX_TRACKING_TOUCHES_ = 5; | |
213 | |
214 | |
215 /** | |
216 * The maximum velocity to return, in pixels per millisecond, that is used | |
217 * to guard against errors in calculating end velocity of a drag. This is a | |
218 * very fast drag velocity. | |
219 * @type {number} | |
220 * @private | |
221 */ | |
222 TouchHandler.MAXIMUM_VELOCITY_ = 5; | |
223 | |
224 | |
225 /** | |
226 * The velocity to return, in pixel per millisecond, when the time stamps on | |
227 * the events are erroneous. The browser can return bad time stamps if the | |
228 * thread is blocked for the duration of the drag. This is a low velocity to | |
229 * prevent the content from moving quickly after a slow drag. It is less | |
230 * jarring if the content moves slowly after a fast drag. | |
231 * @type {number} | |
232 * @private | |
233 */ | |
234 TouchHandler.VELOCITY_FOR_INCORRECT_EVENTS_ = 1; | |
235 | |
236 /** | |
237 * The time, in milliseconds, that a touch must be held to be considered | |
238 * 'long'. | |
239 * @type {number} | |
240 * @private | |
241 */ | |
242 TouchHandler.TIME_FOR_LONG_PRESS_ = 500; | |
243 | |
244 TouchHandler.prototype = { | |
245 /** | |
246 * If defined, the identifer of the single touch that is active. Note that | |
247 * 0 is a valid touch identifier - it should not be treated equivalently to | |
248 * undefined. | |
249 * @type {number|undefined} | |
250 * @private | |
251 */ | |
252 activeTouch_: undefined, | |
253 | |
254 /** | |
255 * @type {boolean|undefined} | |
256 * @private | |
257 */ | |
258 tracking_: undefined, | |
259 | |
260 /** | |
261 * @type {number|undefined} | |
262 * @private | |
263 */ | |
264 startTouchX_: undefined, | |
265 | |
266 /** | |
267 * @type {number|undefined} | |
268 * @private | |
269 */ | |
270 startTouchY_: undefined, | |
271 | |
272 /** | |
273 * @type {number|undefined} | |
274 * @private | |
275 */ | |
276 endTouchX_: undefined, | |
277 | |
278 /** | |
279 * @type {number|undefined} | |
280 * @private | |
281 */ | |
282 endTouchY_: undefined, | |
283 | |
284 /** | |
285 * Time of the touchstart event. | |
286 * @type {number|undefined} | |
287 * @private | |
288 */ | |
289 startTime_: undefined, | |
290 | |
291 /** | |
292 * The time of the touchend event. | |
293 * @type {number|undefined} | |
294 * @private | |
295 */ | |
296 endTime_: undefined, | |
297 | |
298 /** | |
299 * @type {number|undefined} | |
300 * @private | |
301 */ | |
302 lastTouchX_: undefined, | |
303 | |
304 /** | |
305 * @type {number|undefined} | |
306 * @private | |
307 */ | |
308 lastTouchY_: undefined, | |
309 | |
310 /** | |
311 * @type {number|undefined} | |
312 * @private | |
313 */ | |
314 lastMoveX_: undefined, | |
315 | |
316 /** | |
317 * @type {number|undefined} | |
318 * @private | |
319 */ | |
320 lastMoveY_: undefined, | |
321 | |
322 /** | |
323 * @type {number|undefined} | |
324 * @private | |
325 */ | |
326 longPressTimeout_: undefined, | |
327 | |
328 /** | |
329 * If defined and true, the next click event should be swallowed | |
330 * @type {boolean|undefined} | |
331 * @private | |
332 */ | |
333 swallowNextClick_: undefined, | |
334 | |
335 /** | |
336 * Start listenting for events. | |
337 * @param {boolean=} opt_capture True if the TouchHandler should listen to | |
338 * during the capture phase. | |
339 */ | |
340 enable: function(opt_capture) { | |
341 var capture = !!opt_capture; | |
342 | |
343 // Just listen to start events for now. When a touch is occuring we'll | |
344 // want to be subscribed to move and end events on the document, but we | |
345 // don't want to incur the cost of lots of no-op handlers on the document. | |
346 this.events_.add(this.element_, 'touchstart', this.onStart_.bind(this), | |
347 capture); | |
348 this.events_.add(this.element_, 'mousedown', | |
349 this.mouseToTouchCallback_(this.onStart_.bind(this)), | |
350 capture); | |
351 | |
352 // If the element is long-pressed, we may need to swallow a click | |
353 this.events_.add(this.element_, 'click', this.onClick_.bind(this), true); | |
354 }, | |
355 | |
356 /** | |
357 * Stop listening to all events. | |
358 */ | |
359 disable: function() { | |
360 this.stopTouching_(); | |
361 this.events_.removeAll(); | |
362 }, | |
363 | |
364 /** | |
365 * Wraps a callback with translations of mouse events to touch events. | |
366 * NOTE: These types really should be function(Event) but then we couldn't | |
367 * use this with bind (which operates on any type of function). Doesn't | |
368 * JSDoc support some sort of polymorphic types? | |
369 * @param {Function} callback The event callback. | |
370 * @return {Function} The wrapping callback. | |
371 * @private | |
372 */ | |
373 mouseToTouchCallback_: function(callback) { | |
374 return function(e) { | |
375 // Note that there may be synthesizes mouse events caused by touch | |
376 // events (a mouseDown after a touch-click). We leave it up to the | |
377 // client to worry about this if it matters to them (typically a short | |
378 // mouseDown/mouseUp without a click is no big problem and it's not | |
379 // obvious how we identify such synthesized events in a general way). | |
380 var touch = { | |
381 // any fixed value will do for the identifier - there will only | |
382 // ever be a single active 'touch' when using the mouse. | |
383 identifier: 0, | |
384 clientX: e.clientX, | |
385 clientY: e.clientY, | |
386 target: e.target | |
387 }; | |
388 e.touches = []; | |
389 e.targetTouches = []; | |
390 e.changedTouches = [touch]; | |
391 if (e.type != 'mouseup') { | |
392 e.touches[0] = touch; | |
393 e.targetTouches[0] = touch; | |
394 } | |
395 callback(e); | |
396 }; | |
397 }, | |
398 | |
399 /** | |
400 * Begin tracking the touchable element, it is eligible for dragging. | |
401 * @private | |
402 */ | |
403 beginTracking_: function() { | |
404 this.tracking_ = true; | |
405 }, | |
406 | |
407 /** | |
408 * Stop tracking the touchable element, it is no longer dragging. | |
409 * @private | |
410 */ | |
411 endTracking_: function() { | |
412 this.tracking_ = false; | |
413 this.dragging_ = false; | |
414 this.totalMoveY_ = 0; | |
415 this.totalMoveX_ = 0; | |
416 }, | |
417 | |
418 /** | |
419 * Reset the touchable element as if we never saw the touchStart | |
420 * Doesn't dispatch any end events - be careful of existing listeners. | |
421 */ | |
422 cancelTouch: function() { | |
423 this.stopTouching_(); | |
424 this.endTracking_(); | |
425 // If clients needed to be aware of this, we could fire a cancel event | |
426 // here. | |
427 }, | |
428 | |
429 /** | |
430 * Record that touching has stopped | |
431 * @private | |
432 */ | |
433 stopTouching_: function() { | |
434 // Mark as no longer being touched | |
435 this.activeTouch_ = undefined; | |
436 | |
437 // If we're waiting for a long press, stop | |
438 window.clearTimeout(this.longPressTimeout_); | |
439 | |
440 // Stop listening for move/end events until there's another touch. | |
441 // We don't want to leave handlers piled up on the document. | |
442 // Note that there's no harm in removing handlers that weren't added, so | |
443 // rather than track whether we're using mouse or touch we do both. | |
444 this.events_.remove(document, 'touchmove'); | |
445 this.events_.remove(document, 'touchend'); | |
446 this.events_.remove(document, 'touchcancel'); | |
447 this.events_.remove(document, 'mousemove'); | |
448 this.events_.remove(document, 'mouseup'); | |
449 }, | |
450 | |
451 /** | |
452 * Touch start handler. | |
453 * @param {!TouchEvent} e The touchstart event. | |
454 * @private | |
455 */ | |
456 onStart_: function(e) { | |
457 // Only process single touches. If there is already a touch happening, or | |
458 // two simultaneous touches then just ignore them. | |
459 if (e.touches.length > 1) | |
460 // Note that we could cancel an active touch here. That would make | |
461 // simultaneous touch behave similar to near-simultaneous. However, if | |
462 // the user is dragging something, an accidental second touch could be | |
463 // quite disruptive if it cancelled their drag. Better to just ignore | |
464 // it. | |
465 return; | |
466 | |
467 // It's still possible there could be an active "touch" if the user is | |
468 // simultaneously using a mouse and a touch input. | |
469 if (this.activeTouch_ !== undefined) | |
470 return; | |
471 | |
472 var touch = e.targetTouches[0]; | |
473 this.activeTouch_ = touch.identifier; | |
474 | |
475 // We've just started touching so shouldn't swallow any upcoming click | |
476 if (this.swallowNextClick_) | |
477 this.swallowNextClick_ = false; | |
478 | |
479 // Sign up for end/cancel notifications for this touch. | |
480 // Note that we do this on the document so that even if the user drags | |
481 // their finger off the element, we'll still know what they're doing. | |
482 if (e.type == 'mousedown') { | |
483 this.events_.add(document, 'mouseup', | |
484 this.mouseToTouchCallback_(this.onEnd_.bind(this)), false); | |
485 } else { | |
486 this.events_.add(document, 'touchend', this.onEnd_.bind(this), false); | |
487 this.events_.add(document, 'touchcancel', this.onEnd_.bind(this), | |
488 false); | |
489 } | |
490 | |
491 // This timeout is cleared on touchEnd and onDrag | |
492 // If we invoke the function then we have a real long press | |
493 window.clearTimeout(this.longPressTimeout_); | |
494 this.longPressTimeout_ = window.setTimeout( | |
495 this.onLongPress_.bind(this), | |
496 TouchHandler.TIME_FOR_LONG_PRESS_); | |
497 | |
498 // Dispatch the TOUCH_START event | |
499 if (!this.dispatchEvent_(TouchHandler.EventType.TOUCH_START, touch)) | |
500 // Dragging was not enabled, nothing more to do | |
501 return; | |
502 | |
503 // We want dragging notifications | |
504 if (e.type == 'mousedown') { | |
505 this.events_.add(document, 'mousemove', | |
506 this.mouseToTouchCallback_(this.onMove_.bind(this)), false); | |
507 } else { | |
508 this.events_.add(document, 'touchmove', this.onMove_.bind(this), false); | |
509 } | |
510 | |
511 this.startTouchX_ = this.lastTouchX_ = touch.clientX; | |
512 this.startTouchY_ = this.lastTouchY_ = touch.clientY; | |
513 this.startTime_ = e.timeStamp; | |
514 | |
515 this.recentTouchesX_ = []; | |
516 this.recentTouchesY_ = []; | |
517 this.recentTouchesX_.push(touch.clientX, e.timeStamp); | |
518 this.recentTouchesY_.push(touch.clientY, e.timeStamp); | |
519 | |
520 this.beginTracking_(); | |
521 }, | |
522 | |
523 /** | |
524 * Given a list of Touches, find the one matching our activeTouch | |
525 * identifier. Note that Chrome currently always uses 0 as the identifier. | |
526 * In that case we'll end up always choosing the first element in the list. | |
527 * @param {TouchList} touches The list of Touch objects to search. | |
528 * @return {!Touch|undefined} The touch matching our active ID if any. | |
529 * @private | |
530 */ | |
531 findActiveTouch_: function(touches) { | |
532 assert(this.activeTouch_ !== undefined, 'Expecting an active touch'); | |
533 // A TouchList isn't actually an array, so we shouldn't use | |
534 // Array.prototype.filter/some, etc. | |
535 for (var i = 0; i < touches.length; i++) { | |
536 if (touches[i].identifier == this.activeTouch_) | |
537 return touches[i]; | |
538 } | |
539 return undefined; | |
540 }, | |
541 | |
542 /** | |
543 * Touch move handler. | |
544 * @param {!TouchEvent} e The touchmove event. | |
545 * @private | |
546 */ | |
547 onMove_: function(e) { | |
548 if (!this.tracking_) | |
549 return; | |
550 | |
551 // Our active touch should always be in the list of touches still active | |
552 assert(this.findActiveTouch_(e.touches), 'Missing touchEnd'); | |
553 | |
554 var that = this; | |
555 var touch = this.findActiveTouch_(e.changedTouches); | |
556 if (!touch) | |
557 return; | |
558 | |
559 var clientX = touch.clientX; | |
560 var clientY = touch.clientY; | |
561 | |
562 var moveX = this.lastTouchX_ - clientX; | |
563 var moveY = this.lastTouchY_ - clientY; | |
564 this.totalMoveX_ += Math.abs(moveX); | |
565 this.totalMoveY_ += Math.abs(moveY); | |
566 this.lastTouchX_ = clientX; | |
567 this.lastTouchY_ = clientY; | |
568 | |
569 if (!this.dragging_ && (this.totalMoveY_ > | |
570 TouchHandler.MIN_TRACKING_FOR_DRAG_ || | |
571 this.totalMoveX_ > | |
572 TouchHandler.MIN_TRACKING_FOR_DRAG_)) { | |
573 // If we're waiting for a long press, stop | |
574 window.clearTimeout(this.longPressTimeout_); | |
575 | |
576 // Dispatch the DRAG_START event and record whether dragging should be | |
577 // allowed or not. Note that this relies on the current value of | |
578 // startTouchX/Y - handlers may use the initial drag delta to determine | |
579 // if dragging should be permitted. | |
580 this.dragging_ = this.dispatchEvent_( | |
581 TouchHandler.EventType.DRAG_START, touch); | |
582 | |
583 if (this.dragging_) { | |
584 // Update the start position here so that drag deltas have better | |
585 // values but don't touch the recent positions so that velocity | |
586 // calculations can still use touchstart position in the time and | |
587 // distance delta. | |
588 this.startTouchX_ = clientX; | |
589 this.startTouchY_ = clientY; | |
590 this.startTime_ = e.timeStamp; | |
591 } else { | |
592 this.endTracking_(); | |
593 } | |
594 } | |
595 | |
596 if (this.dragging_) { | |
597 this.dispatchEvent_(TouchHandler.EventType.DRAG_MOVE, touch); | |
598 | |
599 this.removeTouchesInWrongDirection_(this.recentTouchesX_, | |
600 this.lastMoveX_, moveX); | |
601 this.removeTouchesInWrongDirection_(this.recentTouchesY_, | |
602 this.lastMoveY_, moveY); | |
603 this.removeOldTouches_(this.recentTouchesX_, e.timeStamp); | |
604 this.removeOldTouches_(this.recentTouchesY_, e.timeStamp); | |
605 this.recentTouchesX_.push(clientX, e.timeStamp); | |
606 this.recentTouchesY_.push(clientY, e.timeStamp); | |
607 } | |
608 | |
609 this.lastMoveX_ = moveX; | |
610 this.lastMoveY_ = moveY; | |
611 }, | |
612 | |
613 /** | |
614 * Filters the provided recent touches array to remove all touches except | |
615 * the last if the move direction has changed. | |
616 * @param {!Array.<number>} recentTouches An array of tuples where the first | |
617 * item is the x or y component of the recent touch and the second item | |
618 * is the touch time stamp. | |
619 * @param {number|undefined} lastMove The x or y component of the previous | |
620 * move. | |
621 * @param {number} recentMove The x or y component of the most recent move. | |
622 * @private | |
623 */ | |
624 removeTouchesInWrongDirection_: function(recentTouches, lastMove, | |
625 recentMove) { | |
626 if (lastMove && recentMove && recentTouches.length > 2 && | |
627 (lastMove > 0 ^ recentMove > 0)) { | |
628 recentTouches.splice(0, recentTouches.length - 2); | |
629 } | |
630 }, | |
631 | |
632 /** | |
633 * Filters the provided recent touches array to remove all touches older | |
634 * than the max tracking time or the 5th most recent touch. | |
635 * @param {!Array.<number>} recentTouches An array of tuples where the first | |
636 * item is the x or y component of the recent touch and the second item | |
637 * is the touch time stamp. | |
638 * @param {number} recentTime The time of the most recent event. | |
639 * @private | |
640 */ | |
641 removeOldTouches_: function(recentTouches, recentTime) { | |
642 while (recentTouches.length && recentTime - recentTouches[1] > | |
643 TouchHandler.MAX_TRACKING_TIME_ || | |
644 recentTouches.length > | |
645 TouchHandler.MAX_TRACKING_TOUCHES_ * 2) { | |
646 recentTouches.splice(0, 2); | |
647 } | |
648 }, | |
649 | |
650 /** | |
651 * Touch end handler. | |
652 * @param {!TouchEvent} e The touchend event. | |
653 * @private | |
654 */ | |
655 onEnd_: function(e) { | |
656 var that = this; | |
657 assert(this.activeTouch_ !== undefined, 'Expect to already be touching'); | |
658 | |
659 // If the touch we're tracking isn't changing here, ignore this touch end. | |
660 var touch = this.findActiveTouch_(e.changedTouches); | |
661 if (!touch) { | |
662 // In most cases, our active touch will be in the 'touches' collection, | |
663 // but we can't assert that because occasionally two touchend events can | |
664 // occur at almost the same time with both having empty 'touches' lists. | |
665 // I.e., 'touches' seems like it can be a bit more up-to-date than the | |
666 // current event. | |
667 return; | |
668 } | |
669 | |
670 // This is touchEnd for the touch we're monitoring | |
671 assert(!this.findActiveTouch_(e.touches), | |
672 'Touch ended also still active'); | |
673 | |
674 // Indicate that touching has finished | |
675 this.stopTouching_(); | |
676 | |
677 if (this.tracking_) { | |
678 var clientX = touch.clientX; | |
679 var clientY = touch.clientY; | |
680 | |
681 if (this.dragging_) { | |
682 this.endTime_ = e.timeStamp; | |
683 this.endTouchX_ = clientX; | |
684 this.endTouchY_ = clientY; | |
685 | |
686 this.removeOldTouches_(this.recentTouchesX_, e.timeStamp); | |
687 this.removeOldTouches_(this.recentTouchesY_, e.timeStamp); | |
688 | |
689 this.dispatchEvent_(TouchHandler.EventType.DRAG_END, touch); | |
690 | |
691 // Note that in some situations we can get a click event here as well. | |
692 // For now this isn't a problem, but we may want to consider having | |
693 // some logic that hides clicks that appear to be caused by a touchEnd | |
694 // used for dragging. | |
695 } | |
696 | |
697 this.endTracking_(); | |
698 } | |
699 | |
700 // Note that we dispatch the touchEnd event last so that events at | |
701 // different levels of semantics nest nicely (similar to how DOM | |
702 // drag-and-drop events are nested inside of the mouse events that trigger | |
703 // them). | |
704 this.dispatchEvent_(TouchHandler.EventType.TOUCH_END, touch); | |
705 }, | |
706 | |
707 /** | |
708 * Get end velocity of the drag. This method is specific to drag behavior, | |
709 * so if touch behavior and drag behavior is split then this should go with | |
710 * drag behavior. End velocity is defined as deltaXY / deltaTime where | |
711 * deltaXY is the difference between endPosition and the oldest recent | |
712 * position, and deltaTime is the difference between endTime and the oldest | |
713 * recent time stamp. | |
714 * @return {Object} The x and y velocity. | |
715 */ | |
716 getEndVelocity: function() { | |
717 // Note that we could move velocity to just be an end-event parameter. | |
718 var velocityX = this.recentTouchesX_.length ? | |
719 (this.endTouchX_ - this.recentTouchesX_[0]) / | |
720 (this.endTime_ - this.recentTouchesX_[1]) : 0; | |
721 var velocityY = this.recentTouchesY_.length ? | |
722 (this.endTouchY_ - this.recentTouchesY_[0]) / | |
723 (this.endTime_ - this.recentTouchesY_[1]) : 0; | |
724 | |
725 velocityX = this.correctVelocity_(velocityX); | |
726 velocityY = this.correctVelocity_(velocityY); | |
727 | |
728 return { | |
729 x: velocityX, | |
730 y: velocityY | |
731 }; | |
732 }, | |
733 | |
734 /** | |
735 * Correct erroneous velocities by capping the velocity if we think it's too | |
736 * high, or setting it to a default velocity if know that the event data is | |
737 * bad. | |
738 * @param {number} velocity The x or y velocity component. | |
739 * @return {number} The corrected velocity. | |
740 * @private | |
741 */ | |
742 correctVelocity_: function(velocity) { | |
743 var absVelocity = Math.abs(velocity); | |
744 | |
745 // We add to recent touches for each touchstart and touchmove. If we have | |
746 // fewer than 3 touches (6 entries), we assume that the thread was blocked | |
747 // for the duration of the drag and we received events in quick succession | |
748 // with the wrong time stamps. | |
749 if (absVelocity > TouchHandler.MAXIMUM_VELOCITY_) { | |
750 absVelocity = this.recentTouchesY_.length < 3 ? | |
751 TouchHandler.VELOCITY_FOR_INCORRECT_EVENTS_ : | |
752 TouchHandler.MAXIMUM_VELOCITY_; | |
753 } | |
754 return absVelocity * (velocity < 0 ? -1 : 1); | |
755 }, | |
756 | |
757 /** | |
758 * Handler when an element has been pressed for a long time | |
759 * @private | |
760 */ | |
761 onLongPress_: function() { | |
762 // Swallow any click that occurs on this element without an intervening | |
763 // touch start event. This simple click-busting technique should be | |
764 // sufficient here since a real click should have a touchstart first. | |
765 this.swallowNextClick_ = true; | |
766 | |
767 // Dispatch to the LONG_PRESS | |
768 this.dispatchEventXY_(TouchHandler.EventType.LONG_PRESS, this.element_, | |
769 this.startTouchX_, this.startTouchY_); | |
770 }, | |
771 | |
772 /** | |
773 * Click handler - used to swallow clicks after a long-press | |
774 * @param {!Event} e The click event. | |
775 * @private | |
776 */ | |
777 onClick_: function(e) { | |
778 if (this.swallowNextClick_) { | |
779 e.preventDefault(); | |
780 e.stopPropagation(); | |
781 this.swallowNextClick_ = false; | |
782 } | |
783 }, | |
784 | |
785 /** | |
786 * Dispatch a TouchHandler event to the element | |
787 * @param {string} eventType The event to dispatch. | |
788 * @param {Touch} touch The touch triggering this event. | |
789 * @return {boolean|undefined} The value of enableDrag after dispatching | |
790 * the event. | |
791 * @private | |
792 */ | |
793 dispatchEvent_: function(eventType, touch) { | |
794 | |
795 // Determine which element was touched. For mouse events, this is always | |
796 // the event/touch target. But for touch events, the target is always the | |
797 // target of the touchstart (and it's unlikely we can change this | |
798 // since the common implementation of touch dragging relies on it). Since | |
799 // touch is our primary scenario (which we want to emulate with mouse), | |
800 // we'll treat both cases the same and not depend on the target. | |
801 var touchedElement; | |
802 if (eventType == TouchHandler.EventType.TOUCH_START) { | |
803 touchedElement = touch.target; | |
804 } else { | |
805 touchedElement = this.element_.ownerDocument. | |
806 elementFromPoint(touch.clientX, touch.clientY); | |
807 } | |
808 | |
809 return this.dispatchEventXY_(eventType, touchedElement, touch.clientX, | |
810 touch.clientY); | |
811 }, | |
812 | |
813 /** | |
814 * Dispatch a TouchHandler event to the element | |
815 * @param {string} eventType The event to dispatch. | |
816 @param {number} clientX The X location for the event. | |
817 @param {number} clientY The Y location for the event. | |
818 * @return {boolean|undefined} The value of enableDrag after dispatching | |
819 * the event. | |
820 * @private | |
821 */ | |
822 dispatchEventXY_: function(eventType, touchedElement, clientX, clientY) { | |
823 var isDrag = (eventType == TouchHandler.EventType.DRAG_START || | |
824 eventType == TouchHandler.EventType.DRAG_MOVE || | |
825 eventType == TouchHandler.EventType.DRAG_END); | |
826 | |
827 // Drag events don't bubble - we're really just dragging the element, | |
828 // not affecting its parent at all. | |
829 var bubbles = !isDrag; | |
830 | |
831 var event = new TouchHandler.Event(eventType, bubbles, clientX, clientY, | |
832 touchedElement); | |
833 | |
834 // Set enableDrag when it can be overridden | |
835 if (eventType == TouchHandler.EventType.TOUCH_START) | |
836 event.enableDrag = false; | |
837 else if (eventType == TouchHandler.EventType.DRAG_START) | |
838 event.enableDrag = true; | |
839 | |
840 if (isDrag) { | |
841 event.dragDeltaX = clientX - this.startTouchX_; | |
842 event.dragDeltaY = clientY - this.startTouchY_; | |
843 } | |
844 | |
845 this.element_.dispatchEvent(event); | |
846 return event.enableDrag; | |
847 } | |
848 }; | |
849 | |
850 return TouchHandler; | |
851 })(); | |
OLD | NEW |