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





5.00/5 (5投票s)
用于在Android中创建触摸手势的DSL的概念。
目录
引言
在 第一部分中,我向您展示了如何使用DSL。本篇文章将更深入地解释我是如何得出DSL的各个类以及它们是如何实现的。
这是两部分系列文章的第二部分
领域特定语言的构建
我想先声明一下:以下描述是我如何得到这个特定的DSL。我绝无意提供任何关于通用DSL构建的指导,尽管我希望这里提出的一些想法能够对实现其他类似DSL有所帮助。
澄清一下:这不仅仅是领域特定语言,它更具体地说是一个 流畅接口。
好了,说清楚了这些,我们开始吧
写下你的句子
为了让我们的语言更有条理,我们写下我们希望能够写出的句子,希望这能揭示我们语言的结构
- 触摸抬起时显示选项菜单
- 移动时绘制一条线
- 如果触摸在矩形上然后移动,则拖动矩形
- 某种触摸事件发生
- 执行某个动作
- 某种触摸事件发生
- 检查是否满足某个条件
- 如果满足,则执行某个动作
- 如果不满足,则执行另一个动作
- 触摸按下后跟着上述任何一个构造
- 然后是移动,再次跟着上述任何一个构造
- 然后是触摸抬起,是的,也跟着上述任何一个构造
这个序列当然可以重复多次,想想例如双击手势:它将会有两次上述的事件序列。
方法化你的句子
请记住,我们的领域特定语言是一个流畅接口。它不是一门全新的编程语言,而是建立在现有语言之上的。在我们的例子中,它建立在Java语言之上,因此我们只能使用Java语言支持的语言构造,例如方法调用和这些方法的参数。
我们基本的事件序列变成
touchdown().andnext().move().andnext().touchup()
动作序列变成
touchdown().do(action)
在此,action是指定在touchdown
事件上做什么的参数。当然,虽然示例显示这与touchdown
事件相关联,但您可以对move
或touchup
事件做同样的事情。
条件序列变成
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中的方法,序列被填充了它需要检测的手势、需要满足的条件以及需要执行的动作。
这导致了两个类
TouchGesture
,代表需要执行的序列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时脑袋里在想些什么。