View Javadoc
1   /*
2    * Copyright 2012 Daniel Kurka
3    * 
4    * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5    * in compliance with the License. You may obtain a copy of the License at
6    * 
7    * http://www.apache.org/licenses/LICENSE-2.0
8    * 
9    * Unless required by applicable law or agreed to in writing, software distributed under the License
10   * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11   * or implied. See the License for the specific language governing permissions and limitations under
12   * the License.
13   */
14  package com.googlecode.mgwt.dom.client.recognizer.tap;
15  
16  import com.google.gwt.event.shared.HasHandlers;
17  import com.googlecode.mgwt.collection.shared.CollectionFactory;
18  import com.googlecode.mgwt.collection.shared.LightArray;
19  import com.googlecode.mgwt.dom.client.event.touch.Touch;
20  import com.googlecode.mgwt.dom.client.event.touch.TouchCancelEvent;
21  import com.googlecode.mgwt.dom.client.event.touch.TouchEndEvent;
22  import com.googlecode.mgwt.dom.client.event.touch.TouchHandler;
23  import com.googlecode.mgwt.dom.client.event.touch.TouchMoveEvent;
24  import com.googlecode.mgwt.dom.client.event.touch.TouchStartEvent;
25  import com.googlecode.mgwt.dom.client.event.touch.TouchUtil;
26  import com.googlecode.mgwt.dom.client.recognizer.SystemTimeProvider;
27  import com.googlecode.mgwt.dom.client.recognizer.TimeProvider;
28  
29  /**
30   * A {@link MultiTapRecognizer} recognizes multiple taps with multiple fingers on the screen
31   * 
32   * @author Daniel Kurka
33   * 
34   */
35  public class MultiTapRecognizer implements TouchHandler {
36  
37    public static final int DEFAULT_DISTANCE = 15;
38    public static final int DEFAULT_TIME_IN_MS = 300;
39  
40    private final HasHandlers source;
41    private final int distance;
42    private final int time;
43    private final int numberOfTabs;
44  
45    private int touchCount;
46  
47    private LightArray<Touch> touches;
48    private final int numberOfFingers;
49  
50    private TimeProvider timeProvider;
51  
52    private enum State {
53      READY, FINGERS_GOING_DOWN, FINGERS_GOING_UP, INVALID
54    }
55  
56    private State state;
57    private int foundTaps;
58    private int touchMax;
59    private long lastTime;
60  
61    private LightArray<LightArray<Touch>> savedStartTouches;
62  
63    /**
64     * Construct a {@link MultiTapRecognizer}
65     * 
66     * @param source the source on which behalf to fire events on
67     * @param numberOfFingers the number of fingers needed for a tap
68     */
69    public MultiTapRecognizer(HasHandlers source, int numberOfFingers) {
70      this(source, numberOfFingers, 1, DEFAULT_DISTANCE, DEFAULT_TIME_IN_MS);
71    }
72  
73    /**
74     * Construct a {@link MultiTapRecognizer}
75     * 
76     * @param source the source on which behalf to fire events on
77     * @param numberOfFingers the number of fingers needed for a tap
78     * @param numberOfTabs the number of times all fingers have to touch and leave the display
79     */
80    public MultiTapRecognizer(HasHandlers source, int numberOfFingers, int numberOfTabs) {
81      this(source, numberOfFingers, numberOfTabs, DEFAULT_DISTANCE, DEFAULT_TIME_IN_MS);
82    }
83  
84    /**
85     * Construct a {@link MultiTapRecognizer}
86     * 
87     * @param source the source on which behalf to fire events on
88     * @param numberOfFingers the number of fingers needed for a tap
89     * @param numberOfTabs the number of times all fingers have to touch and leave the display
90     * @param distance the maximum distance a finger can move on the display
91     */
92    public MultiTapRecognizer(HasHandlers source, int numberOfFingers, int numberOfTabs, int distance) {
93      this(source, numberOfFingers, numberOfTabs, distance, DEFAULT_TIME_IN_MS);
94    }
95  
96    /**
97     * Construct a {@link MultiTapRecognizer}
98     * 
99     * @param source the source on which behalf to fire events on
100    * @param numberOfFingers the number of fingers needed for a tap
101    * @param numberOfTabs the number of times all fingers have to touch and leave the display
102    * @param distance the maximum distance a finger can move on the display
103    * @param time the maximum amount of time for the gesture to happen
104    */
105   public MultiTapRecognizer(HasHandlers source, int numberOfFingers, int numberOfTabs, int distance, int time) {
106 
107     if (source == null)
108       throw new IllegalArgumentException("source can not be null");
109 
110     if (numberOfFingers < 1) {
111       throw new IllegalArgumentException("numberOfFingers > 0");
112     }
113 
114     if (numberOfTabs < 1) {
115       throw new IllegalArgumentException("numberOfTabs > 0");
116     }
117 
118     if (distance < 0)
119       throw new IllegalArgumentException("distance > 0");
120 
121     if (time < 1) {
122       throw new IllegalArgumentException("time > 0");
123     }
124     this.source = source;
125     this.numberOfFingers = numberOfFingers;
126     this.numberOfTabs = numberOfTabs;
127     this.distance = distance;
128     this.time = time;
129     touchCount = 0;
130     touches = CollectionFactory.constructArray();
131     savedStartTouches = CollectionFactory.constructArray();
132     state = State.READY;
133     foundTaps = 0;
134     timeProvider = new SystemTimeProvider();
135 
136   }
137 
138   /*
139    * (non-Javadoc)
140    * 
141    * @see
142    * com.googlecode.mgwt.dom.client.event.touch.TouchStartHandler#onTouchStart(com.googlecode.mgwt
143    * .dom.client.event.touch.TouchStartEvent)
144    */
145   @Override
146   public void onTouchStart(TouchStartEvent event) {
147     touchCount++;
148     LightArray<Touch> currentTouches = event.getTouches();
149 
150     switch (state) {
151       case READY:
152         touches.push(TouchUtil.cloneTouch(currentTouches.get(touchCount - 1)));
153         state = State.FINGERS_GOING_DOWN;
154         break;
155 
156       case FINGERS_GOING_DOWN:
157         touches.push(TouchUtil.cloneTouch(currentTouches.get(touchCount - 1)));
158         break;
159 
160       case FINGERS_GOING_UP:
161       default:
162         state = State.INVALID;
163         break;
164     }
165 
166     if (touchCount > numberOfFingers) {
167       state = State.INVALID;
168     }
169 
170   }
171 
172   /*
173    * (non-Javadoc)
174    * 
175    * @see
176    * com.googlecode.mgwt.dom.client.event.touch.TouchMoveHandler#onTouchMove(com.googlecode.mgwt
177    * .dom.client.event.touch.TouchMoveEvent)
178    */
179   @Override
180   public void onTouchMove(TouchMoveEvent event) {
181     switch (state) {
182       case FINGERS_GOING_DOWN:
183       case FINGERS_GOING_UP:
184         // compare positions
185         LightArray<Touch> currentTouches = event.getTouches();
186         for (int i = 0; i < currentTouches.length(); i++) {
187           Touch currentTouch = currentTouches.get(i);
188           for (int j = 0; j < touches.length(); j++) {
189             Touch startTouch = touches.get(j);
190             if (currentTouch.getIdentifier() == startTouch.getIdentifier()) {
191               if (Math.abs(currentTouch.getPageX() - startTouch.getPageX()) > distance || Math.abs(currentTouch.getPageY() - startTouch.getPageY()) > distance) {
192                 state = State.INVALID;
193                 break;
194               }
195             }
196             if (state == State.INVALID) {
197               break;
198             }
199           }
200         }
201 
202         break;
203 
204       default:
205         break;
206     }
207 
208   }
209 
210   /*
211    * (non-Javadoc)
212    * 
213    * @see
214    * com.googlecode.mgwt.dom.client.event.touch.TouchEndHandler#onTouchEnd(com.googlecode.mgwt.dom
215    * .client.event.touch.TouchEndEvent)
216    */
217   @Override
218   public void onTouchEnd(TouchEndEvent event) {
219 
220     switch (state) {
221       case FINGERS_GOING_DOWN:
222         state = State.FINGERS_GOING_UP;
223         touchMax = touchCount;
224 
225         touchCount--;
226         handleTouchEnd();
227         break;
228       case FINGERS_GOING_UP:
229         touchCount--;
230         handleTouchEnd();
231         break;
232 
233       case INVALID:
234       case READY:
235         savedStartTouches = CollectionFactory.constructArray();
236         if (event.getTouches().length() == 0)
237           reset();
238         break;
239       default:
240         reset();
241         break;
242     }
243 
244   }
245 
246   /*
247    * (non-Javadoc)
248    * 
249    * @see
250    * com.googlecode.mgwt.dom.client.event.touch.TouchCancelHandler#onTouchCanceled(com.googlecode
251    * .mgwt.dom.client.event.touch.TouchCancelEvent)
252    */
253   @Override
254   public void onTouchCanceled(TouchCancelEvent event) {
255     state = State.INVALID;
256     reset();
257 
258   }
259 
260   protected void handleTouchEnd() {
261     if (touchCount == 0) {
262       // found one successful tap
263       if (foundTaps > 0) {
264         // check time otherwise invalid
265         if (timeProvider.getTime() - lastTime > time) {
266           savedStartTouches = CollectionFactory.constructArray();
267           reset();
268           return;
269         }
270       }
271       foundTaps++;
272       lastTime = timeProvider.getTime();
273 
274       // remember touches
275       savedStartTouches.push(touches);
276 
277       if (foundTaps == numberOfTabs) {
278 
279         MultiTapEvent multiTapEvent = new MultiTapEvent(touchMax, numberOfTabs, savedStartTouches);
280         source.fireEvent(multiTapEvent);
281         savedStartTouches = CollectionFactory.constructArray();
282         reset();
283       } else {
284         state = State.READY;
285         touches = CollectionFactory.constructArray();
286       }
287 
288     }
289   }
290 
291   protected void reset() {
292     touchCount = 0;
293     foundTaps = 0;
294     touches = CollectionFactory.constructArray();
295     state = State.READY;
296   }
297 
298   protected void setTimeProvider(TimeProvider timeProvider) {
299     if (timeProvider == null) {
300       throw new IllegalArgumentException("timeprovider can not be null");
301     }
302     this.timeProvider = timeProvider;
303   }
304 
305 }