1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  3 // Copyright: ©2010 Strobe Inc. All rights reserved.
  4 // Author:    Peter Wagenet
  5 // License:   Licensed under MIT license (see license.js)
  6 // ==========================================================================
  7 
  8 sc_require("system/gesture");
  9 
 10 /**
 11   ## What is a "tap"?
 12 
 13   A tap is a touch that starts and ends in a short amount of time without moving along either axis.
 14   A tap may consist of more than one touch, provided that the touches start and end together. The time
 15   allowed for touches to start and end together is defined by `touchUnityDelay`.
 16   Again, to be considered a tap, there should be very little movement of any touches on either axis
 17   while still touching. The amount of movement allowed is defined by `tapWiggle`.
 18 
 19   @class
 20   @extends SC.Gesture
 21 */
 22 SC.TapGesture = SC.Gesture.extend(
 23 /** @scope SC.TapGesture.prototype */{
 24 
 25   /** @private The time that the first touch started at. */
 26   _sc_firstTouchAddedAt: null,
 27 
 28   /** @private The time that the first touch ended at. */
 29   _sc_firstTouchEndedAt: null,
 30 
 31   /** @private A flag used to track when the touch was long enough to register tapStart (and tapEnd). */
 32   _sc_isTapping: false,
 33 
 34   /** @private The number of touches in the current tap. */
 35   _sc_numberOfTouches: 0,
 36 
 37   /** @private A timer started after the first touch starts. */
 38   _sc_tapStartTimer: null,
 39 
 40   /**
 41     @type String
 42     @default "tap"
 43     @readOnly
 44   */
 45   name: "tap",
 46 
 47   /**
 48     The amount of time in milliseconds between when the first touch starts and the last touch ends
 49     that should be considered a short enough time to constitute a tap.
 50 
 51     @type Number
 52     @default 250
 53   */
 54   tapLengthDelay: 250,
 55 
 56   /**
 57     The amount of time in milliseconds after the first touch starts at which, *if the tap hasn't
 58     ended in that time*, the `tapStart` event should trigger.
 59 
 60     Because taps may be very short or because movement of the touch may invalidate a tap gesture
 61     entirely, you generally won't want to update the state of the view immediately when a touch
 62     starts.
 63 
 64     @type Number
 65     @default 150
 66     */
 67   tapStartDelay: 150,
 68 
 69   /**
 70     The number of pixels that a touch may move before it will no longer be considered a tap. If any
 71     of the touches move more than this amount, the gesture will give up.
 72 
 73     @type Number
 74     @default 10
 75   */
 76   tapWiggle: 10,
 77 
 78   /**
 79     The number of milliseconds that touches must start and end together in in order to be considered a
 80     tap. If the touches start too far apart in time or end too far apart in time based on this
 81     value, the gesture will give up.
 82 
 83     @type Number
 84     @default 75
 85   */
 86   touchUnityDelay: 75,
 87 
 88   /** @private Calculates the distance a touch has moved. */
 89   _sc_calculateDragDistance: function (touch) {
 90     return Math.sqrt(Math.pow(touch.pageX - touch.startX, 2) + Math.pow(touch.pageY - touch.startY, 2));
 91   },
 92 
 93   /** @private Cleans up the touch session. */
 94   _sc_cleanUpTouchSession: function (wasCancelled) {
 95     if (this._sc_isTapping) {
 96       // Trigger the gesture, 'tapCancelled'.
 97       if (wasCancelled) {
 98         this.cancel();
 99 
100       // Trigger the gesture, 'tapEnd'.
101       } else {
102         this.end();
103       }
104 
105       this._sc_isTapping = false;
106     }
107 
108     // Clean up.
109     this._sc_tapStartTimer.invalidate();
110     this._sc_numberOfTouches = 0;
111     this._sc_tapStartTimer = this._sc_firstTouchAddedAt = this._sc_firstTouchEndedAt = null;
112   },
113 
114   /** @private Triggers the tapStart event. Should *not* be reachable unless the tap is still valid. */
115   _sc_triggerTapStart: function () {
116       // Trigger the gesture, 'tapStart'.
117     this.start();
118 
119     this._sc_isTapping = true;
120   },
121 
122   /**
123     The tap gesture only remains interested in a touch session as long as none of the touches have
124     started too long after the first touch (value of `touchUnityDelay`). Once any touch has started
125     too late, the tap gesture gives up for the entire touch session and won't attempt to re-engage
126     (i.e. even if an extra touch "taps" cleanly in the same touch session, it won't trigger any
127     further tap callbacks).
128 
129     @param {SC.Touch} touch The touch to be added to the session.
130     @param {Array} touchesInSession The touches already in the session.
131     @returns {Boolean} True as long as the new touch doesn't start too late after the first touch.
132     @see SC.Gesture#touchAddedToSession
133     */
134   touchAddedToSession: function (touch, touchesInSession) {
135     var stillInterestedInSession,
136         delay;
137 
138     // If the new touch came in too late after the first touch was added.
139     delay = Date.now() - this._sc_firstTouchAddedAt;
140     stillInterestedInSession = delay < this.get('touchUnityDelay');
141 
142     return stillInterestedInSession;
143   },
144 
145   /**
146     If a touch cancels, the tap doesn't care and remains interested.
147 
148     @param {SC.Touch} touch The touch to be removed from the session.
149     @param {Array} touchesInSession The touches in the session.
150     @returns {Boolean} True
151     @see SC.Gesture#touchCancelledInSession
152     */
153   touchCancelledInSession: function (touch, touchesInSession) {
154     return true;
155   },
156 
157   /**
158     The tap gesture only remains interested in a touch session as long as none of the touches have
159     ended too long after the first touch ends (value of `touchUnityDelay`). Once any touch has ended
160     too late, the tap gesture gives up for the entire touch session and won't attempt to re-engage
161     (i.e. even if an extra touch "taps" cleanly in the same touch session, it won't trigger any
162     further tap callbacks).
163 
164     @param {SC.Touch} touch The touch to be removed from the session.
165     @param {Array} touchesInSession The touches in the session.
166     @returns {Boolean} True if it is the first touch to end or a subsequent touch that ends not too long after the first touch ended.
167     @see SC.Gesture#touchEndedInSession
168   */
169   touchEndedInSession: function (touch, touchesInSession) {
170     var stillInterestedInSession;
171 
172     // Increment the number of touches in the tap.
173     this._sc_numberOfTouches += 1;
174 
175     // If it's the first touch to end, remain interested unless tapLengthDelay has passed.
176     if (this._sc_firstTouchEndedAt === null) {
177       this._sc_firstTouchEndedAt = Date.now();
178       stillInterestedInSession = this._sc_firstTouchEndedAt - this._sc_firstTouchAddedAt < this.get('tapLengthDelay');
179 
180     // If the touch ended too late after the first touch ended, give up entirely.
181     } else {
182       stillInterestedInSession = Date.now() - this._sc_firstTouchEndedAt < this.get('touchUnityDelay');
183     }
184 
185     return stillInterestedInSession;
186   },
187 
188   /**
189     The tap gesture only remains interested in a touch session as long as none of the touches have
190     moved too far (value of `tapWiggle`). Once any touch has moved too far, the tap gesture gives
191     up for the entire touch session and won't attempt to re-engage (i.e. even if an extra touch
192     "taps" cleanly in the same touch session, it won't trigger any further tap callbacks).
193 
194     @param {Array} touchesInSession All touches in the session.
195     @returns {Boolean} True as long as none of the touches have moved too far to be a clean tap.
196     @see SC.Gesture#touchesMovedInSession
197     */
198   touchesMovedInSession: function (touchesInSession) {
199     var stillInterestedInSession = true;
200 
201     for (var i = 0, len = touchesInSession.length; i < len; i++) {
202       var touch = touchesInSession[i],
203           movedTooFar = this._sc_calculateDragDistance(touch) > this.get('tapWiggle');
204 
205       // If any touch has gone too far, we don't want to consider any further tap actions for this
206       // session. No need to continue.
207       if (movedTooFar) {
208         stillInterestedInSession = false;
209         break;
210       }
211     }
212 
213     return stillInterestedInSession;
214   },
215 
216   /**
217     Cleans up all touch session variables.
218 
219     @returns {void}
220     @see SC.Gesture#touchSessionCancelled
221     */
222   touchSessionCancelled: function () {
223     // Clean up (will fire tapCancelled if _sc_isTapping is true).
224     this._sc_cleanUpTouchSession(true);
225   },
226 
227   /**
228     Cleans up all touch session variables and triggers the gesture.
229 
230     @returns {void}
231     @see SC.Gesture#touchSessionEnded
232     */
233   touchSessionEnded: function () {
234     // Trigger the gesture, 'tap'.
235     this.trigger(this._sc_numberOfTouches);
236 
237     // Clean up (will fire tapEnd if _sc_isTapping is true).
238     this._sc_cleanUpTouchSession(false);
239   },
240 
241   /**
242     Registers when the first touch started.
243 
244     @param {SC.Touch} touch The touch that started the session.
245     @returns {void}
246     @see SC.Gesture#touchSessionStarted
247     */
248   touchSessionStarted: function (touch) {
249     // Initialize.
250     this._sc_firstTouchAddedAt = Date.now();
251 
252     this._sc_tapStartTimer = SC.Timer.schedule({
253       target: this,
254       action: this._sc_triggerTapStart,
255       interval: this.get('tapStartDelay')
256     });
257   }
258 
259 });
260