OLD | NEW |
| (Empty) |
1 // Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 /** | |
6 * Touch Handler. Class that handles all touch events and | |
7 * uses them to interpret higher level gestures and behaviors. TouchEvent is a | |
8 * built in mobile safari type: | |
9 * [http://developer.apple.com/safari/library/documentation/UserExperience/Refer
ence/TouchEventClassReference/TouchEvent/TouchEvent.html]. | |
10 * | |
11 * Examples of higher level gestures this class is intended to support | |
12 * - click, double click, long click | |
13 * - dragging, swiping, zooming | |
14 * | |
15 * Touch Behavior: | |
16 * Use this class to make your elements 'touchable' (see Touchable.dart). | |
17 * Intended to work with all webkit browsers. | |
18 * | |
19 * Drag Behavior: | |
20 * Use this class to make your elements 'draggable' (see draggable.js). | |
21 * This behavior will handle all of the required events and report the | |
22 * properties of the drag to you while the touch is happening and at the | |
23 * end of the drag sequence. This behavior will NOT perform the actual | |
24 * dragging (redrawing the element) for you, this responsibility is left to | |
25 * the client code. This behavior contains a work around for a mobile | |
26 * safari bug where the 'touchend' event is not dispatched when the touch | |
27 * goes past the bottom of the browser window. | |
28 * This is intended to work well in iframes. | |
29 * Intended to work with all webkit browsers, tested only on iPhone 3.x so | |
30 * far. | |
31 * | |
32 * Click Behavior: | |
33 * Not yet implemented. | |
34 * | |
35 * Zoom Behavior: | |
36 * Not yet implemented. | |
37 * | |
38 * Swipe Behavior: | |
39 * Not yet implemented. | |
40 */ | |
41 class TouchHandler { | |
42 Touchable _touchable; | |
43 Element _element; | |
44 | |
45 /** The absolute sum of all touch y deltas. */ | |
46 int _totalMoveY; | |
47 | |
48 /** The absolute sum of all touch x deltas. */ | |
49 int _totalMoveX; | |
50 | |
51 /** | |
52 * A list of tuples where the first item is the horizontal component of a | |
53 * recent relevant touch and the second item is the touch's time stamp. Old | |
54 * touches are removed based on the max tracking time and when direction | |
55 * changes. | |
56 */ | |
57 List<int> _recentTouchesX; | |
58 | |
59 /** | |
60 * A list of tuples where the first item is the vertical component of a | |
61 * recent relevant touch and the second item is the touch's time stamp. Old | |
62 * touches are removed based on the max tracking time and when direction | |
63 * changes. | |
64 */ | |
65 List<int> _recentTouchesY; | |
66 | |
67 // TODO(jacobr): make customizable by passing optional parameters to the | |
68 // TouchHandler constructor. | |
69 /** | |
70 * Minimum movement of touch required to be considered a drag. | |
71 */ | |
72 static final _MIN_TRACKING_FOR_DRAG = 2; | |
73 | |
74 /** | |
75 * The maximum number of ms to track a touch event. After an event is older | |
76 * than this value, it will be ignored in velocity calculations. | |
77 */ | |
78 static final _MAX_TRACKING_TIME = 250; | |
79 | |
80 /** The maximum number of touches to track. */ | |
81 static final _MAX_TRACKING_TOUCHES = 5; | |
82 | |
83 /** | |
84 * The maximum velocity to return, in pixels per millisecond, that is used to | |
85 * guard against errors in calculating end velocity of a drag. This is a very | |
86 * fast drag velocity. | |
87 */ | |
88 static final _MAXIMUM_VELOCITY = 5; | |
89 | |
90 /** | |
91 * The velocity to return, in pixel per millisecond, when the time stamps on | |
92 * the events are erroneous. The browser can return bad time stamps if the | |
93 * thread is blocked for the duration of the drag. This is a low velocity to | |
94 * prevent the content from moving quickly after a slow drag. It is less | |
95 * jarring if the content moves slowly after a fast drag. | |
96 */ | |
97 static final _VELOCITY_FOR_INCORRECT_EVENTS = 1; | |
98 | |
99 Draggable _draggable; | |
100 bool _tracking; | |
101 bool _dragging; | |
102 bool _touching; | |
103 int _startTouchX; | |
104 int _startTouchY; | |
105 int _startTime; | |
106 TouchEvent _lastEvent; | |
107 int _lastTouchX; | |
108 int _lastTouchY; | |
109 int _lastMoveX; | |
110 int _lastMoveY; | |
111 int _endTime; | |
112 int _endTouchX; | |
113 int _endTouchY; | |
114 | |
115 TouchHandler(Touchable touchable, [Element element = null]) | |
116 : _touchable = touchable, | |
117 _totalMoveY = 0, | |
118 _totalMoveX = 0, | |
119 _recentTouchesX = new List<int>(), | |
120 _recentTouchesY = new List<int>(), | |
121 // TODO(jmesserly): I don't like having to initialize all booleans here | |
122 // See b/5045736 | |
123 _dragging = false, | |
124 _tracking = false, | |
125 _touching = false { | |
126 _element = element != null ? element : touchable.getElement(); | |
127 } | |
128 | |
129 /** | |
130 * Begin tracking the touchable element, it is eligible for dragging. | |
131 */ | |
132 void _beginTracking() { | |
133 _tracking = true; | |
134 } | |
135 | |
136 /** | |
137 * Stop tracking the touchable element, it is no longer dragging. | |
138 */ | |
139 void _endTracking() { | |
140 _tracking = false; | |
141 _dragging = false; | |
142 _totalMoveY = 0; | |
143 _totalMoveX = 0; | |
144 } | |
145 | |
146 /** | |
147 * Correct erroneous velocities by capping the velocity if we think it's too | |
148 * high, or setting it to a default velocity if know that the event data is | |
149 * bad. Returns the corrected velocity. | |
150 */ | |
151 num _correctVelocity(num velocity) { | |
152 num absVelocity = velocity.abs(); | |
153 if (absVelocity > _MAXIMUM_VELOCITY) { | |
154 absVelocity = _recentTouchesY.length < 6 ? | |
155 _VELOCITY_FOR_INCORRECT_EVENTS : _MAXIMUM_VELOCITY; | |
156 } | |
157 return absVelocity * (velocity < 0 ? -1 : 1); | |
158 } | |
159 | |
160 /** | |
161 * Start listenting for events. | |
162 * If [capture] is True the TouchHandler should listen during the capture | |
163 * phase. | |
164 */ | |
165 void enable([bool capture = false]) { | |
166 Function onEnd = (e) { _onEnd(e.timeStamp, e); }; | |
167 _addEventListeners( | |
168 _element, | |
169 (e) { _onStart(e); }, | |
170 (e) { _onMove(e); }, onEnd, onEnd, capture); | |
171 } | |
172 | |
173 /** | |
174 * Get the current horizontal drag delta. Drag delta is defined as the deltaX | |
175 * of the start touch position and the last touch position. | |
176 */ | |
177 int getDragDeltaX() { | |
178 return _lastTouchX - _startTouchX; | |
179 } | |
180 | |
181 /** | |
182 * Get the current vertical drag delta. Drag delta is defined as the deltaY of | |
183 * the start touch position and the last touch position. | |
184 */ | |
185 int getDragDeltaY() { | |
186 return _lastTouchY - _startTouchY; | |
187 } | |
188 | |
189 /** | |
190 * Get end velocity of the drag. This method is specific to drag behavior, so | |
191 * if touch behavior and drag behavior is split then this should go with drag | |
192 * behavior. End velocity is defined as deltaXY / deltaTime where deltaXY is | |
193 * the difference between endPosition and the oldest recent position, and | |
194 * deltaTime is the difference between endTime and the oldest recent time | |
195 * stamp. | |
196 */ | |
197 Coordinate getEndVelocity() { | |
198 num velocityX = 0; | |
199 num velocityY = 0; | |
200 | |
201 if (_recentTouchesX.length > 0) { | |
202 num timeDeltaX = Math.max(1, _endTime - _recentTouchesX[1]); | |
203 velocityX = (_endTouchX - _recentTouchesX[0]) / timeDeltaX; | |
204 } | |
205 | |
206 if (_recentTouchesY.length > 0) { | |
207 num timeDeltaY = Math.max(1, _endTime - _recentTouchesY[1]); | |
208 velocityY = (_endTouchY - _recentTouchesY[0]) / timeDeltaY; | |
209 } | |
210 velocityX = _correctVelocity(velocityX); | |
211 velocityY = _correctVelocity(velocityY); | |
212 return new Coordinate(velocityX, velocityY); | |
213 } | |
214 | |
215 /** | |
216 * Return the touch of the last event. | |
217 */ | |
218 Touch _getLastTouch() { | |
219 assert (_lastEvent != null); // Last event not set | |
220 return _lastEvent.touches[0]; | |
221 } | |
222 | |
223 /** | |
224 * Is the touch manager currently tracking touch moves to detect a drag? | |
225 */ | |
226 bool isTracking() { | |
227 return _tracking; | |
228 } | |
229 | |
230 /** | |
231 * Touch end handler. | |
232 */ | |
233 void _onEnd(int timeStamp, [TouchEvent e = null]) { | |
234 _touching = false; | |
235 _touchable.onTouchEnd(); | |
236 if (!_tracking || _draggable === null) { | |
237 return; | |
238 } | |
239 Touch touch = _getLastTouch(); | |
240 int clientX = touch.clientX; | |
241 int clientY = touch.clientY; | |
242 if (_dragging) { | |
243 _endTime = timeStamp; | |
244 _endTouchX = clientX; | |
245 _endTouchY = clientY; | |
246 _recentTouchesX = _removeOldTouches(_recentTouchesX, timeStamp); | |
247 _recentTouchesY = _removeOldTouches(_recentTouchesY, timeStamp); | |
248 _draggable.onDragEnd(); | |
249 if (e !== null) { | |
250 e.preventDefault(); | |
251 } | |
252 ClickBuster.preventGhostClick(_startTouchX, _startTouchY); | |
253 } | |
254 _endTracking(); | |
255 } | |
256 | |
257 /** | |
258 * Touch move handler. | |
259 */ | |
260 void _onMove(TouchEvent e) { | |
261 if (!_tracking || _draggable === null) { | |
262 return; | |
263 } | |
264 final touch = e.touches[0]; | |
265 int clientX = touch.clientX; | |
266 int clientY = touch.clientY; | |
267 int moveX = _lastTouchX - clientX; | |
268 int moveY = _lastTouchY - clientY; | |
269 _totalMoveX += moveX.abs(); | |
270 _totalMoveY += moveY.abs(); | |
271 _lastTouchX = clientX; | |
272 _lastTouchY = clientY; | |
273 if (!_dragging && | |
274 ((_totalMoveY > _MIN_TRACKING_FOR_DRAG && _draggable.verticalEnabled) || | |
275 (_totalMoveX > _MIN_TRACKING_FOR_DRAG && | |
276 _draggable.horizontalEnabled))) { | |
277 _dragging = _draggable.onDragStart(e); | |
278 if (!_dragging) { | |
279 _endTracking(); | |
280 } else { | |
281 _startTouchX = clientX; | |
282 _startTouchY = clientY; | |
283 _startTime = e.timeStamp; | |
284 } | |
285 } | |
286 if (_dragging) { | |
287 _draggable.onDragMove(); | |
288 _lastEvent = e; | |
289 e.preventDefault(); | |
290 _recentTouchesX = | |
291 _removeTouchesInWrongDirection(_recentTouchesX, _lastMoveX, moveX); | |
292 _recentTouchesY = | |
293 _removeTouchesInWrongDirection(_recentTouchesY, _lastMoveY, moveY); | |
294 _recentTouchesX = _removeOldTouches(_recentTouchesX, e.timeStamp); | |
295 _recentTouchesY = _removeOldTouches(_recentTouchesY, e.timeStamp); | |
296 _recentTouchesX.add(clientX); | |
297 _recentTouchesX.add(e.timeStamp); | |
298 _recentTouchesY.add(clientY); | |
299 _recentTouchesY.add(e.timeStamp); | |
300 } | |
301 _lastMoveX = moveX; | |
302 _lastMoveY = moveY; | |
303 } | |
304 | |
305 /** | |
306 * Touch start handler. | |
307 */ | |
308 void _onStart(TouchEvent e) { | |
309 if (_touching) { | |
310 return; | |
311 } | |
312 _touching = true; | |
313 if (!_touchable.onTouchStart(e) || _draggable === null) { | |
314 return; | |
315 } | |
316 final touch = e.touches[0]; | |
317 _startTouchX = _lastTouchX = touch.clientX; | |
318 _startTouchY = _lastTouchY = touch.clientY; | |
319 _startTime = e.timeStamp; | |
320 // TODO(jacobr): why don't we just clear the lists? | |
321 _recentTouchesX = new List<int>(); | |
322 _recentTouchesY = new List<int>(); | |
323 _recentTouchesX.add(touch.clientX); | |
324 _recentTouchesX.add(e.timeStamp); | |
325 _recentTouchesY.add(touch.clientY); | |
326 _recentTouchesY.add(e.timeStamp); | |
327 _lastEvent = e; | |
328 _beginTracking(); | |
329 } | |
330 | |
331 /** | |
332 * Filters the provided recent touches list to remove all touches older than | |
333 * the max tracking time or the 5th most recent touch. | |
334 * [recentTouches] specifies a list of tuples where the first item is the x | |
335 * or y component of the recent touch and the second item is the touch time | |
336 * stamp. The time of the most recent event is specified by [recentTime]. | |
337 */ | |
338 List<int> _removeOldTouches(List<int> recentTouches, | |
339 int recentTime) { | |
340 int count = 0; | |
341 final len = recentTouches.length; | |
342 assert (len % 2 == 0); | |
343 while (count < len && | |
344 recentTime - recentTouches[count + 1] > _MAX_TRACKING_TIME || | |
345 (len - count) > _MAX_TRACKING_TOUCHES * 2) { | |
346 count += 2; | |
347 } | |
348 return count == 0 ? recentTouches : _removeFirstN(recentTouches, count); | |
349 } | |
350 | |
351 static List<int> _removeFirstN(List<int> list, int n) { | |
352 return list.getRange(n, list.length - n); | |
353 } | |
354 | |
355 /** | |
356 * Filters the provided recent touches list to remove all touches except the | |
357 * last if the move direction has changed. | |
358 * [recentTouches] specifies a list of tuples where the first item is the x | |
359 * or y component of the recent touch and the second item is the touch time | |
360 * stamp. The x or y component of the most recent move is specified by | |
361 * [recentMove]. | |
362 */ | |
363 List<int> _removeTouchesInWrongDirection(List<int> recentTouches, | |
364 int lastMove, int recentMove) { | |
365 if (lastMove !=0 && recentMove != 0 && recentTouches.length > 2 && | |
366 _xor(lastMove > 0, recentMove > 0)) { | |
367 return _removeFirstN(recentTouches, recentTouches.length - 2); | |
368 } | |
369 return recentTouches; | |
370 } | |
371 | |
372 // TODO(jacobr): why doesn't bool implement the xor operator directly? | |
373 static bool _xor(bool a, bool b) { | |
374 return (a === true || b === true) && !(a === true && b === true); | |
375 } | |
376 | |
377 /** | |
378 * Reset the touchable element. | |
379 */ | |
380 void reset() { | |
381 _endTracking(); | |
382 _touching = false; | |
383 } | |
384 | |
385 /** | |
386 * Call this method to enable drag behavior on a draggable delegate. | |
387 * The [draggable] object can be the same as the [_touchable] object, they are | |
388 * assigned to different members to allow for strong typing with interfaces. | |
389 */ | |
390 void setDraggable(Draggable draggable) { | |
391 _draggable = draggable; | |
392 } | |
393 } | |
OLD | NEW |