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