1 // ========================================================================== 2 // Project: SproutCore - JavaScript Application Framework 3 // Copyright: ©2010 Strobe Inc. All rights reserved. 4 // Author: Peter Wagenet 5 // License: Licensed under MIT license (see license.js) 6 // ========================================================================== 7 8 sc_require("system/gesture"); 9 10 /** 11 ## What is a "tap"? 12 13 A tap is a touch that starts and ends in a short amount of time without moving along either axis. 14 A tap may consist of more than one touch, provided that the touches start and end together. The time 15 allowed for touches to start and end together is defined by `touchUnityDelay`. 16 Again, to be considered a tap, there should be very little movement of any touches on either axis 17 while still touching. The amount of movement allowed is defined by `tapWiggle`. 18 19 @class 20 @extends SC.Gesture 21 */ 22 SC.TapGesture = SC.Gesture.extend( 23 /** @scope SC.TapGesture.prototype */{ 24 25 /** @private The time that the first touch started at. */ 26 _sc_firstTouchAddedAt: null, 27 28 /** @private The time that the first touch ended at. */ 29 _sc_firstTouchEndedAt: null, 30 31 /** @private A flag used to track when the touch was long enough to register tapStart (and tapEnd). */ 32 _sc_isTapping: false, 33 34 /** @private The number of touches in the current tap. */ 35 _sc_numberOfTouches: 0, 36 37 /** @private A timer started after the first touch starts. */ 38 _sc_tapStartTimer: null, 39 40 /** 41 @type String 42 @default "tap" 43 @readOnly 44 */ 45 name: "tap", 46 47 /** 48 The amount of time in milliseconds between when the first touch starts and the last touch ends 49 that should be considered a short enough time to constitute a tap. 50 51 @type Number 52 @default 250 53 */ 54 tapLengthDelay: 250, 55 56 /** 57 The amount of time in milliseconds after the first touch starts at which, *if the tap hasn't 58 ended in that time*, the `tapStart` event should trigger. 59 60 Because taps may be very short or because movement of the touch may invalidate a tap gesture 61 entirely, you generally won't want to update the state of the view immediately when a touch 62 starts. 63 64 @type Number 65 @default 150 66 */ 67 tapStartDelay: 150, 68 69 /** 70 The number of pixels that a touch may move before it will no longer be considered a tap. If any 71 of the touches move more than this amount, the gesture will give up. 72 73 @type Number 74 @default 10 75 */ 76 tapWiggle: 10, 77 78 /** 79 The number of milliseconds that touches must start and end together in in order to be considered a 80 tap. If the touches start too far apart in time or end too far apart in time based on this 81 value, the gesture will give up. 82 83 @type Number 84 @default 75 85 */ 86 touchUnityDelay: 75, 87 88 /** @private Calculates the distance a touch has moved. */ 89 _sc_calculateDragDistance: function (touch) { 90 return Math.sqrt(Math.pow(touch.pageX - touch.startX, 2) + Math.pow(touch.pageY - touch.startY, 2)); 91 }, 92 93 /** @private Cleans up the touch session. */ 94 _sc_cleanUpTouchSession: function (wasCancelled) { 95 if (this._sc_isTapping) { 96 // Trigger the gesture, 'tapCancelled'. 97 if (wasCancelled) { 98 this.cancel(); 99 100 // Trigger the gesture, 'tapEnd'. 101 } else { 102 this.end(); 103 } 104 105 this._sc_isTapping = false; 106 } 107 108 // Clean up. 109 this._sc_tapStartTimer.invalidate(); 110 this._sc_numberOfTouches = 0; 111 this._sc_tapStartTimer = this._sc_firstTouchAddedAt = this._sc_firstTouchEndedAt = null; 112 }, 113 114 /** @private Triggers the tapStart event. Should *not* be reachable unless the tap is still valid. */ 115 _sc_triggerTapStart: function () { 116 // Trigger the gesture, 'tapStart'. 117 this.start(); 118 119 this._sc_isTapping = true; 120 }, 121 122 /** 123 The tap gesture only remains interested in a touch session as long as none of the touches have 124 started too long after the first touch (value of `touchUnityDelay`). Once any touch has started 125 too late, the tap gesture gives up for the entire touch session and won't attempt to re-engage 126 (i.e. even if an extra touch "taps" cleanly in the same touch session, it won't trigger any 127 further tap callbacks). 128 129 @param {SC.Touch} touch The touch to be added to the session. 130 @param {Array} touchesInSession The touches already in the session. 131 @returns {Boolean} True as long as the new touch doesn't start too late after the first touch. 132 @see SC.Gesture#touchAddedToSession 133 */ 134 touchAddedToSession: function (touch, touchesInSession) { 135 var stillInterestedInSession, 136 delay; 137 138 // If the new touch came in too late after the first touch was added. 139 delay = Date.now() - this._sc_firstTouchAddedAt; 140 stillInterestedInSession = delay < this.get('touchUnityDelay'); 141 142 return stillInterestedInSession; 143 }, 144 145 /** 146 If a touch cancels, the tap doesn't care and remains interested. 147 148 @param {SC.Touch} touch The touch to be removed from the session. 149 @param {Array} touchesInSession The touches in the session. 150 @returns {Boolean} True 151 @see SC.Gesture#touchCancelledInSession 152 */ 153 touchCancelledInSession: function (touch, touchesInSession) { 154 return true; 155 }, 156 157 /** 158 The tap gesture only remains interested in a touch session as long as none of the touches have 159 ended too long after the first touch ends (value of `touchUnityDelay`). Once any touch has ended 160 too late, the tap gesture gives up for the entire touch session and won't attempt to re-engage 161 (i.e. even if an extra touch "taps" cleanly in the same touch session, it won't trigger any 162 further tap callbacks). 163 164 @param {SC.Touch} touch The touch to be removed from the session. 165 @param {Array} touchesInSession The touches in the session. 166 @returns {Boolean} True if it is the first touch to end or a subsequent touch that ends not too long after the first touch ended. 167 @see SC.Gesture#touchEndedInSession 168 */ 169 touchEndedInSession: function (touch, touchesInSession) { 170 var stillInterestedInSession; 171 172 // Increment the number of touches in the tap. 173 this._sc_numberOfTouches += 1; 174 175 // If it's the first touch to end, remain interested unless tapLengthDelay has passed. 176 if (this._sc_firstTouchEndedAt === null) { 177 this._sc_firstTouchEndedAt = Date.now(); 178 stillInterestedInSession = this._sc_firstTouchEndedAt - this._sc_firstTouchAddedAt < this.get('tapLengthDelay'); 179 180 // If the touch ended too late after the first touch ended, give up entirely. 181 } else { 182 stillInterestedInSession = Date.now() - this._sc_firstTouchEndedAt < this.get('touchUnityDelay'); 183 } 184 185 return stillInterestedInSession; 186 }, 187 188 /** 189 The tap gesture only remains interested in a touch session as long as none of the touches have 190 moved too far (value of `tapWiggle`). Once any touch has moved too far, the tap gesture gives 191 up for the entire touch session and won't attempt to re-engage (i.e. even if an extra touch 192 "taps" cleanly in the same touch session, it won't trigger any further tap callbacks). 193 194 @param {Array} touchesInSession All touches in the session. 195 @returns {Boolean} True as long as none of the touches have moved too far to be a clean tap. 196 @see SC.Gesture#touchesMovedInSession 197 */ 198 touchesMovedInSession: function (touchesInSession) { 199 var stillInterestedInSession = true; 200 201 for (var i = 0, len = touchesInSession.length; i < len; i++) { 202 var touch = touchesInSession[i], 203 movedTooFar = this._sc_calculateDragDistance(touch) > this.get('tapWiggle'); 204 205 // If any touch has gone too far, we don't want to consider any further tap actions for this 206 // session. No need to continue. 207 if (movedTooFar) { 208 stillInterestedInSession = false; 209 break; 210 } 211 } 212 213 return stillInterestedInSession; 214 }, 215 216 /** 217 Cleans up all touch session variables. 218 219 @returns {void} 220 @see SC.Gesture#touchSessionCancelled 221 */ 222 touchSessionCancelled: function () { 223 // Clean up (will fire tapCancelled if _sc_isTapping is true). 224 this._sc_cleanUpTouchSession(true); 225 }, 226 227 /** 228 Cleans up all touch session variables and triggers the gesture. 229 230 @returns {void} 231 @see SC.Gesture#touchSessionEnded 232 */ 233 touchSessionEnded: function () { 234 // Trigger the gesture, 'tap'. 235 this.trigger(this._sc_numberOfTouches); 236 237 // Clean up (will fire tapEnd if _sc_isTapping is true). 238 this._sc_cleanUpTouchSession(false); 239 }, 240 241 /** 242 Registers when the first touch started. 243 244 @param {SC.Touch} touch The touch that started the session. 245 @returns {void} 246 @see SC.Gesture#touchSessionStarted 247 */ 248 touchSessionStarted: function (touch) { 249 // Initialize. 250 this._sc_firstTouchAddedAt = Date.now(); 251 252 this._sc_tapStartTimer = SC.Timer.schedule({ 253 target: this, 254 action: this._sc_triggerTapStart, 255 interval: this.get('tapStartDelay') 256 }); 257 } 258 259 }); 260