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 /** @class SC.Touch 9 Represents a touch. Single touch objects are passed to `touchStart`, `touchEnd` and `touchCancelled` event handlers; 10 a specialized multitouch event object is sent to `touchesDragged`, which include access to all in-flight touches 11 (see "The touchesDragged Multitouch Event Object" below). 12 13 SC.Touch exposes a number of properties, including pageX/Y, clientX/Y, screenX/Y, and startX/Y (the values that 14 pageX/Y had when the touch began, useful for calculating how far the touch has moved). It also exposes the touch's 15 target element at `target`, its target SC.View at `targetView`, and the touch's unique identifier at `identifier`, 16 which may be relied upon to identify a particular touch for the duration of its lifecycle. 17 18 A touch object exists for the duration of the touch – literally for as long as your finger is on the screen – and 19 is sent to a number of touch events on your views. Touch events are sent to the touch's current responder, set initially 20 by checking the responder chain for views which implement `touchStart` (or `captureTouch`, see "Touch Events" below), 21 and can be passed to other views as needed (see "Touch Responders" below). 22 23 Touch Events 24 ----- 25 You can use the following methods on your views to capture and handle touch events: 26 27 - `captureTouch` -- Sometimes, a touch responder part way up the chain may need to capture the touch and prevent it 28 from being made available to its childViews. The canonical use case for this behavior is SC.ScrollView, which by 29 default captures touches and holds onto them for 150ms to see if the user is scrolling, only passing them on to 30 children if not. (See SC.ScrollView#delaysContentTouches for more.) In order to support this use case, `captureTouch` 31 bubbles the opposite way as usual: beginning with the target's pane and bubbling *down* towards the target itself. 32 `captureTouch` is passed a single instance of `SC.Touch`, and must return YES if it wishes to capture the touch and 33 become its responder. (If your view doesn't want to immediately capture the touch, but instead wants to suggest itself 34 as a fallback handler in case the child view resigns respondership, it can do so by passing itself to the touch's 35 `stackCandidateTouchResponder` method.) 36 - `touchStart` -- When a touch begins, or when a new view responder is first given access to it (see "Touch Responders" 37 below), the touch is passed to this method. 38 - `touchesDragged` -- Whenever any touches move, the `touchesDragged` method is called on the current view responder 39 for any touches that have changed. The method is provided two arguments: a special multitouch event object (see "The 40 touchesDragged Multitouch Event Object" below), and an array containing all of the touches on that view. (This is 41 the same as calling `touch.touchesForView(this)`.) 42 - `touchEnd` -- When a touch is complete, its current responder's `touchEnd` handler is invoked, if present, and passed 43 the touch object which is ending. 44 - `touchCancelled` -- This method is generally only called if you have changed the touch's responder. See "Touch 45 Responders" below; in brief, if you pass the touch to another responder via `makeTouchResponder`, fully resigning 46 your touch respondership, you will receive a `touchCancelled` call for the next event; if you pass the touch to another 47 responder via `stackNextTouchResponder`, and never receive it back, you will receive a `touchCancelled` call when the 48 touch finishes. (Note that because RootResponder must call touchStart to determine if a view will accept respondership, 49 touchStart is called on a new responder before touchCancelled is called on the outgoing one.) 50 51 The touchesDragged Multitouch Event Object 52 ----- 53 The specialized event object sent to `touchesDragged` includes access to all touches currently in flight. You can 54 access the touches for a specific view from the `touchesForView` method, or get an average position of the touches 55 on a view from the convenient `averagedTouchesForView` method. For your convenience when dealing with the common 56 single-touch view, the `touchesDragged` event object also exposes the positional page, client, screen and start X/Y 57 values from the *first touch*. If you are interested in handling more than one touch, or in handling an average of 58 in-flight touches, you should ignore these values. (Note that this event object exposes an array of touch events at 59 `touches`. These are the browser's raw touch events, and should be avoided or used with care.) 60 61 Touch Responders: Passing Touches Around 62 ----- 63 The touch responder is the view which is currently handling events for that touch. A touch may only have one responder 64 at a time, though a view with `acceptsMultitouch: YES` may respond to more than one touch at a time. 65 66 A view becomes a touch responder by implementing touchStart (and not returning NO). (Out-of-order views can capture 67 touch responder status by implementing captureTouch and returning YES.) Once a view is a touch responder, only that 68 view will receive subsequent `touchesDragged` and `touchEnd` events; these events do not bubble like mouse events, and 69 they do *not* automatically switch to other views if the touch moves outside of its initial responder. 70 71 In some situations, you will want to pass control on to another view during the course of a touch, for example if 72 it goes over another view. To permanently pass respondership to another view: 73 74 if (shouldPassTouch) { 75 touch.makeTouchResponder(nextView); 76 } 77 78 This will trigger `touchStart` on the new responder, and `touchCancel` on the outgoing one. The new responder will begin 79 receiving `touchesDragged` events in place of the outgoing one. 80 81 If you want to pass respondership to another view, but are likely to want it back – for example, when a ScrollView 82 passes respondership to a child view but expects that the child view will pass it back if it moves more than a certain 83 amount: 84 85 if (shouldTemporarlyPassTouch) { 86 touch.stackNextTouchResponder(nextView); 87 } 88 89 This will trigger `touchStart` on the new responder, and it will start receiving `touchesDragged` and `touchEnd` events. 90 Note that the previous responder will not receive `touchCancelled` immediately, since the touch may return to it before 91 the end; instead, it will only receive `touchCancelled` when the touch is ended. 92 93 (If you would like to add a view as a fallback responder without triggering unnecessary calls to its `touchStart` and 94 `touchCancelled` events, for example as an alternative to returning YES from `captureTouch`, you can call 95 `stackCandidateTouchResponder` instead.) 96 97 When the child view decides that the touch has moved enough to be a scroll, it should pass touch respondership back 98 to the scroll view with: 99 100 if (Math.abs(touch.pageX - touch.startX) > 4) { 101 touch.restoreLastTouchResponder(); 102 } 103 104 This will trigger `touchCancelled` on the second responder, and the first one will begin receiving `touchDragged` events 105 again. 106 */ 107 SC.Touch = function(touch, touchContext) { 108 // get the raw target view (we'll refine later) 109 this.touchContext = touchContext; 110 // Get the touch's unique ID. 111 this.identifier = touch.identifier; 112 113 var target = touch.target, targetView; 114 115 // Special-case handling for TextFieldView's touch intercept overlays. 116 if (target && SC.$(target).hasClass("touch-intercept")) { 117 touch.target.style[SC.browser.experimentalStyleNameFor('transform')] = "translate3d(0px,-5000px,0px)"; 118 target = document.elementFromPoint(touch.pageX, touch.pageY); 119 if (target) targetView = SC.viewFor(target); 120 121 this.hidesTouchIntercept = NO; 122 if (target.tagName === "INPUT") { 123 this.hidesTouchIntercept = touch.target; 124 } else { 125 touch.target.style[SC.browser.experimentalStyleNameFor('transform')] = "translate3d(0px,0px,0px)"; 126 } 127 } else { 128 targetView = touch.target ? SC.viewFor(touch.target) : null; 129 } 130 131 this.targetView = targetView; 132 this.target = target; 133 this.type = touch.type; 134 135 this.touchResponders = []; 136 137 this.startX = this.pageX = touch.pageX; 138 this.startY = this.pageY = touch.pageY; 139 this.clientX = touch.clientX; 140 this.clientY = touch.clientY; 141 this.screenX = touch.screenX; 142 this.screenY = touch.screenY; 143 }; 144 145 SC.Touch.prototype = { 146 //@if(debug) 147 // Debug-mode only. 148 toString: function () { 149 return "SC.Touch (%@)".fmt(this.identifier); 150 }, 151 //@endif 152 153 /**@scope SC.Touch.prototype*/ 154 155 /** @private 156 The responder that's responsible for the creation and management of this touch. Usually this will be 157 your app's root responder. You must pass this on create, and should not change it afterwards. 158 159 @type {SC.RootResponder} 160 */ 161 touchContext: null, 162 163 /** 164 This touch's unique identifier. Provided by the browser and used to track touches through their lifetime. 165 You will not usually need to use this, as SproutCore's touch objects themselves persist throughout the 166 lifetime of a touch. 167 168 @type {Number} 169 */ 170 identifier: null, 171 172 /** 173 The touch's initial target element. 174 175 @type: {Element} 176 */ 177 target: null, 178 179 /** 180 The view for the touch's initial target element. 181 182 @type {SC.View} 183 */ 184 targetView: null, 185 186 /** 187 The touch's current view. (Usually this is the same as the current touchResponder.) 188 189 @type {SC.View} 190 */ 191 view: null, 192 193 /** 194 The touch's current responder, i.e. the view that is currently receiving events for this touch. 195 196 You can use the following methods to pass respondership for this touch between views as needed: 197 `makeTouchResponder`, `stackNextTouchResponder`, `restoreLastTouchResponder`, and `stackCandidateTouchResponder`. 198 See each method's documentation, and "Touch Responders: Passing Touches Around" above, for more. 199 200 @type {SC.Responder} 201 */ 202 touchResponder: null, 203 204 /** 205 Whether the touch has ended yet. If you are caching touches outside of the RootResponder, it is your 206 responsibility to check this property and handle ended touches appropriately. 207 208 @type {Boolean} 209 */ 210 hasEnded: NO, 211 212 /** 213 The touch's latest browser event's type, for example 'touchstart', 'touchmove', or 'touchend'. 214 215 Note that SproutCore's event model differs from that of the browser, so it is not recommended that 216 you use this property unless you know what you're doing. 217 218 @type {String} 219 */ 220 type: null, 221 222 /** @private 223 A faked mouse event property used to prevent unexpected behavior when proxying touch events to mouse 224 event handlers. 225 */ 226 clickCount: 1, 227 228 /** 229 The timestamp of the touch's most recent event. This is the time as of when all of the touch's 230 positional values are accurate. 231 232 @type {Number} 233 */ 234 timeStamp: null, 235 236 /** 237 The touch's latest clientX position (relative to the viewport). 238 239 @type {Number} 240 */ 241 clientX: null, 242 243 /** 244 The touch's latest clientY position (relative to the viewport). 245 246 @type {Number} 247 */ 248 clientY: null, 249 250 /** 251 The touch's latest screenX position (relative to the screen). 252 253 @type {Number} 254 */ 255 screenX: null, 256 257 /** 258 The touch's latest screenY position (relative to the screen). 259 260 @type {Number} 261 */ 262 screenY: null, 263 264 /** 265 The touch's latest pageX position (relative to the document). 266 267 @type {Number} 268 */ 269 pageX: null, 270 271 /** 272 The touch's latest pageY position (relative to the document). 273 274 @type {Number} 275 */ 276 pageY: null, 277 278 /** 279 The touch's initial pageX value. Useful for tracking a touch's total relative movement. 280 281 @type {Number} 282 */ 283 startX: null, 284 285 /** 286 The touch's initial pageY value. 287 288 @type {Number} 289 */ 290 startY: null, 291 292 /** 293 The touch's horizontal velocity, in pixels per millisecond, at the time of its last event. (Positive 294 velocities indicate movement leftward, negative velocities indicate movement rightward.) 295 296 @type {Number} 297 */ 298 velocityX: 0, 299 300 /** 301 The touch's vertical velocity, in pixels per millisecond, at the time of its last event. (Positive 302 velocities indicate movement downward, negative velocities indicate movement upward.) 303 304 @type {Number} 305 */ 306 velocityY: 0, 307 308 /** @private */ 309 unhideTouchIntercept: function() { 310 var intercept = this.hidesTouchIntercept; 311 if (intercept) { 312 setTimeout(function() { intercept.style[SC.browser.experimentalStyleNameFor('transform')] = "translate3d(0px,0px,0px)"; }, 500); 313 } 314 }, 315 316 /** 317 Indicates that you want to allow the normal default behavior. Sets 318 the hasCustomEventHandling property to YES but does not cancel the event. 319 */ 320 allowDefault: function() { 321 if (this.event) this.event.hasCustomEventHandling = YES ; 322 }, 323 324 /** 325 If the touch is associated with an event, prevents default action on the event. This is the 326 default behavior in SproutCore, which handles events through the RootResponder instead of 327 allowing native handling. 328 */ 329 preventDefault: function() { 330 if (this.event) this.event.preventDefault(); 331 }, 332 333 /** 334 Calls the native event's stopPropagation method, which prevents the method from continuing to 335 bubble. Usually, SproutCore will be handling the event via delegation at the `document` level, 336 so this method will have no effect. 337 */ 338 stopPropagation: function() { 339 if (this.event) this.event.stopPropagation(); 340 }, 341 342 stop: function() { 343 if (this.event) this.event.stop(); 344 }, 345 346 /** 347 Removes from and calls touchEnd on the touch responder. 348 */ 349 end: function() { 350 this.touchContext.endTouch(this); 351 }, 352 353 /** @private 354 This property, contrary to its name, stores the last touch responder for possible use later in the touch's 355 lifecycle. You will usually not use this property directly, instead calling `stackNextTouchResponder` to pass 356 the touch to a different view, and `restoreLastTouchResponder` to pass it back to the previous one. 357 358 @type {SC.Responder} 359 */ 360 nextTouchResponder: null, 361 362 /** @private 363 An array of previous touch responders. 364 365 @type {Array} 366 */ 367 touchResponders: null, 368 369 /** @private 370 A lazily-created array of candidate touch responders. Use `stackCandidateTouchResponder` to add candidates; 371 candidates are used as a fallback if the touch is out of previous touch responders. 372 373 @type {Array} 374 */ 375 candidateTouchResponders: null, 376 377 /** 378 A convenience method for making the passed view the touch's new responder, retaining the 379 current responder for possible use later in the touch's lifecycle. 380 381 For example, if the touch moves over a childView which implements its own touch handling, 382 you may pass the touch to it with: 383 384 touchesDragged: function(evt, viewTouches) { 385 if ([touches should be passed to childView]) { 386 this.viewTouches.forEach(function(touch) { 387 touch.stackNextTouchResponder(this.someChildView); 388 }, this); 389 } 390 } 391 392 The child view may easily pass the touch back to this view with `touch.restoreLastTouchResponder`. In the 393 mean time, this view will no longer receive `touchesDragged` events; if the touch is not returned to this 394 view before ending, it will receive a `touchCancelled` event rather than `touchEnd`. 395 396 @param {SC.Responder} view The view which should become this touch's new responder. 397 @param {Boolean} upChain Whether or not a fallback responder should be sought up the responder chain if responder doesn't capture or handle the touch. 398 */ 399 stackNextTouchResponder: function(view, upStack) { 400 this.makeTouchResponder(view, YES, upStack); 401 }, 402 403 /** 404 A convenience method for returning touch respondership to the previous touch responder. 405 406 For example, if your view is in a ScrollView and has captured the touch from it, your view 407 will prevent scrolling until you return control of the touch to the ScrollView with: 408 409 touchesDragged: function(evt, viewTouches) { 410 if (Math.abs(evt.pageY - evt.startY) > this.MAX_SWIPE) { 411 viewTouches.invoke('restoreLastTouchResponder'); 412 } 413 } 414 */ 415 restoreLastTouchResponder: function() { 416 // If we have a previous touch responder, go back to it. 417 if (this.nextTouchResponder) { 418 this.makeTouchResponder(this.nextTouchResponder); 419 } 420 // Otherwise, check if we have a candidate responder queued up. 421 else { 422 var candidates = this.candidateTouchResponders, 423 candidate = candidates ? candidates.pop() : null; 424 if (candidate) { 425 this.makeTouchResponder(candidate); 426 } 427 } 428 }, 429 430 /** 431 Changes the touch responder for the touch. If shouldStack is YES, 432 the current responder will be saved so that the next responder may 433 return to it. 434 435 You will generally not call this method yourself, instead exposing on 436 your view either a `touchStart` event handler method, or a `captureTouch` 437 method which is passed a touch object and returns YES. This method 438 is used in situations where touches need to be juggled between views, 439 such as when being handled by a descendent of a ScrollView. 440 441 When returning control of a touch to a previous handler, you should call 442 `restoreLastTouchResponder` instead. 443 444 @param {SC.Responder} responder The view to assign to the touch. (It, or if bubbling then an ancestor, 445 must implement touchStart.) 446 @param {Boolean} shouldStack Whether the new responder should replace the old one, or stack with it. 447 Stacked responders are easy to revert via `SC.Touch#restoreLastTouchResponder`. 448 @param {Boolean|SC.Responder} bubblesTo If YES, will attempt to find a `touchStart` responder up the 449 responder chain. If NO or undefined, will only check the passed responder. If you pass a responder 450 for this argument, the attempt will bubble until it reaches the passed responder, allowing you to 451 restrict the bubbling to a portion of the responder chain. (Note that this responder will not be 452 given an opportunity to respond to the event.) 453 @returns {Boolean} Whether a valid touch responder was found and assigned. 454 */ 455 makeTouchResponder: function(responder, shouldStack, bubblesTo) { 456 return this.touchContext.makeTouchResponder(this, responder, shouldStack, bubblesTo); 457 }, 458 459 /** 460 You may want your view to insert itself into the responder chain as a fallback, but without 461 having touchStart etc. called if it doesn't end up coming into play. For example, SC.ScrollView 462 adds itself as a candidate responder (when delaysTouchResponder is NO) so that views can easily 463 give it control, but without receiving unnecessary events if not. 464 */ 465 stackCandidateTouchResponder: function(responder) { 466 // Fast path: if we're the first one it's easy. 467 if (!this.candidateTouchResponders) { 468 this.candidateTouchResponders = [responder]; 469 } 470 // Just make sure it's not at the top of the stack. There may be a weird case where a 471 // view wants to be in a couple of spots in the stack, but it shouldn't want to be twice 472 // in a row. 473 else if (responder !== this.candidateTouchResponders[this.candidateTouchResponders.length - 1]) { 474 this.candidateTouchResponders.push(responder); 475 } 476 }, 477 478 /** 479 Captures, or recaptures, this touch. This works from the startingPoint's first child up to the 480 touch's target view to find a view which implements `captureTouch` and returns YES. If the touch 481 is captured, then this method will perform a standard `touchStart` event bubbling beginning with 482 the view which captured the touch. If no view captures the touch, then this method returns NO, 483 and you should call the `makeTouchResponder` method to trigger a standard `touchStart` bubbling 484 from the initial target on down. 485 486 You will generally not call this method yourself, instead exposing on 487 your view either a `touchStart` event handler method, or a `captureTouch` 488 method which is passed a touch object and returns YES. This method 489 is used in situations where touches need to be juggled between views, 490 such as when being handled by a descendent of a ScrollView. 491 492 @param {?SC.Responder} startingPoint The view whose children should be given an opportunity 493 to capture the event. (The starting point itself is not asked.) 494 @param {Boolean} shouldStack Whether any capturing responder should stack with existing responders. 495 Stacked responders are easy to revert via `SC.Touch#restoreLastTouchResponder`. 496 497 @returns {Boolean} Whether the touch was captured. If it was not, you should pass it to 498 `makeTouchResponder` for standard event bubbling. 499 */ 500 captureTouch: function(startingPoint, shouldStack) { 501 return this.touchContext.captureTouch(this, startingPoint, shouldStack); 502 }, 503 504 /** 505 Returns all touches for a specified view. Put as a convenience on the touch itself; 506 this method is also available on the event. 507 508 For example, to retrieve the list of touches impacting the current event handler: 509 510 touchesDragged: function(evt) { 511 var myTouches = evt.touchesForView(this); 512 } 513 514 @param {SC.Responder} view 515 */ 516 touchesForView: function(view) { 517 return this.touchContext.touchesForView(view); 518 }, 519 520 /** 521 A synonym for SC.Touch#touchesForView. 522 */ 523 touchesForResponder: function(responder) { 524 return this.touchContext.touchesForView(responder); 525 }, 526 527 /** 528 Returns average data--x, y, and d (distance)--for the touches owned by the supplied view. 529 530 See notes on the addSelf argument for an important consideration when calling from `touchStart`. 531 532 @param {SC.Responder} view 533 @param {Boolean} addSelf Includes the receiver in calculations. Pass YES for this if calling 534 from touchStart, as the touch will not yet be included by default. 535 */ 536 averagedTouchesForView: function(view, addSelf) { 537 return this.touchContext.averagedTouchesForView(view, (addSelf ? this : null)); 538 } 539 }; 540 541 SC.mixin(SC.Touch, { 542 543 create: function(touch, touchContext) { 544 return new SC.Touch(touch, touchContext); 545 }, 546 547 /** 548 Returns the averaged touch for an array of given touches. The averaged touch includes the 549 average position of all the touches (i.e. the center point), the averaged distance of all 550 the touches (i.e. the average of each touch's distance to the center) and the average velocity 551 of each touch. 552 553 The returned Object has a signature like, 554 555 { 556 x: ..., 557 y: ..., 558 velocityX: ..., 559 velocityY: ..., 560 d: ... 561 } 562 563 @param {Array} touches An array of touches to average. 564 @param {Object} objectRef An Object to assign the values to rather than creating a new Object. If you pass an Object to this method, that same Object will be returned with the values assigned to it. This is useful in order to only alloc memory once and hold it in order to avoid garbage collection. The trade-off is that more memory remains in use indefinitely. 565 @returns {Object} The averaged touch. 566 */ 567 averagedTouch: function (touches, objectRef) { 568 var len = touches.length, 569 ax = 0, ay = 0, avx = 0, avy = 0, 570 idx, touch; 571 572 // If no holder object is given, create a new one. 573 if (objectRef === undefined) { objectRef = {}; } 574 575 // Sum the positions and velocities of each touch. 576 for (idx = 0; idx < len; idx++) { 577 touch = touches[idx]; 578 ax += touch.pageX; 579 ay += touch.pageY; 580 avx += touch.velocityX; 581 avy += touch.velocityY; 582 } 583 584 // Average each value. 585 ax /= len; 586 ay /= len; 587 avx /= len; 588 avy /= len; 589 590 // Calculate average distance. 591 var ad = 0; 592 for (idx = 0; idx < len; idx++) { 593 touch = touches[idx]; 594 595 // Get distance for each from average position. 596 var dx = Math.abs(touch.pageX - ax); 597 var dy = Math.abs(touch.pageY - ay); 598 599 // Pythagoras was clever... 600 ad += Math.pow(dx * dx + dy * dy, 0.5); 601 } 602 603 // Average value. 604 ad /= len; 605 606 objectRef.x = ax; 607 objectRef.y = ay; 608 objectRef.velocityX = avx; 609 objectRef.velocityY = avy; 610 objectRef.d = ad; 611 612 return objectRef; 613 } 614 }); 615