The standard sentence structure is based on the normal event sequence of touch events:
ontouchdown().andnext().move().andnext().touchup()
It is of course also possible to have multiple clicks, so what you actually get is a chaining of the above:
ontouchdown().andnext().move().andnext().touchup().
andnext().touchdown.andnext().touchup()
The move event can be optional. For example during a click event you would think you'd have a touchdown followed by a touchup but in reality however you will almost always have a small movement in between. For this following sentences are supported:
ontouchdown().andnext().canmove().andnext().touchup()
And we actually would like to do something when any of the touch events happen, so after each type of event we can give an action to perform. For example, performing an action after a touchdown event:
ontouchdown().do(youractionhere())
And also, you may not want to perform the action always, but only if a certain condition is met. This then results in sentences of the form:
touchdown().if(<cond>).do(<action>).else().do(<action>)
touchdown().if(<cond>).andnext().move().do(<action>)
Constructing gestures is done by overriding the class GestureBuilder
. The basic structure of the code is thus:
public class SampleGesture extends GestureBuilder<YourViewClass> {
public SampleGesture(YourViewClass view)
{
super(view);
}
public TouchGesture create()
{
TouchGesture gesture = new TouchGesture("SampleGestureName");
this.Create(gesture).TouchDown()
.If(SomeCondition())
.AndNext().CanMove()
.AndNext().TouchUp()
.Do2(YourAction())
;
return gesture;
}
}
By inheriting your class from GestureBuilder
you get access to the methods implementing the standard conditions and actions, like for
example, the within()
construction in the above example.
Supported start-words for conditions are:
exceed()
: Specify a distance or time range condition within()
: Specify a distance or time range condition not()
: Negate a condition
Supported start-words for actions are:
nothing()
: Do nothingafter()
: Start a timer to perform some actionendCurrentTimer()
: End a running timer. The action for the timer will not be performed.invalidateGesture()
: Invalidate the gesturegestureIsCompleted()
: Set the gesture as completed
At the same time you can provide your own methods implementing custom conditions and actions. Methods providing conditions must return an object of a type implementing the IGestureCondition
interface, those providing actions must return an object of a type implementing the IGestureAction
interface.
public class SampleGesture extends GestureBuilder<YourViewClass> {
IGestureCondition SomeCondition()
{
}
IGestureAction YourAction()
{
}
}
The interface IGestureCondition
is defined like:
public interface IGestureCondition {
boolean checkCondition(GestureEvent motion, TouchGesture gesture);
}
The interface IGestureAction
is defined like:
public interface IGestureAction {
void executeAction(GestureEvent motion, TouchGesture gesture);
}
Often you will want to check things using properties of the touch events, like for example if you did touch inside a certain area of the view. Or you will want to take actions based on properties of the touch events. For this, the main methods of the interfaces have the GestureEvent motion
and the TouchGesture gesture
parameters. The motion
parameter provides you the event on which your condition is checked or your action is executed. The GestureEvent
class has methods to get the position and time properties of the event:
public class GestureEvent {
public GestureEvent(MotionEvent event)
{
androidEvent = event;
position = new ScreenVector((int)androidEvent.getX(), (int)androidEvent.getY());
}
public ScreenVector getPosition()
{
return position;
}
public long getTime()
{
return androidEvent.getEventTime();
}
ScreenVector position;
MotionEvent androidEvent;
}
The gesture parameter represents the gesture for which the event is evaluated. The TouchGesture
class provides methods for storing data:
public class TouchGesture implements IResetable {
public boolean contextExists(String key)
{
return context.containsKey(key);
}
public void addContext(String key, Object data)
{
context.put(key, data);
}
public void removeContext(String key)
{
context.remove(key);
}
public Object getContext(String key)
{
return context.get(key);
}
}
There are a number of default keys available in the TouchHandler
class to get at data automatically stored for each gesture:
TouchHandler.ActionDownPos
: the position where the screen was touchedTouchHandler.ActionDownTime
: the time at which the screen was touchedTouchHandler.ActionMovePos
: the last position of a continuous series of move eventsTouchHandler.ActionMoveTime
: the last time at which a continuous series of move events happenedTouchHandler.ActionUpPos
: the position at which a touchup event happenedTouchHandler.ActionUpTime
: the time at which the touchup event happened
It is of course possible to have multiple touch events in a single gesture, like for example during a double click gesture. In this case you have two touchdown and two touchup events. For this the TouchHandler
class has the static method getEventId
:
public static String getEventId(String dataKey, int index)
{
return dataKey + "_" + ((Integer)index).toString();
}
Mind that the index for the first event is 1 and not zero!
All that often you will want to have access to the view on which your gestures are executed. After all, mostly your actions will change the state of the view. For this, I created the GestureConditionBase<View>
and the GestureActionBase<View>
classes from which you can derive your own condition and action classes. These give you the opportunity to retrieve the view using the getTouchedView()
method.
public abstract class GestureConditionBase<T> implements IGestureCondition {
public GestureConditionBase(T view) {
touchView = view;
}
public T getTouchedView()
{
return touchView;
}
private T touchView;
}
public abstract class GestureActionBase<T> implements IGestureAction {
public GestureActionBase(T view) {
touchView = view;
}
public T getTouchedView()
{
return touchView;
}
private T touchView;
}
public class ClickOnRectangleGesture extends GestureBuilder<AndroidGestureDSLView> {
public ClickOnRectangleGesture(AndroidGestureDSLView view)
{
super(view);
}
public TouchGesture create()
{
TouchGesture gesture = new TouchGesture("ClickOnRectangleGesture");
this.Create(gesture).TouchDown()
.If(OnRectangle())
.AndNext().CanMove()
.If(within().milliMeters(2).fromTouchDown(1))
.AndNext().TouchUp()
.If(within().seconds(1).fromTouchDown(1))
.Do2(ShowMessage("You clicked on the rectangle"))
;
return gesture;
}
IGestureCondition OnRectangle()
{
return new OnRectangleCondition(getView());
}
IGestureAction ShowMessage(String message)
{
return new ShowMessageAction(getView(), message);
}
}
Okay, let's dissect this code:
The touchdown and touchup events will be clear I think, and also the OnRectangle
condition and ShowMessage
action connected to them. This condition and action are the only custom code in this gesture. All other code is part of the DSL. The within()
condition on the touchup event makes sure the touchup happens within a certain timeframe from the touchdown event. This way we can make sure it is a click and not a long click.
The move event might be a little bit more strange: after all, all we want is a click which doesn't really involve a move. However,
although this is true in the android emulator, on a real phone the user can make unintentional small movements. All this results in the following code:
.AndNext().CanMove()
.If(within().milliMeters(2).fromTouchDown(1))
There is little "under the hood semantics" in this piece of code. If you write a condition without performing an action, you always say that the condition must be
fulfilled. If the condition is not fulfilled, then the gesture is invalidated.
public class ClickAndDoubleClickOnRectangleGesture extends GestureBuilder<AndroidGestureDSLView> {
public ClickAndDoubleClickOnRectangleGesture(AndroidGestureDSLView view)
{
super(view);
}
public TouchGesture create()
{
TouchGesture gesture = new TouchGesture("ClickAndDoubleClickOnRectangleGesture");
this.Create(gesture).TouchDown()
.If(OnRectangle())
.AndNext().CanMove()
.If(within().milliMeters(2).fromTouchDown(1))
.AndNext().TouchUp()
.Do1(after().seconds(1).Do(
ShowMessage("You clicked on the rectangle"),
gestureIsCompleted()))
.AndNext().TouchDown()
.Do1(endCurrentTimer())
.AndNext().CanMove()
.If(within().milliMeters(2).fromTouchDown(1))
.AndNext().TouchUp()
.If(within().seconds(2).fromTouchDown(1))
.Do2(ShowMessage("You doubleclicked on the rectangle"))
;
return gesture;
}
IGestureCondition OnRectangle()
{
return new OnRectangleCondition(getView());
}
IGestureAction ShowMessage(String message)
{
return new ShowMessageAction(getView(), message);
}
}
Again, most of this will be obvious so I will restrict myself to what's important here: how to differentiate between the click and the double click.
From watching the code you will probably get the basic idea, after all, this is a DSL which should make things more obvious. How do we know we have a Click event and not a Double Click? If the last touchup event of the Click is not immediately followed by a touchdown event. We know this by starting a timer on the touchup event of the Click and destroying it when the second touchup event of the Double Click happens. Now, lets say you have a single click. The second touchup event will never happen, thus the timer will never be cancelled and will fire. The action you want to perform on the Click gesture is connected to this timer and will thus get executed. Lastly, once this action is executed, the gesture is finished so we call the gestureIsCompleted()
method.
public class DragRectangleGesture extends GestureBuilder<AndroidGestureDSLView> {
public DragRectangleGesture(AndroidGestureDSLView view)
{
super(view);
}
public TouchGesture create()
{
TouchGesture gesture = new TouchGesture("DragRectangleGesture");
this.Create(gesture).TouchDown()
.If(OnRectangle())
.Do2(RegisterRectangleHitPoint())
.AndNext()
.Move()
.If(not(within().milliMeters(2).fromTouchDown(1)))
.Do2(DragRectangle())
.Else()
.Do3(nothing())
.AndNext()
.TouchUp()
.Do1(nothing())
;
return gesture;
}
IGestureCondition OnRectangle()
{
return new OnRectangleCondition(getView());
}
IGestureAction RegisterRectangleHitPoint()
{
return new RegisterRectangleHitPointAction(getView());
}
IGestureAction DragRectangle()
{
return new DragRectangleAction(getView());
}
IGestureAction NoDragging()
{
return new NoDraggingAction(getView());
}
IGestureAction ShowMessage(String message)
{
return new ShowMessageAction(getView(), message);
}
}
The interesting things here are the action on the touchdown event and the condition and actions on the move event:
The action executed on the touchdown event stores the hit point on the rectangle in the event storage using the addContext
method of the gesture:
public class RegisterRectangleHitPointAction extends GestureActionBase<AndroidGestureDSLView> {
public static String RECTANGLE_CENTER_HITOFFSET = "RECTANGLE_CENTER_HITOFFSET";
public RegisterRectangleHitPointAction(AndroidGestureDSLView view) {
super(view);
}
@Override
public void executeAction(GestureEvent motion, TouchGesture gesture) {
Point rectangleCenter = getTouchedView().getRectangleCenter();
ScreenVector touchDownPoint = motion.getPosition();
Point hitOffset = new Point(rectangleCenter.x - touchDownPoint.x,
rectangleCenter.y - touchDownPoint.y);
gesture.addContext(RECTANGLE_CENTER_HITOFFSET, hitOffset);
}
String message;
}
The condition on the move event checks that the movement is bigger than two millimeters from the touchdown event. This is done in order to be able to combine
this drag gesture with a click gesture: you wouldn't want to start dragging anything when all the user wants to do is click it. Of course, if you do not support
the clicking, then you do not need the condition. Remember I stated above the implicit functionality that a condition which is not
fulfilled invalidates the gesture?
That is not what you want in this case, because otherwise your gesture would always immediately be invalidated: the condition will always start with failure because
the distance will always start with a value smaller than two millimeters. And that is why the Else()
part of the condition has an action nothing()
.