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 /** 9 @namespace 10 11 You can mix in SC.Gesturable to your views to add some support for recognizing gestures. 12 13 SproutCore views have built-in touch events. However, sometimes you may want 14 to recognize gestures like tap, pinch, swipe, etc. This becomes tedious if you 15 need to do this often, and more so if you need to check for multiple possible 16 gestures on the same view. 17 18 SC.Gesturable allows you to define a collection of gestures (SC.Gesture objects) 19 that your view should recognize. When a gesture is recognized, methods will be 20 called on the view: 21 22 - [gestureName](gesture, args...): called when the gesture has occurred. This is 23 useful for event-style gestures, where you aren't interested in when it starts or 24 ends, but just that it has occurred. SC.SwipeGesture triggers this after the 25 swipe has moved a minimum amount—40px by default. 26 - [gestureName]Start(gesture, args...): called when the gesture is first recognized. 27 For instance, a swipe gesture may be recognized after the finger has moved a 28 minimum distance in a horizontal. 29 - [gestureName]Changed(gesture, args...): called when some property of the gesture 30 has changed. For instance, this may be called continuously as the user swipes as 31 the swipe's distance changes. 32 - [gestureName]Cancelled(gesture, args...): called when a gesture, for one reason 33 or another, is no longer recognized. For instance, a horizontal swipe gesture 34 could cancel if the user moves too far in a vertical direction. 35 - [gestureName]End(gesture, args...): called when a gesture ends. A swipe would end 36 when the user lifts their finger. 37 38 Each of these methods is passed the gesture instance, in addition to any arguments 39 the gesture sends for your convenience. The default swipe gesture sends an SC.Touch 40 instance, the swipe direction, and the distance the swipe has moved in that direction. 41 42 Using SC.Gesturable 43 ------------------- 44 45 To make your view recognize gestures, mix in Gesturable and add items to the 'gestures' 46 property: 47 48 SC.View.extend(SC.Gesturable, { 49 gestures: [SC.PinchGesture, 'mySwipeGesture'], 50 51 // specifying as a string allows you to configure it: 52 mySwipeGesture: SC.SwipeGesture.extend({ 53 direction: SC.SWIPE_VERTICAL, 54 startDistance: 3, 55 swipeDistance: 20 56 }), 57 58 // handle the swipe action 59 swipe: function(touch, direction) { 60 console.error("Swiped! In direction: " + direction); 61 }, 62 63 swipeStart: function(touch, direction, delta) { 64 console.error("Swipe started in direction: " + direction + "; dist: " + delta); 65 }, 66 67 swipeChanged: function(touch, direction, delta) { 68 console.error("Swipe continued in direction: " + direction + "; dist: " + delta); 69 }, 70 71 swipeEnd: function(touch, direction, delta) { 72 console.error("Completed swipe in direction: " + direction + "; dist: " + delta); 73 } 74 75 }) 76 77 @extends SC.ObjectMixinProtocol 78 @extends SC.ResponderProtocol 79 */ 80 SC.Gesturable = { 81 82 /** @private An array of all gestures currently interested in the touch session. 83 84 @type Array 85 @default null 86 */ 87 _sc_interestedGestures: null, 88 89 /** @private An array of the touches that are currently active in a touch session. 90 91 @type Array 92 @default null 93 */ 94 _sc_touchesInSession: null, 95 96 /** 97 Gestures need to understand multiple touches. 98 99 @type Boolean 100 @default true 101 @see SC.View#acceptsMultitouch 102 */ 103 acceptsMultitouch: true, 104 105 /** 106 @type Array 107 @default ['gestures'] 108 @see SC.Object#concatenatedProperties 109 */ 110 concatenatedProperties: ['gestures'], 111 112 /** 113 The gestures that the view will support. This property must be set on the consumer of 114 `SC.Gesturable` before it is initialized. 115 116 These gestures should be objects that extend the `SC.Gesture` class. You can use SproutCore's 117 pre-built gestures or create your own. If you create your own, you can use a property name 118 in the list of gestures to refer to the actual gesture class, similar to how the childViews 119 array works. For example, 120 121 gestures: [SC.PinchGesture, 'mySwipeGesture'], 122 123 // Specifying the Gesture by property name allows you to configure it. 124 mySwipeGesture: SC.SwipeGesture.extend({ 125 direction: SC.SWIPE_VERTICAL, 126 startDistance: 3, 127 swipeDistance: 20 128 }), 129 130 Note that `gestures` is a *concatenated property*, which means that it will not be overwritten 131 by subclasses. So for example, if the base class lists gestures as `[SC.PinchGesture]` and its 132 subclass lists gestures as `[SC.TapGesture]`, the actual gestures supported by the subclass will 133 be `[SC.PinchGesture, SC.TapGesture]`. 134 135 @type Array 136 @default null 137 */ 138 gestures: null, 139 140 /** @private Shared method for finishing a touch. 141 142 @param {SC.Touch} touch The touch that ended or cancelled. 143 @param {Boolean} wasCancelled Whether the touch was cancelled or not (i.e. ended normally). 144 */ 145 _sc_gestureTouchFinish: function (touch, wasCancelled) { 146 var touchesInSession = this._sc_touchesInSession, 147 touchIndexInSession = touchesInSession.indexOf(touch); 148 149 // Decrement our list of touches that are being acted upon. 150 touchesInSession.replace(touchIndexInSession, 1); 151 152 var gestures = this._sc_interestedGestures, 153 idx, 154 gesture; 155 156 // Loop through the gestures in reverse, as the list may be mutated. 157 for (idx = gestures.length - 1; idx >= 0; idx--) { 158 var isInterested; 159 160 gesture = gestures[idx]; 161 162 if (wasCancelled) { 163 isInterested = gesture.touchCancelledInSession(touch, touchesInSession); 164 } else { 165 isInterested = gesture.touchEndedInSession(touch, touchesInSession); 166 } 167 168 // If the gesture is no longer interested in *any* touches for this session, remove it. 169 if (!isInterested) { 170 // Tell the gesture that the touch session has ended for it. 171 gesture.touchSessionCancelled(); 172 173 gestures.replace(idx, 1); 174 } 175 } 176 177 // Once there are no more touches in the session, reset the interested gestures. 178 if (touchesInSession.length === 0) { 179 // Notify all remaining interested gestures that the touch session has finished cleanly. 180 var len; 181 182 for (idx = 0, len = gestures.length; idx < len; idx++) { 183 gesture = gestures[idx]; 184 185 gesture.touchSessionEnded(); 186 } 187 188 // Clear out the current cache of interested gestures for the session. 189 this._sc_interestedGestures.length = 0; 190 } 191 }, 192 193 /** 194 When SC.Gesturable initializes, any gestures named on the view are instantiated. 195 196 @see SC.ObjectMixinProtocol#initMixin 197 */ 198 initMixin: function() { 199 //@if(debug) 200 if (SC.none(this.gestures)) { 201 SC.error("Developer Error: When mixing in SC.Gesturable, you must define a list of gestures to use."); 202 } 203 //@endif 204 this.createGestures(); 205 }, 206 207 /** @private Instantiates the gestures. */ 208 createGestures: function() { 209 var gestures = this.get("gestures"), 210 len = gestures.length, 211 instantiatedGestures = [], 212 idx; 213 214 // loop through all gestures 215 for (idx = 0; idx < len; idx++) { 216 var gesture; 217 218 // get the proper gesture 219 if (SC.typeOf(gestures[idx]) === SC.T_STRING) { 220 gesture = this.get(gestures[idx]); 221 } else { 222 gesture = gestures[idx]; 223 } 224 225 // if it was not found, well, that's an error. 226 if (!gesture) { 227 throw new Error("Developer Error: Could not find gesture named '" + gestures[idx] + "' on view."); 228 } 229 230 // if it is a class, instantiate (it really ought to be a class...) 231 if (gesture.isClass) { 232 gesture = gesture.create({ 233 view: this 234 }); 235 } 236 237 // and set the gesture instance and add it to the array. 238 if (SC.typeOf(gestures[idx]) === SC.T_STRING) this[gestures[idx]] = gesture; 239 instantiatedGestures.push(gesture); 240 } 241 242 this.set("gestures", instantiatedGestures); 243 }, 244 245 /** 246 Handles touch start by handing it to the gesture recognizing code. 247 248 If you override touchStart, you will need to call gestureTouchStart to 249 give the gesture system control of the touch. You will continue to get 250 events until if and when a gesture decides to take "possession" of a touch— 251 at this point, you will get a [gestureName]Start event. 252 253 You do not have to call gestureTouchStart immediately; you can call it 254 at any time. This allows you to avoid passing control until _after_ you 255 have determined your own touchStart, touchesDragged, and touchEnd methods 256 are not going to handle it. 257 258 @param {SC.Touch} touch The touch that started. 259 @returns {Boolean} Whether the touch should be claimed by the view or not. 260 @see SC.ResponderProtocol#touchStart 261 */ 262 touchStart: function(touch) { 263 return this.gestureTouchStart(touch); 264 }, 265 266 /** 267 Tells the gesture recognizing code about touches moving. 268 269 If you override touchesDragged, you will need to call gestureTouchesDragged 270 (at least for any touches you called gestureTouchStart for in touchStart) to 271 allow the gesture system to update. 272 273 @see SC.ResponderProtocol#touchesDragged 274 */ 275 touchesDragged: function(evt, touches) { 276 this.gestureTouchesDragged(evt, touches); 277 }, 278 279 /** 280 Tells the gesture recognizing code about a touch ending. 281 282 If you override touchEnd, you will need to call gestureTouchEnd 283 for any touches you called gestureTouchStart for in touchStart (if overridden). 284 285 @param {SC.Touch} touch The touch that ended. 286 @see SC.ResponderProtocol#touchEnd 287 */ 288 touchEnd: function(touch) { 289 this.gestureTouchEnd(touch); 290 }, 291 292 /** 293 Tells the gesture recognizing code about a touch cancelling. 294 295 If you override touchCancelled, you will need to call gestureTouchCancelled 296 for any touches you called gestureTouchStart for in touchStart (if overridden). 297 298 @param {SC.Touch} touch The touch that cancelled. 299 @see SC.ResponderProtocol#touchCancelled 300 */ 301 touchCancelled: function (touch) { 302 this.gestureTouchCancelled(touch); 303 }, 304 305 /** 306 Called by a gesture that has lost interest in the entire touch session, likely due to too much 307 time having passed since `gestureTouchStart` or `gestureTouchesDragged` having been called. 308 309 Simply removes the gesture from the list of interested gestures and calls 310 `touchSessionCancelled` on the gesture. 311 */ 312 gestureLostInterest: function (gesture) { 313 var gestures = this._sc_interestedGestures, 314 gestureIndex = gestures.indexOf(gesture); 315 316 // Remove the gesture. 317 if (gestureIndex >= 0) { 318 gesture.touchSessionCancelled(); 319 320 gestures.replace(gestureIndex, 1); 321 } 322 }, 323 324 /** 325 Tells the gesture recognizing system about a new touch. This notifies all gestures of a new 326 touch session starting (if there were no previous touches) or notifies all interested gestures 327 that a touch has been added. 328 329 As touches are added beyond the first touch, gestures may "lose interest" in the touch session. 330 For example, a gesture may explicitly want only a single touch and if a second touch appears, 331 the gesture may not want any further updates on this touch session (even if the second touch 332 ends again). 333 334 @param {SC.Touch} touch The touch that started. 335 @returns {Boolean} Whether any gesture is interested in the touch or not. 336 */ 337 gestureTouchStart: function (touch) { 338 var interestedGestures = this._sc_interestedGestures, 339 touchesInSession = this._sc_touchesInSession, 340 claimedTouch = false, 341 idx; 342 343 // Instantiate once. 344 if (touchesInSession === null) { 345 touchesInSession = this._sc_touchesInSession = []; 346 interestedGestures = this._sc_interestedGestures = []; 347 } 348 349 // When there are no touches in the session, check all gestures. 350 if (touchesInSession.length === 0) { 351 var gestures = this.get("gestures"), 352 len; 353 354 for (idx = 0, len = gestures.length; idx < len; idx++) { 355 var gesture = gestures[idx]; 356 357 gesture.touchSessionStarted(touch); 358 interestedGestures.push(gesture); 359 } 360 361 // Keep this touch. 362 claimedTouch = true; 363 364 // Only check gestures that are interested. 365 } else { 366 // Loop through the gestures in reverse, as the list may be mutated. 367 for (idx = interestedGestures.length - 1; idx >= 0; idx--) { 368 var interestedGesture = interestedGestures[idx], 369 isInterested; 370 371 // Keep only the gestures still interested in the touch. 372 isInterested = interestedGesture.touchAddedToSession(touch, touchesInSession); 373 374 if (isInterested) { 375 // Keep this touch. 376 claimedTouch = true; 377 } else { 378 // Tell the gesture that the touch session has ended for it. 379 interestedGesture.touchSessionCancelled(); 380 381 interestedGestures.replace(idx, 1); 382 } 383 } 384 } 385 386 // If any gesture is interested in the new touch. Add it to the list of touches in the session. 387 if (claimedTouch) { 388 touchesInSession.push(touch); 389 } 390 391 return claimedTouch; 392 }, 393 394 /** 395 Tells the gesture recognition system that touches have moved. 396 397 @param {SC.Event} evt The touch event. 398 @param {Array} touches The touches previously claimed by this view. 399 @returns {void} 400 */ 401 gestureTouchesDragged: function (evt, touches) { 402 var gestures = this._sc_interestedGestures, 403 touchesInSession = this._sc_touchesInSession; 404 405 // Loop through the gestures in reverse, as the list may be mutated. 406 for (var i = gestures.length - 1; i >= 0; i--) { 407 var gesture = gestures[i], 408 isInterested = gesture.touchesMovedInSession(touchesInSession); 409 410 // If the gesture is no longer interested in *any* touches for this session, remove it. 411 if (!isInterested) { 412 // Tell the gesture that the touch session has ended for it. 413 gesture.touchSessionCancelled(); 414 415 gestures.replace(i, 1); 416 417 // TODO: When there are no more interested gestures? Do what with the touches? Anything? 418 } 419 } 420 }, 421 422 /** 423 Tells the gesture recognition system that an unassigned touch has ended. 424 425 This informs all of the gestures that the touch ended. The touch is 426 an unassigned touch as, if it were assigned to a gesture, it would have 427 been sent directly to the gesture, bypassing this view. 428 */ 429 gestureTouchEnd: function(touch) { 430 this._sc_gestureTouchFinish(touch, false); 431 }, 432 433 /** 434 Tells the gesture recognition system that an unassigned touch has cancelled. 435 436 This informs all of the gestures that the touch cancelled. The touch is 437 an unassigned touch as, if it were assigned to a gesture, it would have 438 been sent directly to the gesture, bypassing this view. 439 */ 440 gestureTouchCancelled: function(touch) { 441 this._sc_gestureTouchFinish(touch, true); 442 } 443 }; 444