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