1 // ========================================================================== 2 // Project: SproutCore - JavaScript Application Framework 3 // Copyright: ©2006-2011 Strobe Inc. and contributors. 4 // Portions ©2008-2011 Apple Inc. All rights reserved. 5 // License: Licensed under MIT license (see license.js) 6 // ========================================================================== 7 8 sc_require("system/gesture"); 9 10 /* 11 TODO Document this class 12 */ 13 14 /** 15 @class 16 @extends SC.Gesture 17 */ 18 SC.PinchGesture = SC.Gesture.extend( 19 /** @scope SC.PinchGesture.prototype */{ 20 21 /** @private Whether we have started pinching or not. 22 23 @type Boolean 24 @default false 25 */ 26 _sc_isPinching: false, 27 28 /** @private The previous distance between touches. 29 30 @type Number 31 @default null 32 */ 33 _sc_pinchAnchorD: null, 34 35 /** @private The initial scale of the view before pinching. 36 37 @type Number 38 @default null 39 */ 40 _sc_pinchAnchorScale: null, 41 42 /** 43 @type String 44 @default "pinch" 45 */ 46 name: "pinch", 47 48 /** 49 The amount of time in milliseconds that touches should stop moving before a `pinchEnd` event 50 will fire. When a pinch gesture begins, the `pinchStart` event is fired and as long as the 51 touches continue to change distance, multiple `pinch` events will fire. If the touches remain 52 active but don't change distance any longer, then after `pinchDelay` milliseconds the `pinchEnd` 53 event will fire. 54 55 @type Number 56 @default 500 57 */ 58 pinchDelay: 500, 59 60 /** 61 The number of pixels that multiple touches need to expand or contract in order to trigger the 62 beginning of a pinch. 63 64 @type Number 65 @default 3 66 */ 67 // pinchStartThreshold: 3, 68 69 /** @private Cleans up the touch session. */ 70 _sc_cleanUpTouchSession: function () { 71 // If we were pinching before, end the pinch immediately. 72 if (this._sc_isPinching) { 73 this._sc_pinchingTimer.invalidate(); 74 this._sc_pinchingTimer = null; 75 this._sc_lastPinchTime = null; 76 this._sc_isPinching = false; 77 78 // Trigger the gesture, 'pinchEnd'. 79 this.end(); 80 } 81 82 // Clean up. 83 this._sc_pinchAnchorD = null; 84 }, 85 86 /** @private Shared function for when a touch ends or cancels. */ 87 _sc_touchFinishedInSession: function (touch, touchesInSession) { 88 // If there are more than two touches, keep monitoring for pinches by updating _sc_pinchAnchorD. 89 if (touchesInSession.length > 1) { 90 // Get the averaged touches for the the view. Because pinch is always interested in every touch 91 // the touchesInSession will equal the touches for the view. 92 var avgTouch = touch.averagedTouchesForView(this.view); 93 94 this._sc_pinchAnchorD = avgTouch.d; 95 96 // Disregard incoming touches by clearing out _sc_pinchAnchorD and end an active pinch immediately. 97 } else { 98 this._sc_cleanUpTouchSession(); 99 } 100 }, 101 102 /** @private Triggers pinchEnd and resets _sc_isPinching if enough time has passed. */ 103 _sc_triggerPinchEnd: function () { 104 // If a pinch came in since the time the timer was registered, set up a new timer for the 105 // remaining time. 106 if (this._sc_lastPinchTime) { 107 var timePassed = Date.now() - this._sc_lastPinchTime, 108 pinchDelay = this.get('pinchDelay'); 109 110 // Prepare to send 'pinchEnd' again. 111 this._sc_pinchingTimer = SC.Timer.schedule({ 112 target: this, 113 action: this._sc_triggerPinchEnd, 114 interval: pinchDelay - timePassed // Trigger the timer the amount of time left since the last pinch 115 }); 116 117 // Clear out the last pinch time. 118 this._sc_lastPinchTime = null; 119 120 // No additional pinches appeared in the amount of time. 121 } else { 122 // Trigger the gesture, 'pinchEnd'. 123 this.end(); 124 125 // Clear out the pinching session. 126 this._sc_isPinching = false; 127 this._sc_pinchingTimer = null; 128 } 129 }, 130 131 /** 132 The pinch gesture is always interested in the touch session. When a new touch is added, the 133 distance between all of the touches is registered in order to check for distance changes 134 equating to a pinch gesture. 135 136 @param {SC.Touch} touch The touch to be added to the session. 137 @param {Array} touchesInSession The touches already in the session. 138 @returns {Boolean} True. 139 @see SC.Gesture#touchAddedToSession 140 */ 141 touchAddedToSession: function (touch, touchesInSession) { 142 // Get the averaged touches for the the view. Because pinch is always interested in every touch 143 // the touchesInSession will equal the touches for the view. 144 var avgTouch = touch.averagedTouchesForView(this.view, true); 145 146 this._sc_pinchAnchorD = avgTouch.d; 147 148 return true; 149 }, 150 151 /** 152 If a touch cancels, the pinch remains interested (even if there's only one touch left, because a 153 second touch may appear again), but updates its internal variable for tracking for pinch 154 movements. 155 156 @param {SC.Touch} touch The touch to be removed from the session. 157 @param {Array} touchesInSession The touches in the session. 158 @returns {Boolean} True 159 @see SC.Gesture#touchCancelledInSession 160 */ 161 touchCancelledInSession: function (touch, touchesInSession) { 162 this._sc_touchFinishedInSession(touch, touchesInSession); 163 164 return true; 165 }, 166 167 /** 168 If a touch ends, the pinch remains interested (even if there's only one touch left, because a 169 second touch may appear again), but updates its internal variable for tracking for pinch 170 movements. 171 172 @param {SC.Touch} touch The touch to be removed from the session. 173 @param {Array} touchesInSession The touches in the session. 174 @returns {Boolean} True 175 @see SC.Gesture#touchEndedInSession 176 */ 177 touchEndedInSession: function (touch, touchesInSession) { 178 this._sc_touchFinishedInSession(touch, touchesInSession); 179 180 return true; 181 }, 182 183 /** 184 The pinch is only interested in more than one touch moving. If there are multiple touches 185 moving and the distance between the touches has changed then a `pinchStart` event will fire. 186 If the touches keep expanding or contracting, the `pinch` event will repeatedly fire. Finally, 187 if the touch distance stops changing and enough time passes (value of `pinchDelay`), the 188 `pinchEnd` event will fire. 189 190 Therefore, it's possible for a pinch gesture to start and end more than once in a single touch 191 session. For example, a person may touch two fingers down, expand them to zoom in (`pinchStart` 192 and multiple `pinch` events fire) and then if they stop or move their fingers in one direction 193 in tandem to scroll content (`pinchEnd` event fires after `pinchDelay` exceeded). If the person 194 then starts expanding their fingers again without lifting them, a new set of pinch events will 195 fire. 196 197 @param {Array} touchesInSession All touches in the session. 198 @returns {Boolean} True. 199 @see SC.Gesture#touchesMovedInSession 200 */ 201 touchesMovedInSession: function (touchesInSession) { 202 // console.log('touchesMovedInSession: %@'.fmt(touchesInSession.length)); 203 // We should pay attention to the movement. 204 if (touchesInSession.length > 1) { 205 // Get the averaged touches for the the view. Because pinch is always interested in every touch 206 // the touchesInSession will equal the touches for the view. 207 var avgTouch = SC.Touch.averagedTouch(touchesInSession); // touchesInSession[0].averagedTouchesForView(this.view); 208 209 var touchDeltaD = this._sc_pinchAnchorD - avgTouch.d, 210 absDeltaD = Math.abs(touchDeltaD); 211 212 // console.log(' this._sc_pinchAnchorD, %@ - avgTouch.d, %@ = touchDeltaD, %@'.fmt(this._sc_pinchAnchorD, avgTouch.d, touchDeltaD)); 213 if (absDeltaD > 0) { 214 // Trigger the gesture, 'pinchStart', once. 215 if (!this._sc_isPinching) { 216 this.start(); 217 218 // Prepare to send 'pinchEnd'. 219 this._sc_pinchingTimer = SC.Timer.schedule({ 220 target: this, 221 action: this._sc_triggerPinchEnd, 222 interval: this.get('pinchDelay') 223 }); 224 225 // Track that we are pinching. 226 this._sc_isPinching = true; 227 228 // Update the last pinch time so that when the timer expires, it doesn't fire pinchEnd. 229 // This is faster than invalidating and creating a new timer each time this method is called. 230 } else { 231 this._sc_lastPinchTime = Date.now(); 232 } 233 234 // The percentage difference in touch distance. 235 var scalePercentChange = avgTouch.d / this._sc_pinchAnchorD, 236 scale = this._sc_pinchAnchorScale * scalePercentChange; 237 238 // Trigger the gesture, 'pinch'. 239 this.trigger(scale, touchesInSession.length); 240 241 // Reset the anchor. 242 this._sc_pinchAnchorD = avgTouch.d; 243 this._sc_pinchAnchorScale = scale; 244 } 245 } 246 247 return true; 248 }, 249 250 /** 251 Cleans up all touch session variables. 252 253 @returns {void} 254 @see SC.Gesture#touchSessionCancelled 255 */ 256 touchSessionCancelled: function () { 257 // Clean up. 258 this._sc_cleanUpTouchSession(); 259 }, 260 261 /** 262 Cleans up all touch session variables and triggers the gesture. 263 264 @returns {void} 265 @see SC.Gesture#touchSessionEnded 266 */ 267 touchSessionEnded: function () { 268 // Clean up. 269 this._sc_cleanUpTouchSession(); 270 }, 271 272 /** 273 Registers the scale of the view when it starts. 274 275 @param {SC.Touch} touch The touch that started the session. 276 @returns {void} 277 @see SC.Gesture#touchSessionStarted 278 */ 279 touchSessionStarted: function (touch) { 280 var viewLayout = this.view.get('layout'); 281 282 /*jshint eqnull:true*/ 283 this._sc_pinchAnchorScale = viewLayout.scale == null ? 1 : viewLayout.scale; 284 } 285 286 }); 287