1 var XUI_GestureUtils = {
\r
3 * get the center of all the touches
\r
4 * @param {Array} touches
\r
5 * @returns {Object} center
\r
7 getCenter : function( touches ){
\r
10 x, y, minX, minY, maxX, maxY;
\r
17 pageX : touches[ 0 ].pageX,
\r
18 pageY : touches[ 0 ].pageY
\r
22 pageX : ( touches[ 0 ].pageX + touches[ 1 ].pageX ) / 2,
\r
23 pageY : ( touches[ 0 ].pageY + touches[ 1 ].pageY ) / 2
\r
26 minX = minY = 1 / 0;
\r
27 maxX = maxY = - 1 / 0;
\r
28 for( ; i < l; ++i ){
\r
29 x = touches[ i ].pageX;
\r
30 minX = x < minX ? x : minX;
\r
31 maxX = maxX < x ? x : maxX;
\r
32 y = touches[ i ].pageY;
\r
33 minY = y < minY ? y : minY;
\r
34 maxY = maxY < y ? y : maxY;
\r
37 pageX : ( minX + maxX ) / 2 | 0,
\r
38 pageY : ( minY + maxY ) / 2 | 0
\r
43 * calculate the velocity between two points
\r
44 * @param {Number} deltaTime
\r
45 * @param {Number} deltaX
\r
46 * @param {Number} deltaY
\r
47 * @returns {Object} velocity
\r
49 getVelocity : function( deltaTime, deltaX, deltaY ) {
\r
51 x : Math.abs( deltaX / deltaTime ) || 0,
\r
52 y : Math.abs( deltaY / deltaTime ) || 0
\r
57 * calculate the angle between two coordinates
\r
58 * @param {Touch} touch1
\r
59 * @param {Touch} touch2
\r
60 * @returns {Number} angle
\r
62 getAngle : function( touch1, touch2 ){
\r
63 var y = touch2.pageY - touch1.pageY,
\r
64 x = touch2.pageX - touch1.pageX;
\r
65 return Math.atan2( y, x ) * 180 / Math.PI;
\r
69 * angle to direction define
\r
70 * @param {Touch} touch1
\r
71 * @param {Touch} touch2
\r
72 * @returns {String} direction constant, like 'left'
\r
74 getDirection : function( touch1, touch2 ){
\r
75 var x = touch1.pageX - touch2.pageX,
\r
76 y = touch1.pageY - touch2.pageY;
\r
77 return Math.abs( y ) <= Math.abs( x ) ?
\r
78 ( x > 0 ? 'left' : 'right' ) :
\r
79 ( y > 0 ? 'up' : 'down' );
\r
83 * calculate the distance between two touches
\r
84 * @param {Touch} touch1
\r
85 * @param {Touch} touch2
\r
86 * @returns {Number} distance
\r
88 getDistance : function( touch1, touch2 ){
\r
89 var x = touch2.pageX - touch1.pageX,
\r
90 y = touch2.pageY - touch1.pageY;
\r
91 return Math.sqrt( ( x * x ) + ( y * y ) );
\r
95 * calculate the scale factor between two touchLists (fingers)
\r
96 * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
\r
97 * @param {Array} start
\r
98 * @param {Array} end
\r
99 * @returns {Number} scale
\r
101 getScale : function( start, end ){
\r
102 // need two fingers...
\r
103 return ( 2 <= start.length && 2 <= end.length ) ?
\r
104 XUI_GestureUtils.getDistance( end[ 0 ], end[ 1 ] ) / XUI_GestureUtils.getDistance( start[ 0 ], start[ 1 ] ) :
\r
109 * calculate the rotation degrees between two touchLists (fingers)
\r
110 * @param {Array} start
\r
111 * @param {Array} end
\r
112 * @returns {Number} rotation
\r
114 getRotation : function getRotation( start, end ){
\r
115 // need two fingers
\r
116 return ( 2 <= start.length && 2 <= end.length ) ?
\r
117 XUI_GestureUtils.getAngle( end[ 1 ], end[ 0 ] ) - XUI_GestureUtils.getAngle( start[ 1 ], start[ 0 ] ) :
\r
122 * boolean if the direction is vertical
\r
123 * @param {String} direction
\r
124 * @returns {Boolean} is_vertical
\r
126 isVertical : function isVertical( direction ){
\r
127 return direction === 'up' || direction === 'down';
\r
131 var XUI_Gesture_POINTERS = {},
\r
132 XUI_Gesture_CAPTURED = {},
\r
133 XUI_Gesture_DEFAULTS = {};
\r
135 // AbstractUINode に移動
\r
137 var XUI_Gesture = Hammer = X_Class_create(
\r
154 lastMoveEvent : null,
\r
156 'Constructor' : function( uinodeRoot, uinode, type, opt_options ){
\r
157 this.uinodeRoot = uinodeRoot;
\r
158 this.uinode = uinode;
\r
159 this.options = X_Object_override( X_Object_copy( XUI_Gesture_DEFAULTS ), opt_options );
\r
161 this.triggered = {};
\r
162 this.canceled = {};
\r
164 uinode[ 'listen' ]( XUI_Event._POINTER_DOWN, this, XUI_Gesture_handleEvent );
\r
166 this[ 'listen' ]( type );
\r
169 trigger : function( type, gesture ){
\r
172 if( !this.types[ type ] ) return X_CALLBACK_NONE;
\r
173 e = X_Object_copy( gesture );
\r
175 return this.uinode[ 'dispatch' ]( e ) || X_CALLBACK_NONE;
\r
178 listen : function( type ){
\r
179 var gestures = XUI_Gesture_LIST,
\r
180 i = gestures.length, g;
\r
183 g = gestures[ --i ];
\r
184 if( g.startID <= type && type <= g.endID ){
\r
185 if( !this.activated ) this.activated = {};
\r
186 if( !this.types ) this.types = {};
\r
187 this.activated[ g.name ] = this.types[ type ] = 1;
\r
193 unlisten : function( type ){
\r
194 var gestures = XUI_Gesture_LIST,
\r
195 i = gestures.length,
\r
196 active = this.activated, g;
\r
198 if( !active ) return;
\r
200 g = gestures[ --i ];
\r
201 if( g.startID <= type && type <= g.endID ){
\r
202 if( active[ g.name ] ){
\r
203 if( this.types[ type ] ) delete this.types[ type ];
\r
204 for( i = g.startID; i <= g.endID; ++i ){
\r
205 if( this.types[ i ] ) return;
\r
207 delete active[ g.name ];
\r
216 function XUI_Gesture_handleEvent( e ){
\r
217 var gestures = XUI_Gesture_LIST,
\r
219 uid = e[ 'pointerId' ],
\r
220 isStart = type === XUI_Event._POINTER_DOWN,
\r
221 isEnd = type === XUI_Event._POINTER_UP || type === XUI_Event._POINTER_CANCEL || type === XUI_Event.POINTER_OUT,
\r
223 isMouse = e.pointerType === 'mouse',
\r
225 numTouches = 0,// count the total touches on the screen
\r
226 i, p, l, j, captured, ret, activated, gesture, startEv,
\r
227 deltaTime, deltaX, deltaY, velocity, center, startCenter;
\r
229 if( !isStart && !hammer.startEvent ) return;
\r
231 if( type === XUI_Event.POINTER_OUT ) console.log( 'canceled ...' + e.button )
\r
234 if( XUI_Gesture_POINTERS[ uid ] ){
\r
235 delete XUI_Gesture_POINTERS[ uid ];
\r
236 if( XUI_Gesture_CAPTURED[ uid ] ) delete XUI_Gesture_CAPTURED[ uid ];
\r
239 XUI_Gesture_POINTERS[ uid ] = e;
\r
242 // mousebutton must be down or a touch event
\r
243 if( ( isEnd || !isMouse || e.button === 0 ) ){
\r
246 for( i in XUI_Gesture_POINTERS ){
\r
247 if( p = XUI_Gesture_POINTERS[ i ] ){
\r
248 // いずれかの hammer によって束縛されている場合、その束縛している hammer なら
\r
249 captured = XUI_Gesture_CAPTURED[ p[ 'pointerId' ] ];
\r
250 if( captured && captured !== hammer ){
\r
253 touches[ ++numTouches ] = p;
\r
258 // if we are in a end event, but when we remove one touch and
\r
259 // we still have enough, set eventType to move
\r
260 if( !numTouches ){ // no touches, force the end event
\r
264 // because touchend has no touches, and we often want to use these in our gestures,
\r
265 // we send the last move event as our eventData in touchend
\r
266 ( isEnd && hammer.lastMoveEvent ) ? ( e = hammer.lastMoveEvent ) : ( hammer.lastMoveEvent = e ); // store the last move event
\r
268 hammerEvent = X_Object_copy( e );
\r
269 hammerEvent.touches = touches;
\r
271 if( isStart && !hammer.startEvent ){
\r
272 console.log( '=- add -=' );
\r
273 // already busy with a Hammer.gesture detection on an element
\r
274 hammer.startEvent = hammerEvent;
\r
275 hammer.uinodeRoot[ 'listen' ]( [ XUI_Event._POINTER_MOVE, XUI_Event._POINTER_UP, XUI_Event._POINTER_CANCEL, XUI_Event.POINTER_OUT ], hammer, XUI_Gesture_handleEvent );
\r
278 startEv = hammer.startEvent;
\r
281 // if the touches change, set the new touches over the startEvent touches
\r
282 // this because touchevents don't have all the touches on touchstart, or the
\r
283 // user must place his fingers at the EXACT same time on the screen, which is not realistic
\r
284 // but, sometimes it happens that both fingers are touching at the EXACT same time
\r
285 if( startEv && ( numTouches !== startEv.touches.length || touches !== startEv.touches ) ){
\r
286 // extend 1 level deep to get the touchlist with the touch objects
\r
287 startEv.touches.length = i = 0;
\r
289 for( ; i < numTouches; ++i ){
\r
290 startEv.touches[ ++j ] = touches[ i ];
\r
294 deltaTime = hammerEvent.timestamp - startEv.timestamp;
\r
295 center = XUI_GestureUtils.getCenter( touches );
\r
296 startCenter = startEv.center;
\r
297 deltaX = startCenter ? ( center.pageX - startCenter.pageX ) : 0;
\r
298 deltaY = startCenter ? ( center.pageY - startCenter.pageY ) : 0;
\r
299 velocity = XUI_GestureUtils.getVelocity( deltaTime, deltaX, deltaY );
\r
301 X_Object_override( hammerEvent, {
\r
302 type : isEnd ? XUI_Event._POINTER_UP : type,
\r
304 deltaTime : deltaTime,
\r
309 velocityX : velocity.x,
\r
310 velocityY : velocity.y,
\r
313 distance : startCenter ? XUI_GestureUtils.getDistance( startCenter, center ) : 0,
\r
314 angle : startCenter ? XUI_GestureUtils.getAngle( startCenter, center ) : 0,
\r
315 direction : startCenter ? XUI_GestureUtils.getDirection( startCenter, center ) : 0,
\r
317 scale : XUI_GestureUtils.getScale( startEv.touches, touches ),
\r
318 rotation : XUI_GestureUtils.getRotation( startEv.touches, touches ),
\r
320 startEvent : startEv
\r
323 // store as previous event event
\r
324 hammer.lastEvent = hammerEvent;
\r
325 activated = hammer.activated;
\r
326 console.log( '... ' );
\r
327 // call Hammer.gesture handlers
\r
328 for( i = 0, l = gestures.length; i < l; ++i ){
\r
329 gesture = gestures[ i ];
\r
331 if( activated[ gesture.name ] && !hammer.canceled[ gesture.name ] ){
\r
332 ( console.log( '... ' + i + ' ' + gesture.name ) );
\r
333 // if a handler returns false, we stop with the detection
\r
334 ( ret |= ( gesture.handler( hammerEvent, hammer ) || X_CALLBACK_NONE ) );
\r
337 if( ret & X_CALLBACK_CAPTURE_POINTER ){
\r
338 for( i = touches.length; i; ){
\r
339 uid = touches[ --i ][ 'pointerId' ];
\r
340 XUI_Gesture_CAPTURED[ uid ] = hammer;
\r
341 //console.log( 'captured. ' + uid );
\r
345 if( ret & X_CALLBACK_STOP_NOW ){
\r
349 //console.log( '----' );
\r
354 if( isEnd || ( ret & X_CALLBACK_RELEASE_POINTER ) ){
\r
355 for( i = touches.length; i; ){
\r
356 uid = touches[ --i ][ 'pointerId' ];
\r
357 if( XUI_Gesture_CAPTURED[ uid ] === hammer ){
\r
358 console.log( 'released. ' + uid );
\r
359 delete XUI_Gesture_CAPTURED[ uid ];
\r
365 console.log( '=- clear -=' );
\r
366 hammer.uinodeRoot[ 'unlisten' ]( [ XUI_Event._POINTER_MOVE, XUI_Event._POINTER_UP, XUI_Event._POINTER_CANCEL, XUI_Event.POINTER_OUT ], hammer, XUI_Gesture_handleEvent );
\r
368 hammer.previous = {
\r
369 currentName : hammer.currentName,
\r
370 startEvent : hammer.startEvent,
\r
371 lastEvent : hammer.lastEvent,
\r
372 lastMoveEvent : hammer.lastMoveEvent
\r
375 X_Object_clear( hammer.triggered );
\r
376 X_Object_clear( hammer.canceled );
\r
378 delete hammer.currentName;
\r
379 delete hammer.startEvent;
\r
380 delete hammer.lastEvent;
\r
381 delete hammer.lastMoveEvent;
\r
383 ret |= X_CALLBACK_RELEASE_POINTER;
\r
391 var XUI_Gesture_LIST = [
\r
394 * Called as first, tells the user has touched the screen
\r
401 // call preventDefault at touchstart, and makes the element blocking by
\r
402 // disabling the scrolling of the page, but it improves gestures like
\r
403 // transforming and dragging.
\r
404 // be careful with using this, it can be very annoying for users to be stuck
\r
406 prevent_default : false,
\r
408 // disable mouse events, so only touch (or pen!) input triggers events
\r
409 prevent_mouseevents : false
\r
411 handler : function( e, hammer ){
\r
412 if( hammer.options.prevent_mouseevents && e[ 'pointerType' ] === 'mouse' ){
\r
413 return X_CALLBACK_STOP_NOW;
\r
416 //hammer.options.prevent_default && e.preventDefault();
\r
418 return e.type === XUI_Event._POINTER_DOWN && hammer.trigger( XUI_Event.TOUCH, e );
\r
424 * User want to scale or rotate with 2 fingers
\r
425 * @events transform, transformstart, transformend, pinch, pinchin, pinchout, rotate
\r
428 name : 'transform',
\r
430 startID : XUI_Event.TRANSFORM,
\r
431 endID : XUI_Event.ROTATE,
\r
433 // factor, no scale is 1, zoomin is to 0 and zoomout until higher then 1
\r
434 transform_min_scale : 0.01,
\r
435 // rotation in degrees
\r
436 transform_min_rotation : 1,
\r
437 // prevent default browser behavior when two touches are on the screen
\r
438 // but it makes the element a blocking element
\r
439 // when you are using the transform gesture, it is a good practice to set this true
\r
440 transform_always_block : false
\r
443 handler : function( e, hammer ){
\r
444 var transform = this, ret = X_CALLBACK_NONE, scale_threshold, rotation_threshold;
\r
446 // current gesture isnt drag, but dragged is true
\r
447 // this means an other gesture is busy. now call dragend
\r
448 if( hammer.currentName !== transform.name && hammer.triggered[ transform.name ] ){
\r
449 ret = hammer.trigger( XUI_Event.TRANSFORM_END, e );
\r
450 delete hammer.triggered[ transform.name ];
\r
454 // atleast multitouch
\r
455 if( e.touches.length < 2 ) return;
\r
457 // prevent default when two fingers are on the screen
\r
458 //hammer.options.transform_always_block && e.preventDefault();
\r
461 case XUI_Event._POINTER_DOWN :
\r
462 //hammer.triggered[ transform.name ] = false;
\r
465 case XUI_Event._POINTER_MOVE:
\r
466 scale_threshold = Math.abs( 1 - e.scale );
\r
467 rotation_threshold = Math.abs( e.rotation );
\r
469 // when the distance we moved is too small we skip this gesture
\r
470 // or we can be already in dragging
\r
471 if( scale_threshold < hammer.options.transform_min_scale && rotation_threshold < hammer.options.transform_min_rotation ) return;
\r
473 // we are transforming!
\r
474 hammer.currentName = transform.name;
\r
476 // first time, trigger dragstart event
\r
477 if( !hammer.triggered[ transform.name ] ){
\r
478 ret = hammer.trigger( XUI_Event.TRANSFORM_START, e );
\r
479 if( ret & X_CALLBACK_PREVENT_DEFAULT ){
\r
480 hammer.canceled[ transform.name ] = true;
\r
483 hammer.triggered[ transform.name ] = true;
\r
487 ret |= hammer.trigger( XUI_Event.TRANSFORM, e );
\r
488 // basic transform event
\r
490 // trigger rotate event
\r
491 if( hammer.options.transform_min_rotation < rotation_threshold ){
\r
492 ret |= hammer.trigger( XUI_Event.ROTATE, e );
\r
495 // trigger pinch event
\r
496 if( scale_threshold > hammer.options.transform_min_scale ){
\r
497 ret |= hammer.trigger( XUI_Event.PINCH, e );
\r
498 ret |= hammer.trigger( e.scale < 1 ? XUI_Event.PINCH_IN : XUI_Event.PINCH_OUT, e );
\r
502 case XUI_Event.POINTER_OUT :
\r
503 case XUI_Event._POINTER_CANCEL :
\r
504 case XUI_Event._POINTER_UP :
\r
506 ret = hammer.triggered[ transform.name ] && hammer.trigger( XUI_Event.TRANSFORM_END, e );
\r
507 hammer.triggered[ transform.name ] = false;
\r
516 * Move with x fingers (default 1) around on the page. Blocking the scrolling when
\r
517 * moving left and right is a good practice. When all the drag events are blocking
\r
518 * you disable scrolling on that area.
\r
519 * @events drag, dragstart, dragend, drapleft, dragright, dragup, dragdown
\r
524 startID : XUI_Event.DRAG,
\r
525 endID : XUI_Event.DRAG_DOWN,
\r
528 drag_min_distance : 10,
\r
529 // set 0 for unlimited, but this can conflict with transform
\r
530 drag_max_touches : 1,
\r
531 // prevent default browser behavior when dragging occurs
\r
532 // be careful with it, it makes the element a blocking element
\r
533 // when you are using the drag gesture, it is a good practice to set this true
\r
534 drag_block_horizontal : false,
\r
535 drag_block_vertical : false,
\r
536 // drag_lock_to_axis keeps the drag gesture on the axis that it started on,
\r
537 // It disallows vertical directions if the initial direction was horizontal, and vice versa.
\r
538 drag_lock_to_axis : false,
\r
539 // drag lock only kicks in when distance > drag_lock_min_distance
\r
540 // This way, locking occurs only when the distance has become large enough to reliably determine the direction
\r
541 drag_lock_min_distance : 25
\r
544 handler : function( e, hammer ){
\r
545 var drag = this, last_direction, ret;
\r
547 // current gesture isnt drag, but dragged is true
\r
548 // this means an other gesture is busy. now call dragend
\r
549 if( hammer.currentName !== drag.name && hammer.triggered[ drag.name ] ){
\r
550 ret = hammer.trigger( XUI_Event.DRAG_END, e );
\r
551 hammer.triggered[ drag.name ] = false;
\r
556 if( 0 < hammer.options.drag_max_touches && hammer.options.drag_max_touches < e.touches.length ) return;
\r
559 case XUI_Event._POINTER_DOWN :
\r
560 hammer.triggered[ drag.name ] = false;
\r
563 case XUI_Event._POINTER_MOVE :
\r
564 // when the distance we moved is too small we skip this gesture
\r
565 // or we can be already in dragging
\r
566 if( e.distance < hammer.options.drag_min_distance && hammer.currentName !== drag.name ) return;
\r
568 // we are dragging!
\r
569 hammer.currentName = drag.name;
\r
571 // lock drag to axis?
\r
572 if( hammer.lastEvent.drag_locked_to_axis || ( hammer.options.drag_lock_to_axis && hammer.options.drag_lock_min_distance <= e.distance ) ){
\r
573 e.drag_locked_to_axis = true;
\r
575 last_direction = hammer.lastEvent.direction;
\r
576 if( e.drag_locked_to_axis && last_direction !== e.direction ){
\r
577 // keep direction on the axis that the drag gesture started on
\r
578 e.direction = XUI_GestureUtils.isVertical( last_direction ) ?
\r
579 ( e.deltaY < 0 ? 'up' : 'down' ) :
\r
580 ( e.deltaX < 0 ? 'left' : 'right' );
\r
583 ret = X_CALLBACK_NONE;
\r
585 // first time, trigger dragstart event
\r
586 if( !hammer.triggered[ drag.name ] ){
\r
587 ret = hammer.trigger( XUI_Event.DRAG_START, e );
\r
588 //if( ret & X_CALLBACK_PREVENT_DEFAULT ){
\r
589 // hammer.canceled[ drag.name ] = true;
\r
592 ret |= X_CALLBACK_CAPTURE_POINTER;
\r
593 console.log( '----- drag start ....' + e.type );
\r
594 hammer.triggered[ drag.name ] = true;
\r
598 //console.log( '----- drag ....' + e.type );
\r
599 // trigger normal event
\r
600 ret = hammer.trigger( XUI_Event.DRAG, e ) | X_CALLBACK_CAPTURE_POINTER;
\r
602 // direction event, like dragdown
\r
603 ret |= hammer.trigger(
\r
604 e.direction === 'up' ?
\r
605 XUI_Event.DRAG_UP :
\r
606 e.direction === 'down' ?
\r
607 XUI_Event.DRAG_DOWN :
\r
608 e.direction === 'left' ?
\r
609 XUI_Event.DRAG_LEFT :
\r
610 XUI_Event.DRAG_RIGHT,
\r
614 // block the browser events
\r
616 ( hammer.options.drag_block_vertical && XUI_GestureUtils.isVertical( e.direction ) ) ||
\r
617 ( hammer.options.drag_block_horizontal && !XUI_GestureUtils.isVertical( e.direction ) )
\r
618 ) && e.preventDefault(); */
\r
621 case XUI_Event.POINTER_OUT :
\r
622 console.log( 'cancel!!' );
\r
623 case XUI_Event._POINTER_CANCEL :
\r
624 case XUI_Event._POINTER_UP:
\r
626 if( hammer.triggered[ drag.name ] ){
\r
627 ret = hammer.trigger( XUI_Event.DRAG_END, e ) | X_CALLBACK_CAPTURE_POINTER;
\r
628 console.log( '----- drag end ....' + e.type );
\r
629 hammer.triggered[ drag.name ] = false;
\r
639 * Quick touch at a place or double at the same place
\r
640 * @events tap, doubletap
\r
645 startID : XUI_Event.TAP,
\r
646 endID : XUI_Event.DOUBLE_TAP,
\r
648 tap_max_touchtime : 250,
\r
649 tap_max_distance : 3,
\r
651 doubletap_distance : 20,
\r
652 doubletap_interval : 300
\r
654 handler : function( e, hammer ){
\r
655 // previous gesture, for the double tap since these are two different gesture detections
\r
656 var prev = hammer.previous;
\r
658 if( e.type === XUI_Event._POINTER_MOVE && hammer.options.tap_max_distance < e.distance ){
\r
659 hammer.canceled[ 'tap' ] = true;
\r
661 if( e.type === XUI_Event._POINTER_UP ){
\r
662 // when the touchtime is higher then the max touch time
\r
663 // or when the moving distance is too much
\r
664 if( hammer.options.tap_max_touchtime < e.deltaTime || hammer.options.tap_max_distance < e.distance ) return;
\r
666 // check if double tap
\r
667 if( prev && prev.currentName === 'tap' && ( e.timestamp - prev.lastEvent.timestamp ) < hammer.options.doubletap_interval && e.distance < hammer.options.doubletap_distance ){
\r
668 return hammer.trigger( XUI_Event.DOUBLE_TAP, e );
\r
671 if( hammer.options.tap_always ){
\r
672 //hammer.currentName = 'tap';
\r
673 console.log( 'tap! ' + e.deltaTime + 'ms ' + e.type );
\r
674 return hammer.trigger( XUI_Event.TAP, e );
\r
684 g = XUI_Gesture_LIST[ --i ];
\r
685 X_Object_override( XUI_Gesture_DEFAULTS, g.defaults );
\r
688 })( XUI_Gesture_LIST.length );
\r