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






4.98/5 (14投票s)
一个用于在Android中创建触摸手势的DSL。
目录
引言
接下来的文章将描述一个DSL,它允许构建描述触摸Android设备屏幕时需要满足的操作和条件的语句。
这是两部分系列的第一部分
基本思路
在编写自定义控件(后续文章将对此进行介绍)时,我得出结论,大多数时候,在处理触摸事件时,您会有一系列事件和某些您想要响应的条件。如果顺序不正确或条件不满足,那么您就不希望发生任何事情。
一个小例子会使这一点更清楚:
假设您有一个像这样的视图

并且您想执行以下手势
- 当您触摸屏幕,并且如果您触摸了任何矩形,那么接下来,在移动时,您希望被触摸的矩形移动
- 当您触摸屏幕,并且您没有触摸任何矩形,那么接下来,在移动时,您希望执行类似平移的移动
- 当您单击屏幕上的任何矩形时,显示一个操作菜单
- 当您长按屏幕上的任何矩形时,显示有关该矩形的一些数据
- 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 motion
和TouchGesture 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()
操作。
接下来该怎么做?
当然,这还不是功能齐全的。有几件事浮现在脑海中:
- 支持多点触控手势。
- 在复杂移动手势中支持形状识别。
- 性能增强可能很有用。
接下来是什么?
在后续的文章中,我将更详细地讨论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 */)