65.9K
CodeProject 正在变化。 阅读更多。
Home

用于 Android 触摸事件的特定领域语言:第 2 部分:构造和内部工作原理

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2013 年 3 月 10 日

CPOL

7分钟阅读

viewsIcon

24514

用于在Android中创建触摸手势的DSL的概念。

目录

引言

第一部分中,我向您展示了如何使用DSL。本篇文章将更深入地解释我是如何得出DSL的各个类以及它们是如何实现的。

这是两部分系列文章的第二部分

  1. 用于 Android 触摸事件的特定领域语言:第 1 部分:DSL 的描述和用法
  2. 用于 Android 触摸事件的特定领域语言:第 2 部分:构造和内部工作原理

领域特定语言的构建

我想先声明一下:以下描述是我如何得到这个特定的DSL。我绝无意提供任何关于通用DSL构建的指导,尽管我希望这里提出的一些想法能够对实现其他类似DSL有所帮助。

澄清一下:这不仅仅是领域特定语言,它更具体地说是一个 流畅接口

好了,说清楚了这些,我们开始吧

写下你的句子

为了让我们的语言更有条理,我们写下我们希望能够写出的句子,希望这能揭示我们语言的结构

  • 触摸抬起时显示选项菜单
  • 移动时绘制一条线
  • 如果触摸在矩形上然后移动,则拖动矩形
我们已经可以看到一些结构出现了,我们主要有一个动作序列,如下所示:
  1. 某种触摸事件发生
  2. 执行某个动作
或者一个条件序列
  1. 某种触摸事件发生
  2. 检查是否满足某个条件
  3. 如果满足,则执行某个动作
  4. 如果不满足,则执行另一个动作
最后,还有事件的排序
  1. 触摸按下后跟着上述任何一个构造
  2. 然后是移动,再次跟着上述任何一个构造
  3. 然后是触摸抬起,是的,也跟着上述任何一个构造

这个序列当然可以重复多次,想想例如双击手势:它将会有两次上述的事件序列。

方法化你的句子

请记住,我们的领域特定语言是一个流畅接口。它不是一门全新的编程语言,而是建立在现有语言之上的。在我们的例子中,它建立在Java语言之上,因此我们只能使用Java语言支持的语言构造,例如方法调用和这些方法的参数。

我们基本的事件序列变成

touchdown().andnext().move().andnext().touchup()

动作序列变成

touchdown().do(action)

在此,action是指定在touchdown事件上做什么的参数。当然,虽然示例显示这与touchdown事件相关联,但您可以对movetouchup事件做同样的事情。

条件序列变成

touchdown.if(condition).do(trueAction).else().do(falseAction)

在此,condition是需要检查什么的参数,trueAction是在条件满足时执行的动作,falseAction是在条件结果为false时需要执行的操作。else()do()方法的man ual分离是一个品味问题,你也可以选择只提供一个elsedo()方法。同样,if序列可以连接到每个事件,即touchdown、move和touchup。

创建上下文

我们现在已经“方法化”了句子,因此,根据Java的实现方式,每个方法调用都必须返回一个对象,该对象的类型具有从该点开始可以调用的下一个可能方法。

为了使其更具象化,我创建了以下表格,将应由单个类型创建的方法对齐。

(由于本页面可用宽度有限,我将表格分为3部分:第二张表是第一张表的延续,第三张表是第二张表的延续)

表 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)  

表 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)   

表 3

    
    
    
    
.andnext().touchup().do1(act) 
.andnext().touchup().if(cond).do1(act)

您可能会问:那些do方法后面的数字是什么意思?它们在技术上并非真正必需,但我发现它们在调试时很有用。

正如我们在写句子时已经看到的,并且在上面的表格中再次显现,条件序列和动作序列对于每个运动事件都是通用的。因此,在任何这些序列结束之后,我们都必须能够切换到不同的运动事件。我通过使用泛型来实现这一点:实现序列的类型具有一个泛型类型参数,代表下一个运动事件,然后它是允许我们继续下一个事件的方法的结果类型。在这种情况下,就是andnext()方法。

所有这些都导致了以下类

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

实现动作和条件

现在我们实现了句子,接下来我们需要将动作和条件参数传递给句子中的方法。

我选择使用一个基类来能够流畅地将这些传递给我们的方法。为此,我们在基类中实现方法,这些方法要么直接返回动作或条件,要么返回上下文(即实现接口的类),最终返回动作或条件。

这些接口的构造方式与上面解释的类似

现在我们有了我们的语言。下一步当然是让它起作用。

实现领域特定语言

有一件事我还没有讨论,那就是如何最终返回我们用语言定义的事件序列。这里有一些可能性,例如用AndCreate()之类的东西来结束我们的句子。

但我没有这样做,而是选择从Create方法开始,并将需要填充的序列传递给它。因此,通过调用DSL中的方法,序列被填充了它需要检测的手势、需要满足的条件以及需要执行的动作。

这导致了两个类

  1. TouchGesture,代表需要执行的序列
  2. TouchEvent,代表带有其条件和动作的触摸事件

TouchGesture

以下代码清单仅显示了对事件排序重要的代码。要查看完整的源代码,您应该查阅源代码。

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

要构建一个手势,我们创建一个TouchGesture类的实例,并将其传递给GestureBuilder类,该类的Create方法返回我们DSL的初始上下文。

public class GestureBuilder<V> {
    
    public GestureBuilder(V view)
    {
        this.view = view;
    }
    
    public INextGestureAfterCreate Create(TouchGesture gesture)
    {
        return new NextGestureAfterCreate(gesture);
    }

    V view;
}

现在我们有了手势,我们需要一些东西来检查传入的手势事件,看看它们是否符合定义的手势序列。为此,我们有TouchHandler类。

但在我们继续之前,我想多谈谈这个“手势引擎”背后的通用思想。

正如本系列 第一部分所述,手势通常是一个触摸事件序列。所以我们想要做的是首先检查这个序列是否正确,其次是这个序列的条件是否正确。

为了检查序列,我们使用手势定义本身,并维护一个指向我们序列中位置的指针。如果当前事件与我们的预期不符,我们将整个序列作废。如果它与我们的预期相符,我们 then 检查条件,并根据结果执行动作。

但是如果我们作废了一个序列,那么我们什么时候才能重新启用它?假设你有三个手势:在控件的特定区域单击,无论何处长按,以及从任何地方开始的拖动。现在,假设我们触摸屏幕,但**不在**特定区域。因此,我们的单击手势立即作废。其他手势仍然可能。因此,当下一个事件到来时,单击手势将无法再评估。只有当所有手势都无效,或者一个有效的手势完成时,它才能被重新评估。当这些中的任何一个发生时,我们将重新启用所有手势。

所有这些都导致了以下TouchHandler

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

结论

虽然这篇文章有些理论化,但我希望它能帮助您了解我在编写这个DSL时脑袋里在想些什么。

© . All rights reserved.