/*
* Copyright 2012 Daniel Kurka
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.googlecode.mgwt.dom.client.recognizer.tap;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.dom.client.Touch;
import com.google.gwt.event.dom.client.TouchCancelEvent;
import com.google.gwt.event.dom.client.TouchEndEvent;
import com.google.gwt.event.dom.client.TouchMoveEvent;
import com.google.gwt.event.dom.client.TouchStartEvent;
import com.google.gwt.event.shared.HasHandlers;
import com.googlecode.mgwt.collection.shared.CollectionFactory;
import com.googlecode.mgwt.collection.shared.LightArray;
import com.googlecode.mgwt.dom.client.event.touch.TouchCopy;
import com.googlecode.mgwt.dom.client.event.touch.TouchHandler;
import com.googlecode.mgwt.dom.client.recognizer.SystemTimeProvider;
import com.googlecode.mgwt.dom.client.recognizer.TimeProvider;
/**
* A {@link MultiTapRecognizer} recognizes multiple taps with multiple fingers on the screen
*
* @author Daniel Kurka
*
*/
public class MultiTapRecognizer implements TouchHandler {
public static final int DEFAULT_DISTANCE = 15;
public static final int DEFAULT_TIME_IN_MS = 300;
private final HasHandlers source;
private final int distance;
private final int time;
private final int numberOfTabs;
private int touchCount;
private LightArray<TouchCopy> touches;
private final int numberOfFingers;
private TimeProvider timeProvider;
private enum State {
READY, FINGERS_GOING_DOWN, FINGERS_GOING_UP, INVALID
}
private State state;
private int foundTaps;
private int touchMax;
private long lastTime;
private LightArray<LightArray<TouchCopy>> savedStartTouches;
/**
* Construct a {@link MultiTapRecognizer}
*
* @param source the source on which behalf to fire events on
* @param numberOfFingers the number of fingers needed for a tap
*/
public MultiTapRecognizer(HasHandlers source, int numberOfFingers) {
this(source, numberOfFingers, 1, DEFAULT_DISTANCE, DEFAULT_TIME_IN_MS);
}
/**
* Construct a {@link MultiTapRecognizer}
*
* @param source the source on which behalf to fire events on
* @param numberOfFingers the number of fingers needed for a tap
* @param numberOfTabs the number of times all fingers have to touch and leave the display
*/
public MultiTapRecognizer(HasHandlers source, int numberOfFingers, int numberOfTabs) {
this(source, numberOfFingers, numberOfTabs, DEFAULT_DISTANCE, DEFAULT_TIME_IN_MS);
}
/**
* Construct a {@link MultiTapRecognizer}
*
* @param source the source on which behalf to fire events on
* @param numberOfFingers the number of fingers needed for a tap
* @param numberOfTabs the number of times all fingers have to touch and leave the display
* @param distance the maximum distance a finger can move on the display
*/
public MultiTapRecognizer(HasHandlers source, int numberOfFingers, int numberOfTabs, int distance) {
this(source, numberOfFingers, numberOfTabs, distance, DEFAULT_TIME_IN_MS);
}
/**
* Construct a {@link MultiTapRecognizer}
*
* @param source the source on which behalf to fire events on
* @param numberOfFingers the number of fingers needed for a tap
* @param numberOfTabs the number of times all fingers have to touch and leave the display
* @param distance the maximum distance a finger can move on the display
* @param time the maximum amount of time for the gesture to happen
*/
public MultiTapRecognizer(HasHandlers source, int numberOfFingers, int numberOfTabs, int distance, int time) {
if (source == null)
throw new IllegalArgumentException("source can not be null");
if (numberOfFingers < 1) {
throw new IllegalArgumentException("numberOfFingers > 0");
}
if (numberOfTabs < 1) {
throw new IllegalArgumentException("numberOfTabs > 0");
}
if (distance < 0)
throw new IllegalArgumentException("distance > 0");
if (time < 1) {
throw new IllegalArgumentException("time > 0");
}
this.source = source;
this.numberOfFingers = numberOfFingers;
this.numberOfTabs = numberOfTabs;
this.distance = distance;
this.time = time;
touchCount = 0;
touches = CollectionFactory.constructArray();
savedStartTouches = CollectionFactory.constructArray();
state = State.READY;
foundTaps = 0;
timeProvider = new SystemTimeProvider();
}
@Override
public void onTouchStart(TouchStartEvent event) {
touchCount++;
JsArray<Touch> currentTouches = event.getTouches();
switch (state) {
case READY:
touches.push(TouchCopy.copy(currentTouches.get(touchCount - 1)));
state = State.FINGERS_GOING_DOWN;
break;
case FINGERS_GOING_DOWN:
touches.push(TouchCopy.copy(currentTouches.get(touchCount - 1)));
break;
case FINGERS_GOING_UP:
default:
state = State.INVALID;
break;
}
if (touchCount > numberOfFingers) {
state = State.INVALID;
}
}
@Override
public void onTouchMove(TouchMoveEvent event) {
switch (state) {
case FINGERS_GOING_DOWN:
case FINGERS_GOING_UP:
// compare positions
JsArray<Touch> currentTouches = event.getTouches();
for (int i = 0; i < currentTouches.length(); i++) {
Touch currentTouch = currentTouches.get(i);
for (int j = 0; j < touches.length(); j++) {
TouchCopy startTouch = touches.get(j);
if (currentTouch.getIdentifier() == startTouch.getIdentifier()) {
if (Math.abs(currentTouch.getPageX() - startTouch.getPageX()) > distance || Math.abs(currentTouch.getPageY() - startTouch.getPageY()) > distance) {
state = State.INVALID;
break;
}
}
if (state == State.INVALID) {
break;
}
}
}
break;
default:
break;
}
}
@Override
public void onTouchEnd(TouchEndEvent event) {
switch (state) {
case FINGERS_GOING_DOWN:
state = State.FINGERS_GOING_UP;
touchMax = touchCount;
touchCount--;
handleTouchEnd();
break;
case FINGERS_GOING_UP:
touchCount--;
handleTouchEnd();
break;
case INVALID:
case READY:
savedStartTouches = CollectionFactory.constructArray();
if (event.getTouches().length() == 0)
reset();
break;
default:
reset();
break;
}
}
@Override
public void onTouchCancel(TouchCancelEvent event) {
state = State.INVALID;
reset();
}
protected void handleTouchEnd() {
if (touchCount == 0) {
// found one successful tap
if (foundTaps > 0) {
// check time otherwise invalid
if (getTimeProvider().getTime() - lastTime > time) {
savedStartTouches = CollectionFactory.constructArray();
reset();
return;
}
}
foundTaps++;
lastTime = getTimeProvider().getTime();
// remember touches
savedStartTouches.push(touches);
if (foundTaps == numberOfTabs) {
fireEvent(new MultiTapEvent(touchMax, numberOfTabs, savedStartTouches));
savedStartTouches = CollectionFactory.constructArray();
reset();
} else {
state = State.READY;
touches = CollectionFactory.constructArray();
}
}
}
protected void fireEvent(MultiTapEvent multiTapEvent) {
source.fireEvent(multiTapEvent);
}
protected void reset() {
touchCount = 0;
foundTaps = 0;
touches = CollectionFactory.constructArray();
state = State.READY;
}
// Visible for testing
TimeProvider getTimeProvider() {
return timeProvider;
}
}