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 /** 10 @class 11 12 An SC.Gesture analyzes SC.Touch objects and determines if they are part 13 of a gesture. If they are, SC.Gestures keep the views that own them up-to-date 14 as that gesture progresses, informing it when it starts, when some aspect of 15 it changes, when it ends, and—for convenience—when it is considered to have 16 been "triggered". 17 18 Gestures can call the following methods on their views: 19 20 - [gestureName](args...): called when the gesture has occurred. This is 21 useful for event-style gestures, where you aren't interested in when it starts or 22 ends, but just that it has occurred. SC.SwipeGesture triggers this after the 23 swipe has moved a minimum amount—40px by default. 24 25 - [gestureName]Start(args...): called when the gesture is first recognized. 26 For instance, a swipe gesture may be recognized after the finger has moved a 27 minimum distance in a horizontal. 28 29 - [gestureName]Changed(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 33 - [gestureName]Cancelled(args...): called when a gesture, for one reason 34 or another, is no longer recognized. For instance, a horizontal swipe gesture 35 could cancel if the user moves too far in a vertical direction. 36 37 - [gestureName]End(args...): called when a gesture ends. A swipe would end 38 when the user lifts their finger. 39 40 Gesture Lifecycle 41 ------------------------ 42 Gestures start receiving events when their view—usually mixing in SC.Gesturable—tells it 43 about activities with "unassigned" touches. "Unassigned" touches are touches that have 44 not _yet_ been assigned to a gesture. 45 46 The touch becomes "assigned" when the gesture's touchIsInGesture method returns YES. 47 When a touch is assigned to a gesture, the gesture becomes the touch's touch responder; 48 this means that it will receive a touchStart event (to which it must return YES), and 49 then, all further touch events will be sent _directly_ to the gesture—the gesture's view 50 will not receive them at all. 51 52 At any point, the gesture may tell the view that it has started, ended, or changed. In 53 addition, the gesture may tell the view it has been "triggered." A gesture is not 54 necessarily "triggered" when it starts and ends; for instance, a swipe gesture might 55 only be triggered if the swipe moves more than a specified amount. The ability to track 56 when the gesture has been triggered allows views to easily handle the gesture as its own 57 event, rather than as the individual events that are part of it. 58 59 If, at some point, the gesture must release the touch back (perhaps the gesture had _thought_ 60 the touch was a part of it, but turned out to be incorrect), the release(touch) method releases 61 it back to the view. 62 63 Exclusivity 64 --------------------------------- 65 The concept described above gives the gestures a way to be either exclusive or inclusive as-needed: 66 they can choose to take exclusive control of a touch if they think it is theirs, but if they are 67 not sure, they can wait and see. 68 69 Status Object 70 --------------------------------- 71 It is a common need to track some data related to the touch, but without modifying the touch itself. 72 SC.Gesture is able to keep track of simple hashes for you, mapping them to the SC.Touch object, 73 so that you can maintain some state related to the touch. 74 75 For instance, you could set status.failed in touchesDragged, if a touch that you previously 76 thought may have been part of the gesture turned out not to be, and then check for 77 status.failed in touchIsInGesture, returning NO if present. This would cause the touch 78 to never be considered for your gesture again. 79 80 touchIsInGesture is called with the status hash provided in the second argument. You may look 81 up the status hash for a touch at any time by calling this.statusForTouch(touch). 82 83 84 Implementing a Gesture 85 --------------------------------- 86 To write a gesture, you would generally implement the following methods: 87 88 - touchIsInGesture: Return YES when the touch is—or is likely enough to be that you 89 want your gesture to have exclusive control over the touch. You usually do not 90 perform much gesture logic here—instead, you save it for touchStart, which will 91 get called after you return YES from this method. 92 93 - touchStart: Return YES to accept control of the touch. If you do not return YES, 94 your gesture will not receive touchesDragged nor touchEnd events. At this point, 95 you may (or may not) wish to tell the view that the gesture has started by using the 96 start(args...) method. 97 98 - touchesDragged: Use this as you would use it in an SC.View to track the touches 99 assigned to the gesture. At this point, you might want to tell the view that the 100 gesture has updated by using the change(args...) method. 101 102 - touchEnd: Again, use this like you would in an SC.View to track when touches 103 assigned to the gesture have ended. This is also a potential time to alert the view 104 that the gesture has ended, by using the end(args...) method. Further, this may 105 also be the time to "trigger" the gesture. 106 107 */ 108 SC.Gesture = SC.Object.extend({ 109 110 /** @private Tracks when the gesture is active or not. */ 111 _sc_isActive: false, 112 113 /** 114 Whether to receive touch events for each distinct touch (rather than only the first touch start 115 and last touch end). 116 117 @type Boolean 118 @default false 119 @see SC.View#acceptsMultitouch 120 */ 121 acceptsMultitouch: false, 122 123 /** 124 The gesture's name. When calling events on the owning SC.View, this name will 125 be prefixed to the methods. For instance, if the method to be called is 126 'Start', and the gesture's name is 'swipe', SC.Gesture will call 'swipeStart'. 127 128 @type String 129 @default "gesture" 130 */ 131 name: "gesture", 132 133 /** 134 Return YES to take exclusive control over the touch. In addition to the 135 SC.Touch object you may take control of, you are also provided a "status" 136 hash, which is unique for both the gesture instance and the touch instance, 137 which you may use for your own purposes. 138 139 @param {SC.Touch} touch The touch. 140 @param {Object} status A unique status hash for the given touch. 141 @returns {Boolean} true if the gesture should claim the touch; false to leave it unclaimed. 142 */ 143 touchIsInGesture: function(touch, status) { 144 return NO; 145 }, 146 147 /** 148 After you return YES from touchIsInGesture (or otherwise 'take' a touch, perhaps 149 using the 'take' method), touchStart will be called. 150 151 This is where you do any logic needed now that the touch is part of the gesture. 152 For instance, you could inform the view that the gesture has started by calling 153 this.start(). 154 155 NOTE: SC.Gesture is just like SC.View in that it has an acceptsMultitouch property. 156 If NO (the default), the gesture will only receive touchStart for the first touch 157 assigned to it, and only receive touchEnd for the last touch that ends. 158 159 @param {SC.Touch} touch The touch that started. 160 @returns {Boolean} true if the gesture should respond to the touch; false otherwise (this should always return true) 161 @see SC.ResponderProtocol#touchStart 162 */ 163 touchStart: function(touch) { 164 return true; 165 }, 166 167 /** 168 Called when a touch assigned to the gesture ends. 169 170 If there are no remaining touches on the gesture, you may want to call end() to 171 notify the view that the gesture has ended (if you haven't ended the gesture 172 already). 173 174 NOTE: SC.Gesture is just like SC.View in that it has an acceptsMultitouch property. 175 If NO (the default), the gesture will only receive touchStart for the first touch 176 assigned to it, and only receive touchEnd for the last touch that ends. 177 178 @name touchEnd 179 @function 180 @param {SC.Touch} touch The touch that ended. 181 */ 182 183 /** 184 Starts the gesture (marking it as "active"), and notifies the view. 185 186 You can pass any number of arguments to start. They will, along with 187 the gesture instance itself, will be passed to the appropriate gesture 188 event on the SC.View. 189 */ 190 start: function() { 191 if (!this._sc_isActive) { 192 this._sc_isActive = true; 193 194 // var argumentsLength = arguments.length, 195 // args = new Array(argumentsLength + 1); 196 197 // // Unshift this to the front of arguments. 198 // args[0] = this; 199 // for (var i = 0, len = argumentsLength; i < len; i++) { args[i + 1] = arguments[i]; } 200 201 // var act = this.name + "Start"; 202 // if (this.view[act]) this.view[act].apply(this.view, args); 203 204 // Fast arguments access. Don't materialize the `arguments` object, it is costly. 205 var argumentsLength = arguments.length, 206 args = new Array(argumentsLength); 207 208 for (var i = 0, len = argumentsLength; i < len; i++) { args[i] = arguments[i]; } 209 210 var act = this.name + "Start"; 211 if (this.view[act]) this.view[act].apply(this.view, args); 212 } 213 }, 214 215 /** 216 Ends the gesture, if it is active (marking it as not active), and notifies 217 the view. 218 219 You may pass any number of arguments to end(). They, along with your gesture 220 instance itself, will be passed to the appropriate gesture event on the SC.View. 221 */ 222 end: function() { 223 if (this._sc_isActive) { 224 this._sc_isActive = false; 225 226 // var argumentsLength = arguments.length, 227 // args = new Array(argumentsLength + 1); 228 229 // // Unshift this to the front of arguments. 230 // args[0] = this; 231 // for (var i = 0, len = argumentsLength; i < len; i++) { args[i + 1] = arguments[i]; } 232 233 // var act = this.name + "End"; 234 // if (this.view[act]) this.view[act].apply(this.view, args); 235 236 // Fast arguments access. Don't materialize the `arguments` object, it is costly. 237 var argumentsLength = arguments.length, 238 args = new Array(argumentsLength); 239 240 for (var i = 0, len = argumentsLength; i < len; i++) { args[i] = arguments[i]; } 241 242 var act = this.name + "End"; 243 if (this.view[act]) this.view[act].apply(this.view, args); 244 } 245 }, 246 247 /** 248 If the gesture is active, notifies the view that the gesture has 249 changed. 250 251 The gesture, along with any arguments to change(), will be passed to 252 the appropriate method on the SC.View. 253 */ 254 change: function() { 255 if (this._sc_isActive) { 256 // var argumentsLength = arguments.length, 257 // args = new Array(argumentsLength + 1); 258 259 // // Unshift this to the front of arguments. 260 // args[0] = this; 261 // for (var i = 0, len = argumentsLength; i < len; i++) { args[i + 1] = arguments[i]; } 262 263 // var act = this.name + "Changed"; 264 // if (this.view[act]) this.view[act].apply(this.view, args); 265 266 // Fast arguments access. Don't materialize the `arguments` object, it is costly. 267 var argumentsLength = arguments.length, 268 args = new Array(argumentsLength); 269 270 for (var i = 0, len = argumentsLength; i < len; i++) { args[i] = arguments[i]; } 271 272 var act = this.name + "Changed"; 273 if (this.view[act]) this.view[act].apply(this.view, args); 274 } 275 }, 276 277 /** 278 Cancels the gesture, if it is active, and notifies the view that the 279 gesture has been cancelled. 280 281 Gestures are cancelled when they have ended, but any action that would 282 normally be appropriate due to their ending should not be performed. 283 284 The gesture, along with any arguments to cancel(), will be passed to the 285 appropriate method on the SC.View. 286 */ 287 cancel: function(){ 288 if (this._sc_isActive) { 289 this._sc_isActive = false; 290 291 // var argumentsLength = arguments.length, 292 // args = new Array(argumentsLength + 1); 293 294 // // Unshift this to the front of arguments. 295 // args[0] = this; 296 // for (var i = 0, len = argumentsLength; i < len; i++) { args[i + 1] = arguments[i]; } 297 298 // var act = this.name + "Cancelled"; 299 // if (this.view[act]) this.view[act].apply(this.view, args); 300 301 // Fast arguments access. Don't materialize the `arguments` object, it is costly. 302 var argumentsLength = arguments.length, 303 args = new Array(argumentsLength); 304 305 for (var i = 0, len = argumentsLength; i < len; i++) { args[i] = arguments[i]; } 306 307 var act = this.name + "Cancelled"; 308 if (this.view[act]) this.view[act].apply(this.view, args); 309 } 310 }, 311 312 /** 313 Triggers the gesture, notifying the view that the gesture has happened. 314 315 You should trigger a gesture where it would be natural to say it has "happened"; 316 for instance, if a touch moves a couple of pixels, you probably wouldn't say 317 a swipe has occurred—though you might say it has "begun." And you wouldn't necessarily 318 wait until the touch has ended either. Once the touch has moved a certain amount, 319 there has definitely been a swipe. By calling trigger() at this point, you will 320 tell the view that it has occurred. 321 322 For SC.SwipeGesture, this allows a view to implement only swipe(), and then be 323 automatically notified whenever any swipe has occurred. 324 */ 325 trigger: function() { 326 // Fast arguments access. Don't materialize the `arguments` object, it is costly. 327 var argumentsLength = arguments.length, 328 // args = new Array(argumentsLength + 1); 329 args = new Array(argumentsLength); 330 331 // // Unshift this to the front of arguments. 332 // args[0] = this; 333 // for (var i = 0, len = argumentsLength; i < len; i++) { args[i + 1] = arguments[i]; } 334 335 for (var i = 0, len = argumentsLength; i < len; i++) { args[i] = arguments[i]; } 336 337 var act = this.name; 338 if (this.view[act]) this.view[act].apply(this.view, args); 339 }, 340 341 /** 342 Takes possession of a touch. 343 344 This is called automatically when you return YES from touchIsInGesture. 345 */ 346 // take: function(touch) { 347 // if (!touch.isTaken) { 348 // touch.isTaken = YES; // because even changing responder won't prevent it from being used this cycle. 349 // if (SC.none(touch.touchResponder) || touch.touchResponder !== this) touch.makeTouchResponder(this, YES); 350 // } 351 // //@if(debug) 352 // else { 353 // SC.warn("Developer Warning: A gesture tried to take a touch that was already taken: %@".fmt(this)); 354 // } 355 // //@endif 356 // }, 357 358 /** 359 Releases a touch back to its previous owner, which is usually the view. This allows 360 you to give back control of a touch that it turns out is not part of the gesture. 361 362 This takes effect immediately, because you would usually call this from 363 touchesDragged or such. 364 */ 365 // release: function(touch) { 366 // if (touch.isTaken) { 367 // touch.isTaken = NO; 368 // if (touch.nextTouchResponder) touch.makeTouchResponder(touch.nextTouchResponder); 369 // } 370 // //@if(debug) 371 // else { 372 // SC.warn("Developer Warning: A gesture tried to release a touch that was not taken: %@".fmt(this)); 373 // } 374 // //@endif 375 // }, 376 377 /** 378 Discards a touch, making its responder null. This makes the touch go away and never 379 come back—not to this gesture, nor to any other, nor to the view, nor to any other 380 view. 381 */ 382 // discardTouch: function(touch) { 383 // touch.isTaken = YES; // because even changing responder won't prevent it from being used this cycle. 384 // touch.makeTouchResponder(null); 385 // }, 386 387 /** 388 Called by the view when a touch session has begun. 389 390 You should override this method in your custom SC.Gesturable subclasses to set up any touch 391 session state. For example, you may want to track the initial touch start time in order to 392 decide how to react when or if additional touches start later. 393 394 @param {SC.Touch} touch The touch that started the session. 395 @returns {void} 396 */ 397 touchSessionStarted: function (touch) { 398 }, 399 400 /** 401 Called by the view when the touch session has ended. 402 403 This will occur because all touches in the session have finished. 404 405 You should override this method in your custom SC.Gesturable subclasses to clean up any state 406 variables used in the touch session. 407 408 @returns {void} 409 */ 410 touchSessionEnded: function () { 411 }, 412 413 /** 414 Called by the view when the touch session was cancelled. 415 416 This will occur because this gesture returned `false` in any of `touchAddedToSession`, 417 `touchesMovedInSession`, `touchEndedInSession`, `touchCancelledInSession` to indicate that the 418 gesture is no longer interested in the session or because another gesture claimed the touch 419 session for itself, forcing all other gestures out (rare). 420 421 You should override this method in your custom SC.Gesturable subclasses to clean up any state 422 variables used in the touch session. 423 424 @returns {void} 425 */ 426 touchSessionCancelled: function () { 427 }, 428 429 /** 430 @param {SC.Touch} touch The touch to be added to the session. 431 @param {Array} touchesInSession The touches already in the session. 432 @returns {Boolean} True if the gesture is still interested in the touch session; false to stop getting notified for any further touch changes in the touch session. 433 */ 434 touchAddedToSession: function (touch, touchesInSession) { 435 return true; // Most gestures should theoretically be interested in a new touch session. 436 }, 437 438 /** 439 @param {SC.Touch} touch The touch to be removed from the session. 440 @param {Array} touchesInSession The touches still remaining in the session. 441 @returns {Boolean} True if the gesture is still interested in the touch session; false to stop getting notified for any further touch changes in the touch session. 442 */ 443 touchCancelledInSession: function (touch, touchesInSession) { 444 return true; 445 }, 446 447 /** 448 @param {SC.Touch} touch The touch to be removed from the session. 449 @param {Array} touchesInSession The touches still remaining in the session. 450 @returns {Boolean} True if the gesture is still interested in the touch session; false to stop getting notified for any further touch changes in the touch session. 451 */ 452 touchEndedInSession: function (touch, touchesInSession) { 453 return true; 454 }, 455 456 /** 457 @param {Array} touchesInSession The touches in the session. 458 @returns {Boolean} True if the gesture is still interested in the touch session; false to stop getting notified for any further touch changes in the touch session. 459 */ 460 touchesMovedInSession: function (touchesInSession) { 461 return true; 462 } 463 464 }); 465