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

用于 Android 触摸事件的特定领域语言:第 1 部分:DSL 的描述和用法

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (14投票s)

2013 年 1 月 14 日

CPOL

9分钟阅读

viewsIcon

49079

downloadIcon

505

一个用于在Android中创建触摸手势的DSL。

目录

引言

接下来的文章将描述一个DSL,它允许构建描述触摸Android设备屏幕时需要满足的操作和条件的语句。

这是两部分系列的第一部分

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

基本思路

在编写自定义控件(后续文章将对此进行介绍)时,我得出结论,大多数时候,在处理触摸事件时,您会有一系列事件和某些您想要响应的条件。如果顺序不正确或条件不满足,那么您就不希望发生任何事情。

一个小例子会使这一点更清楚: 

假设您有一个像这样的视图

并且您想执行以下手势

  • 当您触摸屏幕,并且如果您触摸了任何矩形,那么接下来,在移动时,您希望被触摸的矩形移动
  • 当您触摸屏幕,并且您没有触摸任何矩形,那么接下来,在移动时,您希望执行类似平移的移动
  • 当您单击屏幕上的任何矩形时,显示一个操作菜单
  • 当您长按屏幕上的任何矩形时,显示有关该矩形的一些数据
如果您可以写出类似这样的句子,那不是很棒吗?
  • ontouchdown if(on rectangle) andnext move do(move selected rectangle)
  • ontouchdown if(not on rectangle) andnext move do(panning)
“单击”是按下和抬起操作的组合,其中抬起操作必须在按下操作的一定时间范围内完成,因此您会得到类似这样的内容
  • ontouchdown if(on rectangle) andnext touchup if (within 1 second of touchdown) then do(show action menu)
“长按”要求在一定时间内不发生任何事情

  • ontouchdown if(on rectangle) and if(nothinghappened during 4 seconds) then do(show data about rectangle)

支持的语句

主语句

标准的句子结构基于触摸事件的正常事件序列。

ontouchdown().andnext().move().andnext().touchup()

当然,也可以有多次单击,所以您实际得到的是上述的链式操作。

ontouchdown().andnext().move().andnext().touchup().
    andnext().touchdown.andnext().touchup() 
    // and so on ...

移动事件可以是可选的。例如,在单击事件期间,您会认为会有一次按下然后一次抬起,但实际上,在两者之间几乎总会有一个小的移动。为此,支持以下句子:

ontouchdown().andnext().canmove().andnext().touchup()

而且,我们实际上希望在任何触摸事件发生时都做些事情,所以每个事件类型之后,我们可以给出要执行的操作。例如,在按下事件之后执行一个操作:

ontouchdown().do(youractionhere())

而且,您可能不想总是执行操作,而只在满足特定条件时才执行。这会导致如下形式的句子:

// an if then else structure
touchdown().if(<cond>).do(<action>).else().do(<action>)

// only cotinue to the move if the condition on the touchdown is true
touchdown().if(<cond>).andnext().move().do(<action>)

条件的子语句

大多数条件将是您自己实现的自定义条件。然而,可以识别出一些条件,这些条件与触摸事件本身有关。以下是一些示例:

touchup.if(within.seconds(x).from.touchdown).do2(act)

move.if(within.millimeters(x).from.touchdown).do2(act)

move.if(exceed.millimeters(x).from.point(posx,posy)).do2(act) // not yet implemented

touchdown.if(nothinghappened.during.seconds(x).except.move.within.millimeters(x))

构建手势

通过覆盖GestureBuilder类来构建手势。因此,代码的基本结构如下:

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;
    }
    
    // more code here
}

通过继承GestureBuilder类,您可以访问实现标准条件和操作的方法,例如上面示例中的within()构造。

条件支持的起始词是:

  • exceed():指定距离或时间范围条件
  • within():指定距离或时间范围条件
  • not():否定一个条件

操作支持的起始词是:

  • nothing():什么都不做
  • after():启动一个计时器来执行某个操作
  • endCurrentTimer():结束一个正在运行的计时器。计时器的操作将不会被执行。
  • invalidateGesture():使手势无效
  • gestureIsCompleted():将手势设置为已完成
同时,您可以提供自己的方法来实现自定义条件和操作。提供条件的方法必须返回一个实现IGestureCondition接口的类型的对象,提供操作的方法必须返回一个实现IGestureAction接口的类型的对象。

public class SampleGesture extends GestureBuilder<YourViewClass> {
    
    // see above for the constructor and create method
    
    IGestureCondition SomeCondition()
    {
        // return an object of a type implementing the interface
    }
 
    IGestureAction YourAction()
    {
        // return an object of a type implementing the interface
    }
}

IGestureCondition接口定义如下:

public interface IGestureCondition {
    boolean checkCondition(GestureEvent motion, TouchGesture gesture);
}

IGestureAction接口定义如下:

public interface IGestureAction {
    void executeAction(GestureEvent motion, TouchGesture gesture);
}

您通常会想使用触摸事件的属性来检查事物,例如,您是否触摸了视图的某个特定区域。或者您想基于触摸事件的属性来采取行动。为此,接口的主要方法具有GestureEvent motionTouchGesture gesture参数。motion参数提供了您的条件正在检查或您的操作正在执行的事件。GestureEvent类具有获取事件的位置和时间属性的方法。

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

gesture参数表示正在评估事件的手势。TouchGesture类提供了存储数据的方法。

public class TouchGesture implements IResetable  {
 
    // more code ...
    
    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);
    }
}

TouchHandler类中有一些默认的键可用于访问为每个手势自动存储的数据:

  • TouchHandler.ActionDownPos:触摸屏幕的位置
  • TouchHandler.ActionDownTime:触摸屏幕的时间
  • TouchHandler.ActionMovePos:连续移动事件的最后一个位置
  • TouchHandler.ActionMoveTime:连续移动事件发生的最后一个时间
  • TouchHandler.ActionUpPos:发生抬起事件的位置
  • TouchHandler.ActionUpTime:发生抬起事件的时间

当然,一个手势中可能包含多个触摸事件,例如在双击手势中。在这种情况下,您有两个按下事件和两个抬起事件。为此,TouchHandler类具有静态方法getEventId

public static String getEventId(String dataKey, int index)
{
    return dataKey + "_" + ((Integer)index).toString();
}

请注意,第一个事件的索引是1,**不是**0!

您通常会想要访问正在执行手势的视图。毕竟,您的操作大多会改变视图的状态。为此,我创建了GestureConditionBase<View>GestureActionBase<View>类,您可以从中派生自己的条件和操作类。这些类让您有机会使用getTouchedView()方法来检索视图。

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

一些示例手势

我们有一个相当简单的视图类AndroidGestureDSLView,它显示了一个绿色的矩形,如文章开头的图片所示。该类有几个方法允许我们操纵矩形等,这些方法可以被我们定义的手势使用。

单击手势

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

好的,让我们来分析一下这段代码:

我想按下和抬起事件应该很清楚,还有与之相关的OnRectangle条件和ShowMessage操作。这个条件和操作是此手势中唯一的自定义代码。所有其他代码都是DSL的一部分。within()条件在抬起事件上确保抬起事件在按下事件的一定时间范围内发生。这样我们就可以确保它是一个单击,而不是长按。

移动事件可能有点奇怪:毕竟,我们想要的只是一个单击,它实际上不涉及移动。然而,虽然这在Android模拟器中是正确的,但在真实手机上,用户可能会有意想不到的小移动。所有这些都产生了以下代码:

.AndNext().CanMove()    // a move CAN happen but is not necesary
    .If(within().milliMeters(2).fromTouchDown(1))    // but when a move happens, it 
                                                    // must be within a small distance 
                                                    // of the touchdown event

这段代码中的“幕后语义”很少。如果您编写了一个不执行操作的条件,您总是说该条件必须被满足。如果不满足条件,则手势无效。

双击手势

public class DoubleClickOnRectangleGesture extends GestureBuilder<AndroidGestureDSLView> {
    
    public DoubleClickOnRectangleGesture(AndroidGestureDSLView view)
    {
        super(view);
    }
    
    public TouchGesture create()
    {
        TouchGesture gesture = new TouchGesture("DoubleClickOnRectangleGesture");
        
        this.Create(gesture).TouchDown()
                .If(OnRectangle())
            .AndNext().CanMove()
                .If(within().milliMeters(2).fromTouchDown(1))
            .AndNext().TouchUp()
                .Do1(nothing())
            .AndNext().TouchDown()
                .Do1(nothing())
            .AndNext().CanMove()
                .If(within().milliMeters(2).fromTouchDown(2))
            .AndNext().TouchUp()
                .If(within().seconds(1).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);
    }
}

我将简短地介绍一下,因为我认为这很明显:它基本上是连续两次单击,但操作和时间限制都在最后一次单击上。

我之所以想展示它,是因为您应该考虑一下,如果您将其与前面的单击手势结合起来会发生什么。单击事件当然也会触发,这可能不是您想要的。为了解决这个问题,我们必须在一个手势中处理单击和双击。这样我们就得到了下一个手势。

单击和双击的组合手势

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

同样,大部分内容都会是显而易见的,所以我会限制在重要的部分:如何区分单击和双击。

通过查看代码,您可能会得到基本想法,毕竟,这是一个应该让事情更直观的DSL。我们如何知道我们有一个单击事件而不是双击?如果单击的最后一次抬起事件没有立即被按下事件跟随。我们通过在单击的抬起事件上启动一个计时器,并在双击的第二次抬起事件发生时销毁它来知道这一点。现在,假设您有一个单击。第二次抬起事件永远不会发生,因此计时器永远不会被取消,并且会触发。您想在单击手势上执行的操作与此计时器相关联,因此将被执行。最后,一旦执行了该操作,手势就完成了,所以我们调用gestureIsCompleted()方法。

拖动手势

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

这里有趣的地方是按下事件上的操作,以及移动事件上的条件和操作。

在按下事件上执行的操作使用手势的addContext方法将矩形上的命中点存储在事件存储中。

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();
        //(ScreenVector)gesture.getContext(TouchHandler.ActionDownPos);
        
        Point hitOffset = new Point(rectangleCenter.x - touchDownPoint.x, 
                                    rectangleCenter.y - touchDownPoint.y);
        
        gesture.addContext(RECTANGLE_CENTER_HITOFFSET, hitOffset);
    }
    
    String message;
}

移动事件上的条件检查移动距离是否大于与按下事件的距离两毫米。这是为了能够将此拖动手势与单击手势结合起来:当用户只想单击它时,您不想开始拖动任何东西。当然,如果您不支持单击,那么您就不需要这个条件。还记得我上面说的,不满足的条件会使手势无效的隐含功能吗?在这种情况下,您不想要这个,因为否则您的手势将立即无效:条件总是以失败开始,因为距离总是从小于两毫米的值开始。这就是为什么条件中的Else()部分有一个nothing()操作。

接下来该怎么做?

当然,这还不是功能齐全的。有几件事浮现在脑海中:

  1. 支持多点触控手势。
  2. 在复杂移动手势中支持形状识别。
  3. 性能增强可能很有用。

接下来是什么?

在后续的文章中,我将更详细地讨论DSL的内部工作原理。

版本历史

  • 版本 1.0:初始版本
  • 版本1.1:以下是更改:
    • 将初始代码拆分为库和应用程序
    • GestureBuilder现在支持exceed()within()
    • GestureBuilder中,将getView()重命名为getBase()
    • TouchHandler现在支持上下文键LastActionPos
    • AfterConditionalContinuation<NextGesture>中删除了AndIf():这允许创建模糊的句子,例如:
      • .AndNext().TouchUp()
        	.If(not(within().seconds(3).fromTouchDown(1)))
        		.Do2(ShowMessage("You longclicked outside the rectangle"))
        	.AndIf(/* some condition */)
        			
© . All rights reserved.