Contents
In Part I I showed you how you can use the DSL. This post will explain in more depth how I came to the several classes of the DSL and how they are implemented.
It is the second part of a two parts series:
- A Domain Specific Language for Android touch events: Part 1: Description and usage of the DSL
- A Domain Specific Language for Android touch events: Part 2: Construction and inner workings
I would like to start with a disclaimer: the following description is how I came to this specific DSL. It is by no means my intention to provide any guidance on constructing a DSL in general,
although I hope that some of the ideas presented here can be useful for implementing other but similar DSLs.
And a clarification: This is not only a Domain Specific Language, it is more specifically a Fluent API.
OK, with this made clear, let's get started:
In an attempt to get some structure into our language we write down the sentences we would like to be able to write, hoping this will reveal this
structure in our language:
- on touchup show a menu of options
- on move draw a line
- if touchdown on rectangle and next move, drag the rectangle
We can already start to see some structure emerge, we mostly have an action sequence like:
- Some kind of touch event happening
- Do some action
or a conditional sequence:
- Some kind of touch event happening
- Check if a condition is fulfilled
- If so, do some action
- If not, do some other action
Lastly, there is the sequencing of events:
- On touchdown followed by any of the constructions above
- And next on move, again followed by any of the constructions above
- And next touchup, yes, also followed by any of the above
This sequence can of course be repeated several times, think about a double click gesture for example: it will have twice the above event sequence.
Remember that our domain specific language is a Fluent API. It is not a completely new programming language but is built on top of an existing language. In our case it is built on top of the Java language and thus we can only use language constructs supported by the Java language like method calls and arguments to these methods.
Our basic event sequence becomes:
touchdown().andnext().move().andnext().touchup()
The action sequence becomes:
touchdown().do(action)
In this, action is a parameter telling what to do on the touchdown
event. Of course, while the example shows this connected to the
touchdown
event, you can do the same for the move
or
touchup
event.
The conditional sequence becomes:
touchdown.if(condition).do(trueAction).else().do(falseAction)
In this, condition is a parameter telling what needs to be checked, trueAction
is the action performed when the condition is met, and
falseAction
what needs to be done when the result of the condition is false. The
separation of the else()
and do()
methods is a
matter of taste, you can also choose to just provide a method elsedo()
. Again, the if sequence can be connected to each of the events touchdown, move and touchup.
We now got the sentences "methodified" and thus, according to how Java is implemented, each method call must return an object of a type which has the next possible methods which can be called from that point on.
To make this a bit more visual, I created the following tables which aligns the methods that should be created by a single type.
(Due to the restricted width available on this page I split the table in 3: the second table is the continuation of the first, and the third is the continuation of the second)
Table 1
touchdown() | .do1(act) | | | |
touchdown() | .if(cond) | | | |
touchdown() | .if(cond) | .do2(act) | | |
touchdown() | .if(cond) | .do2(act) | .else() | .do3() |
touchdown() | .if(cond) | .do2(act) | | |
touchdown() | .if(cond) | .do2(act) | | |
Table 2
.andnext() | .move() | .do1(act) | | | |
.andnext() | .move() | .do1(act) | | | |
.andnext() | .move() | .do1(act) | | | |
.andnext() | .move() | .if(cond) | .do2(act) | .else() | .do3() |
.andnext() | .move() | .do1(act) | | | |
.andnext() | .canmove() | .do1(act) | | | |
Table 3
| | | |
| | | |
| | | |
| | | |
.andnext() | .touchup() | .do1(act) | |
.andnext() | .touchup() | .if(cond) | .do1(act) |
You may ask yourself: what are those numbers doing after those do
-methods? They are technically not really necessary, but I found them handy for debugging purposes.
As we could already see when we wrote down our sentences and is apparent again in the above table, the conditional sequence and the action sequence are generic for each motion event. As a result of this, after any of these sequence are ended, we must be able to switch to different motion events. I did this by using generics: the types implementing the sequence have a generic type parameter representing the next motion event and which is then the result type of the method allowing us to move on to the next event. In this case this is the andnext()
method.
All this results in following classes:
public interface INextGestureAfterCreate {
IActionAfterGestureOrConditional<INextGestureAfterTouchDown> TouchDown();
}
public interface INextGestureAfterTouchDown {
IActionAfterGestureOrConditional<INextGestureAfterMove> Move();
IActionAfterGestureOrConditional<INextGestureAfterMove> CanMove();
}
public interface INextGestureAfterMove {
IActionAfterGestureOrConditional<INextGestureAfterTouchUp> TouchUp();
}
public interface INextGestureAfterTouchUp {
IActionAfterGestureOrConditional<INextGestureAfterTouchDown> TouchDown();
}
public interface IActionAfterGestureOrConditional<NextGesture> {
INextGestureOrConditional<NextGesture> Do1(IGestureAction action);
INextGestureOrConditional<NextGesture> Do1(IGestureAction action1, IGestureAction action2);
INextGestureOrConditional<NextGesture> Do1(IGestureAction action1,
IGestureAction action2, IGestureAction action3);
IAfterConditional<NextGesture> If(IGestureCondition condition);
}
public interface INextGestureOrConditional<NextGesture> {
NextGesture AndNext();
IAfterConditional<NextGesture> AndIf();
}
public interface IAfterConditional<NextGesture> {
NextGesture AndNext();
public IAfterConditionalContinuation<NextGesture> Do2(IGestureAction action);
public IAfterConditionalContinuation<NextGesture> Do2(IGestureAction action1, IGestureAction action2);
public IAfterConditionalContinuation<NextGesture> Do2(IGestureAction action1,
IGestureAction action2, IGestureAction action3);
public IAfterConditionalContinuation<NextGesture> Do2(IGestureAction action1,
IGestureAction action2, IGestureAction action3, IGestureAction action4);
}
public interface IAfterConditionalContinuation<NextGesture> {
NextGesture AndNext();
IActionAfterGestureOrConditionalContinuation<NextGesture> Else();
}
public interface IActionAfterGestureOrConditionalContinuation<NextGesture> {
public INextGestureOrConditional<NextGesture> Do3(IGestureAction action);
public INextGestureOrConditional<NextGesture> Do3(IGestureAction action1, IGestureAction action2);
public INextGestureOrConditional<NextGesture> Do3(IGestureAction action1,
IGestureAction action2, IGestureAction action3);
public IAfterConditional<NextGesture> If(IGestureCondition condition);
}
Now we got our sentences implemented, we now have to pass the action and condition parameters to the methods in our sentences.
I have chosen to use a base class to be able to pass these fluently to our methods. For this we implement methods in this base class which
either return actions or conditions directly, or return contexts (thus classes implementing interfaces) which eventually result in the returning of actions or conditions.
These interfaces are constructed in a similar way as explained above
We now got our language. The next step is of course make it do something.
There is one thing I haven't discussed yet and that is how to eventually return the sequence of events we have defined with our language. There are some possibilities here,
like for example ending our sentences with something like AndCreate()
.
I didn't do this however, instead I opted to start with a Create
method and pas to it the sequence it has to fill. Thus, by calling methods from the
DSL, the sequence is being filled with gestures it has to detect, conditions which need to be fulfilled and actions to execute.
This results in two classes:
TouchGesture
which represents the sequence which has to be executedTouchEvent
which represents a touch event with its conditions and actions
The following code listing only shows the code which is important for the event sequencing. To see the complete source you should consult the source code:
public class TouchGesture implements IResetable {
public TouchGesture(String id)
{
this.id = id;
}
public String getId()
{
return id;
}
public void addEvent(TouchEvent event)
{
TouchEventExecution eventExecution = new TouchEventExecution();
eventExecution.touchEvent = event;
eventExecution.isExecuted = false;
eventList.add(eventExecution);
}
public TouchEvent getEvent(int index)
{
return eventList.get(index).touchEvent;
}
public int size()
{
return eventList.size();
}
public void reset()
{
for(IResetable resetable:resetableList)
{
resetable.reset();
}
if(onResetAction != null)
{
onResetAction.executeAction(null, this);
}
isValid = true;
index = 0;
context = new Hashtable<String, Object>();
}
public void invalidate()
{
isValid = false;
}
public boolean isValid()
{
return ((index < eventList.size()) && isValid);
}
public TouchEvent current()
{
return eventList.get(index).touchEvent;
}
public boolean isCurrentExecuted()
{
return eventList.get(index).isExecuted;
}
public void currentIsExecuted()
{
eventList.get(index).isExecuted = true;
}
public void moveNext()
{
index++;
}
public boolean isCompleted()
{
if(!isValid)
return false;
return (index >= eventList.size());
}
public void setAllEventsProcessed()
{
index = eventList.size();
}
private String id;
private List<TouchEventExecution> eventList = new ArrayList<TouchEventExecution>();
private List<IResetable> resetableList = new ArrayList<IResetable>();
private boolean isValid = true;
private int index = 0;
private class TouchEventExecution
{
TouchEvent touchEvent;
boolean isExecuted;
}
}
public class TouchEvent {
public static final int TOUCH_DOWN = 1;
public static final int TOUCH_MOVE = 2;
public static final int TOUCH_UP = 3;
public int event;
public boolean isOptional = false;
public ArrayList<IfThenClause> conditionList = new ArrayList<IfThenClause>();
}
To build a gesture, we create an instance of the TouchGesture
class and give it to the
GestureBuilder
class whose Create
method returns the initiating context of our
DSL.
public class GestureBuilder<V> {
public GestureBuilder(V view)
{
this.view = view;
}
public INextGestureAfterCreate Create(TouchGesture gesture)
{
return new NextGestureAfterCreate(gesture);
}
V view;
}
Now that we have our gesture, we need something to check the incoming gesture events to see if they conform with the defined gesture-sequence. For
this, we have the class TouchHandler
.
But before we continue, I would like to discuss a little bit more on the general idea behind this "gesture-engine".
As stated in the first Part I of this series, a gesture is typically a sequence of touch events. So what we want to do is check if first of all this sequence is correct and second of the conditions for this sequence are correct.
To check the sequence we use the gesture definition itself and keep a pointer to where we are in that the sequence. If the current event doesn't match with what we expect, we invalidate the whole sequence. If it does match with what we expect, we then check the conditions and dependent on this result we execute the actions
But if we invalidated a sequence, then when can we re-enable it? Let's say you have three gestures: a click in a specific area of the control, a long-click anywhere, and a dragging gesture starting anywhere. Now, let's say we touch the screen
not in the specific area. Thus our click gesture is immediately invalidated. The other gestures are still possible. So when the next events come in, the click gesture can not be evaluated anymore. It can only be re-evaluated if either all the gestures are invalid, or a valid gesture is completed. When either of these happen, we re-enable all the gestures.
All this results in the following TouchHandler
class:
public class TouchHandler {
public static String TouchHandlerId = "TOUCH_HANDLER";
public static String LastActionPos = "LAST_ACTION_POSITION";
public static String ActionDownPos = "ACTION_DOWN_POSITION";
public static String ActionDownTime = "ACTION_DOWN_TIME";
public static String ActionMovePos = "ACTION_MOVE_POSITION";
public static String ActionMoveTime = "ACTION_MOVE_TIME";
public static String ActionUpPos = "ACTION_UP_POSITION";
public static String ActionUpTime = "ACTION_UP_TIME";
public TouchHandler()
{
handler = new Handler();
}
public void addGesture(TouchGesture gesture)
{
gesture.addContext(TouchHandlerId, this);
gestureList.add(gesture);
}
public static String getEventId(String dataKey, int index)
{
return dataKey + "_" + ((Integer)index).toString();
}
public void tryReset()
{
boolean canReset = true;
for(TouchGesture eventOrder : gestureList)
{
if(eventOrder.isValid() && !eventOrder.isCompleted())
canReset = false;
}
if(canReset)
{
for(TouchGesture eventOrder : gestureList)
{
eventOrder.reset();
eventOrder.addContext(TouchHandlerId, this);
touchDownCounter = 0;
touchUpCounter = 0;
}
}
}
public void onTouchEvent(MotionEvent androidMotion) {
lastMotionEvent = new GestureEvent(androidMotion);
int action = androidMotion.getActionMasked();
GestureEvent motion = new GestureEvent(androidMotion);
switch (action) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
touchDownCounter++;
for(TouchGesture eventOrder : gestureList)
{
if(eventOrder.contextExists(LastActionPos))
{
eventOrder.setContext(LastActionPos, motion.getPosition());
}
else
{
eventOrder.addContext(LastActionPos, motion.getPosition());
}
eventOrder.addContext(TouchHandler.getEventId(ActionDownPos, touchDownCounter), motion.getPosition());
eventOrder.addContext(TouchHandler.getEventId(ActionDownTime, touchDownCounter), motion.getTime());
if(eventOrder.isValid() && eventOrder.current().event == TouchEvent.TOUCH_DOWN)
{
for(IfThenClause condition: eventOrder.current().conditionList)
{
condition.Execute(motion, eventOrder);
if(!eventOrder.isValid())
{
break;
}
}
eventOrder.currentIsExecuted();
eventOrder.moveNext();
}
}
break;
case MotionEvent.ACTION_MOVE:
for(TouchGesture eventOrder : gestureList)
{
if(eventOrder.contextExists(LastActionPos))
{
eventOrder.setContext(LastActionPos, motion.getPosition());
}
else
{
eventOrder.addContext(LastActionPos, motion.getPosition());
}
boolean isValid = false;
if(eventOrder.isValid() && eventOrder.current().event == TouchEvent.TOUCH_MOVE)
{
isValid = true;
for(IfThenClause condition: eventOrder.current().conditionList)
{
condition.Execute(motion, eventOrder);
if(!eventOrder.isValid())
{
break;
}
}
eventOrder.currentIsExecuted();
}
if(!isValid)
eventOrder.invalidate();
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
touchUpCounter++;
for(TouchGesture eventOrder : gestureList)
{
if(eventOrder.contextExists(LastActionPos))
{
eventOrder.setContext(LastActionPos, motion.getPosition());
}
else
{
eventOrder.addContext(LastActionPos, motion.getPosition());
}
eventOrder.addContext(TouchHandler.getEventId(ActionUpPos, touchUpCounter), motion.getPosition());
eventOrder.addContext(TouchHandler.getEventId(ActionUpTime, touchUpCounter), motion.getTime());
if(eventOrder.isValid() && (eventOrder.current().event == TouchEvent.TOUCH_MOVE)
&& !eventOrder.current().isOptional && !eventOrder.isCurrentExecuted())
{
eventOrder.invalidate();
}
if(eventOrder.isValid() && (eventOrder.current().event == TouchEvent.TOUCH_MOVE)
&& (eventOrder.current().isOptional || eventOrder.isCurrentExecuted()))
{
eventOrder.moveNext();
}
if(eventOrder.isValid() && eventOrder.current().event == TouchEvent.TOUCH_UP)
{
for(IfThenClause condition: eventOrder.current().conditionList)
{
condition.Execute(motion, eventOrder);
if(!eventOrder.isValid())
{
break;
}
}
eventOrder.currentIsExecuted();
eventOrder.moveNext();
}
}
break;
}
tryReset();
}
private List<TouchGesture< gestureList = new ArrayList<TouchGesture>();
private GestureEvent lastMotionEvent;
private int touchDownCounter = 0;
private int touchUpCounter = 0;
}
Although this post is somewhat theoretical I hope it helps you to get a picture of what went on inside my head when writing this DSL.