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.longtap;
15  
16  import com.google.gwt.core.client.GWT;
17  import com.google.gwt.event.shared.HasHandlers;
18  import com.googlecode.mgwt.collection.shared.CollectionFactory;
19  import com.googlecode.mgwt.collection.shared.LightArray;
20  import com.googlecode.mgwt.dom.client.event.touch.Touch;
21  import com.googlecode.mgwt.dom.client.event.touch.TouchCancelEvent;
22  import com.googlecode.mgwt.dom.client.event.touch.TouchEndEvent;
23  import com.googlecode.mgwt.dom.client.event.touch.TouchHandler;
24  import com.googlecode.mgwt.dom.client.event.touch.TouchMoveEvent;
25  import com.googlecode.mgwt.dom.client.event.touch.TouchStartEvent;
26  import com.googlecode.mgwt.dom.client.event.touch.TouchUtil;
27  import com.googlecode.mgwt.dom.client.recognizer.EventPropagator;
28  import com.googlecode.mgwt.dom.client.recognizer.TimerExecturGwtTimerImpl;
29  import com.googlecode.mgwt.dom.client.recognizer.TimerExecutor;
30  import com.googlecode.mgwt.dom.client.recognizer.TimerExecutor.CodeToRun;
31  
32  /**
33   * This class can recognize long taps
34   * 
35   * @author Daniel Kurka
36   * 
37   */
38  public class LongTapRecognizer implements TouchHandler {
39  
40    public static final int DEFAULT_WAIT_TIME_IN_MS = 1500;
41    public static final int DEFAULT_MAX_DISTANCE = 15;
42  
43    protected enum State {
44      INVALID, READY, FINGERS_DOWN, FINGERS_UP, WAITING
45    };
46  
47    protected State state;
48    private final HasHandlers source;
49    private final int numberOfFingers;
50    private final int time;
51  
52    private LightArray<Touch> startPositions;
53    private int touchCount;
54    private final int distance;
55  
56    private TimerExecutor timerExecutor;
57  
58    private EventPropagator eventPropagator;
59  
60    private static EventPropagator DEFAULT_EVENT_PROPAGATOR;
61  
62    /**
63     * Construct a LongTapRecognizer with that fires on one finger after 1.5s
64     * 
65     * @param source the source on which to fire events on
66     */
67    public LongTapRecognizer(HasHandlers source) {
68      this(source, 1);
69    }
70  
71    /**
72     * 
73     * Construct a LongTapRecognizer with that after 1.5s
74     * 
75     * @param source source the source on which to fire events on
76     * @param numberOfFingers the number of fingers to detect
77     */
78    public LongTapRecognizer(HasHandlers source, int numberOfFingers) {
79      this(source, numberOfFingers, DEFAULT_WAIT_TIME_IN_MS);
80    }
81  
82    /**
83     * Construct a LongTapRecognizer
84     * 
85     * @param source the source on which to fire events on
86     * @param numberOfFingers the number of fingers that should be detected
87     * @param time the time the fingers need to touch
88     */
89    public LongTapRecognizer(HasHandlers source, int numberOfFingers, int time) {
90      this(source, numberOfFingers, time, DEFAULT_MAX_DISTANCE);
91    }
92  
93    /**
94     * Construct a LongTapRecognizer
95     * 
96     * @param source the source on which to fire events on
97     * @param numberOfFingers the number of fingers that should be detected
98     * @param time the time the fingers need to touch
99     * @param maxDistance the maximum distance each finger is allowed to move
100    */
101   public LongTapRecognizer(HasHandlers source, int numberOfFingers, int time, int maxDistance) {
102 
103     if (source == null) {
104       throw new IllegalArgumentException("source can not be null");
105     }
106     if (numberOfFingers < 1) {
107       throw new IllegalArgumentException("numberOfFingers > 0");
108     }
109 
110     if (time < 200) {
111       throw new IllegalArgumentException("time > 200");
112     }
113 
114     if (maxDistance < 0) {
115       throw new IllegalArgumentException("maxDistance > 0");
116     }
117 
118     this.source = source;
119     this.numberOfFingers = numberOfFingers;
120     this.time = time;
121     this.distance = maxDistance;
122 
123     state = State.READY;
124     startPositions = CollectionFactory.constructArray();
125     touchCount = 0;
126 
127   }
128 
129   /*
130    * (non-Javadoc)
131    * 
132    * @see
133    * com.googlecode.mgwt.dom.client.event.touch.TouchStartHandler#onTouchStart(com.googlecode.mgwt
134    * .dom.client.event.touch.TouchStartEvent)
135    */
136   @Override
137   public void onTouchStart(TouchStartEvent event) {
138 
139     LightArray<Touch> touches = event.getTouches();
140     touchCount++;
141 
142     switch (state) {
143       case INVALID:
144         break;
145       case READY:
146         startPositions.push(TouchUtil.cloneTouch(touches.get(touchCount - 1)));
147         state = State.FINGERS_DOWN;
148         break;
149       case FINGERS_DOWN:
150         startPositions.push(TouchUtil.cloneTouch(touches.get(touchCount - 1)));
151         break;
152       case FINGERS_UP:
153       default:
154         state = State.INVALID;
155         break;
156     }
157 
158     if (touchCount == numberOfFingers) {
159       state = State.WAITING;
160       getTimerExecutor().execute(new CodeToRun() {
161 
162         @Override
163         public void onExecution() {
164           if (state != State.WAITING) {
165             // something else happened forget it
166             return;
167           }
168 
169           getEventPropagator().fireEvent(source, new LongTapEvent(source, numberOfFingers, time, startPositions));
170           reset();
171 
172         }
173       }, time);
174     }
175 
176     if (touchCount > numberOfFingers) {
177       state = State.INVALID;
178     }
179 
180   }
181 
182   /*
183    * (non-Javadoc)
184    * 
185    * @see
186    * com.googlecode.mgwt.dom.client.event.touch.TouchMoveHandler#onTouchMove(com.googlecode.mgwt
187    * .dom.client.event.touch.TouchMoveEvent)
188    */
189   @Override
190   public void onTouchMove(TouchMoveEvent event) {
191     switch (state) {
192       case WAITING:
193       case FINGERS_DOWN:
194       case FINGERS_UP:
195         // compare positions
196         LightArray<Touch> currentTouches = event.getTouches();
197         for (int i = 0; i < currentTouches.length(); i++) {
198           Touch currentTouch = currentTouches.get(i);
199           for (int j = 0; j < startPositions.length(); j++) {
200             Touch startTouch = startPositions.get(j);
201             if (currentTouch.getIdentifier() == startTouch.getIdentifier()) {
202               if (Math.abs(currentTouch.getPageX() - startTouch.getPageX()) > distance || Math.abs(currentTouch.getPageY() - startTouch.getPageY()) > distance) {
203                 state = State.INVALID;
204                 break;
205               }
206             }
207             if (state == State.INVALID) {
208               break;
209             }
210           }
211         }
212 
213         break;
214 
215       default:
216         state = State.INVALID;
217         break;
218     }
219 
220   }
221 
222   /*
223    * (non-Javadoc)
224    * 
225    * @see
226    * com.googlecode.mgwt.dom.client.event.touch.TouchEndHandler#onTouchEnd(com.googlecode.mgwt.dom
227    * .client.event.touch.TouchEndEvent)
228    */
229   @Override
230   public void onTouchEnd(TouchEndEvent event) {
231     int currentTouches = event.getTouches().length();
232     switch (state) {
233       case WAITING:
234         state = State.INVALID;
235         break;
236 
237       case FINGERS_DOWN:
238         state = State.FINGERS_UP;
239         break;
240       case FINGERS_UP:
241         // are we ready?
242         if (currentTouches == 0 && touchCount == numberOfFingers) {
243           // fire and reset
244 
245           reset();
246         }
247         break;
248 
249       case INVALID:
250       default:
251         if (currentTouches == 0)
252           reset();
253         break;
254     }
255 
256   }
257 
258   /*
259    * (non-Javadoc)
260    * 
261    * @see
262    * com.googlecode.mgwt.dom.client.event.touch.TouchCancelHandler#onTouchCanceled(com.googlecode
263    * .mgwt.dom.client.event.touch.TouchCancelEvent)
264    */
265   @Override
266   public void onTouchCanceled(TouchCancelEvent event) {
267     state = State.INVALID;
268     int currentTouches = event.getTouches().length();
269     if (currentTouches == 0)
270       reset();
271   }
272 
273   /**
274    * set the timer executor - used for testing...
275    * 
276    * @param timerExecutor the timer executor to use
277    */
278   protected void setTimerExecutor(TimerExecutor timerExecutor) {
279     this.timerExecutor = timerExecutor;
280   }
281 
282   /**
283    * set the event propagator that is used by the recognizer - used for testing
284    * 
285    * @param eventPropagator
286    */
287   protected void setEventPropagator(EventPropagator eventPropagator) {
288     this.eventPropagator = eventPropagator;
289 
290   }
291 
292   protected TimerExecutor getTimerExecutor() {
293     if (timerExecutor == null) {
294       timerExecutor = new TimerExecturGwtTimerImpl();
295     }
296     return timerExecutor;
297   }
298 
299   protected void reset() {
300     state = State.READY;
301     touchCount = 0;
302   }
303 
304   protected EventPropagator getEventPropagator() {
305     if (eventPropagator == null) {
306       if (DEFAULT_EVENT_PROPAGATOR == null) {
307         DEFAULT_EVENT_PROPAGATOR = GWT.create(EventPropagator.class);
308       }
309       eventPropagator = DEFAULT_EVENT_PROPAGATOR;
310     }
311     return eventPropagator;
312   }
313 
314 }