Click here to Skip to main content
Click here to Skip to main content

A Domain Specific Language for Android touch events: Part 2: Construction and inner workings

, 6 Dec 2013 CPOL
Rate this:
Please Sign up or sign in to vote.
Conception of a DSL for creating touch gestures in Android.

Contents

Introduction

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:

  1. A Domain Specific Language for Android touch events: Part 1: Description and usage of the DSL
  2. A Domain Specific Language for Android touch events: Part 2: Construction and inner workings

Construction of the Domain Specific Language

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:

Write down your sentences

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:
  1. Some kind of touch event happening
  2. Do some action
or a conditional sequence:
  1. Some kind of touch event happening
  2. Check if a condition is fulfilled
  3. If so, do some action
  4. If not, do some other action
Lastly, there is the sequencing of events:
  1. On touchdown followed by any of the constructions above
  2. And next on move, again followed by any of the constructions above
  3. 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.

Methodify your sentences

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.

Create contexts

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);
}

Implementing Actions and Conditions

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.

Implementing the Domain Specific Language

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:

  1. TouchGesture which represents the sequence which has to be executed
  2. TouchEvent which represents a touch event with its conditions and actions

TouchGesture

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;
    }
}    

TouchEvent

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)
        {
            // This can not be done if any gesture is valid but not yet completed
            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:
            // We have a touchdown event
            touchDownCounter++;
            for(TouchGesture eventOrder : gestureList)
            {
                // Store some data we can query in our actions and conditions
                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 the sequence is still valid and we expect a touchdown event, then process it
                if(eventOrder.isValid() && eventOrder.current().event == TouchEvent.TOUCH_DOWN)
                {
                    for(IfThenClause condition: eventOrder.current().conditionList)
                    {
                        // Execute the condition
                        condition.Execute(motion, eventOrder);
                        // Check if our gesture is still valid.
                        //    It is possible that the condition invalidated the gesture.
                        //    If this happened, there is no need to check any further conditions
                        if(!eventOrder.isValid())
                        {
                            break;
                        }
                    }
                    
                    // Signal this part of the sequence as executed
                    eventOrder.currentIsExecuted();
                    // and move the sequence pointer forward
                    eventOrder.moveNext();
                }
            }

            break;
        case MotionEvent.ACTION_MOVE:
            // We have a move event
            for(TouchGesture eventOrder : gestureList)
            {
                // Store some data we can query in our actions and conditions
                if(eventOrder.contextExists(LastActionPos))
                {
                    eventOrder.setContext(LastActionPos, motion.getPosition());
                }
                else
                {
                    eventOrder.addContext(LastActionPos, motion.getPosition());
                }
                
                boolean isValid = false;
                // If the sequence is still valid and we expect a move event, then process it
                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();
                    // Do not move to the next event because there will most likely be a series
                    //    of these move-events and otherwise only one would be accepted
                    //eventOrder.moveNext();
                }

                if(!isValid)
                    eventOrder.invalidate();
            }

            break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_POINTER_UP:
            touchUpCounter++;
            for(TouchGesture eventOrder : gestureList)
            {
                // Store some data we can query in our actions and conditions
                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 the sequence is still valid and we expect a move event
                //    which is not optional and is not executed yet
                // Then our sequence is no longer valid
                if(eventOrder.isValid() && (eventOrder.current().event == TouchEvent.TOUCH_MOVE) 
                        && !eventOrder.current().isOptional && !eventOrder.isCurrentExecuted())
                {
                    eventOrder.invalidate();
                }
                // If the sequence is still valid and we expect a move event
                //    which is optional and is executed yet
                // Then move forward in the sequence
                if(eventOrder.isValid() && (eventOrder.current().event == TouchEvent.TOUCH_MOVE) 
                        && (eventOrder.current().isOptional || eventOrder.isCurrentExecuted()))
                {
                    eventOrder.moveNext();
                }
                
                // If the sequence is still valid and we expect a touchup event, then process it
                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;
        }
        
        // Try resetting all the gestures
        tryReset();

    }
    
    private List<TouchGesture< gestureList = new ArrayList<TouchGesture>();
    private GestureEvent lastMotionEvent;
    
    private int touchDownCounter = 0;
    private int touchUpCounter = 0;
}

Conclusion

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Serge Desmedt
Software Developer (Senior)
Belgium Belgium
No Biography provided
Follow on   LinkedIn

Comments and Discussions

 
GeneralMy vote of 5 PinmemberMihai MOGA12-Apr-13 20:12 
GeneralRe: My vote of 5 PinmemberSerge Desmedt12-Apr-13 22:20 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.141216.1 | Last Updated 6 Dec 2013
Article Copyright 2013 by Serge Desmedt
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid