原文链接 Android事件高级手势处理
GestureDetector只能帮我们处理并识别一些常用的简单的手势,如点击,双击,长按,滑动(Scroll)和快速滑动(Fling)等,一般情况下,这些足够我们使用了,但有些时候需要一些更为复杂的手势操作,如Translate,Zoom,Scale和Rotate,以及像处理一些多点触控(MultiTouch),这就需要开发人猿自己处理了,本文将讨论一下这些内容。
高级手势识别
移动(Translate/Drag)
这里的移动的意思是让物体随着手指在屏幕上移动,或者叫作拖拽。而且这个只需要一个手指就可以办到,不涉及多点触控。
其实,这个实现起来并不复杂,从onTouchEvent处获得事件后,不断的用MotionEvent的坐标来刷新目标View即可,甚至都不用管具体的事件类型,因为无论是ACTION_DOWN,ACTION_UP或者ACTION_MOVE,都可以提供新的坐标,只管从事件处取坐标然后刷新就可以了。
draw at (x0, y0);
onTouchEvent(event) {
x = event.getRawX();
y = event.getRawY();
invalidate with (x, y); // will draw at (x, y);
}
旋转(Rotate)
同样,对于旋转用单个手指也可以办到,以目标View当前的位置为圆心,以手指划过的曲线作为圆弧,由此便可让目标View旋转起来,而且这个手势由单个手指也可以实现,不用管多点触控。
其实可以进一步的做简化,认定屏幕中央为圆心,来计算手势划过的角度,并且为了连惯性,要以事件ACTION_MOVE过程中的增量角度来对View进行旋转,这样会让旋转看起来更顺滑一些,额外的工作是要把事件的坐标进行一下转化,转化为以屏幕中心为原点的坐标。
具体的流程是:
lastTheta = -1;
onTouchEvent(event) {
switch (action) {
case ACTION_DOWN:
lastX = normalize(event.getX());
lastY = normalize(event.getY());
lastTheta = angle(lastX, lastY);
break;
case ACTION_MOVE:
newX = normalize(event.getX());
newY = normalize(event.getY());
theta = angle(newX, newY);
deltaTheta = alpha - beta;
invalidate to rotate with deltaTheta;
lastTheta = theta;
break;
case ACTION_CANCEL:
case ACTION_UP:
we are done.
}
normalizeX(x) {
return 2 * x / screenWidth;
}
normalizeY(y) {
return 2 * y / screenHeight;
}
angle(x, y) {
return atan(y / x);
}
至于缩放,单个手指无法完成,必须要用两个手指才可以,就涉及到多点触控,所以需要先介绍一下多点触控。
多点触控(MultiTouch)
这个并不复杂,虽然听起来像个神秘高科技,但其实,处理流程并不复杂,主体流程仍然是在onTouchEvent方法中,并且主要的对象仍是MotionEvent,文档里面基本上都说清楚了,要点就是:
- MotionEvent对象,会用pointerId和pointerIndex来区分不同的触控点(术语是Pointer)
- 事件流是:ACTION_DOWN 称为主触控点(Primary Pointer),然后是ACTION_POINTER_DOWN 另外一个触控点来了(非Primary Pointer),然后是ACTION_MOVE 这里没有显示 区分不同的pointer,需要开发人猿自己去区分,然后是ACTION_POINTER_UP 非主触控点 离开了,最后是ACTION_UP 主触控点离开了。需要注意的是,这是处理事件的逻辑上的顺序 ,真实的事件流,不一定是这样的(ACTION_DOWN肯定是第一个,ACTION_UP肯定 肯定最后一个,但中间的几个有顺序 不定)。
- 注意的要点,每次事件来了后,不同的触控点(Pointer)的index并不是固定的,比如上一次MOVE时它在index 0,但下次可能就在index 1,而其Pointer Id是固定的。所以在处理的整个流程中要记录不同Pointer的id,然后获得其index,再用index去取坐标啊之类的数据。
- 多点触控,天生就支持,所以即使你不识别多点触控手势(如scale),只关心单个手指手势,在处理的时候,仍要考虑到多点的逻辑。比如说translate时,如果不考虑多点,那么当另外一个手指触摸了屏幕,产生了ACTION_MOVE事件,但它的坐标跟最初产生事件的Pointer差距很远,那么如果不做排除,就可能产生瞬间漂移。
加强版的单触控点手势
对于前面提到的单触控点手势(单手指就能识别的手势)如Translate和Rotate,其实都需要加强一下逻辑,以防止多触控点产生的干扰。
加强版本的单触控点手势处理:
primaryPointerId = INVALIDE_POINTER_ID;
onTouchEvent(event) {
switch (event.getActionMasked()) {
case ACTION_DOWN:
primaryPointer = event.getPointerId(event.getActionIndex());
break;
case ACTION_MOVE:
pointerIndex = event.findPointerIndex(primaryPointerId);
x = event.getX(pointerIndex);
y = event.getY(pointerIndex);
be happy with x and y;
break;
case ACTION_UP:
case ACTION_CANCEL:
primaryIndex = INVALIDE_POINTER_ID;
break;
}
}
当然,这里也取决于具体的使用场景,假如允许切换触控点,比如先一个手指拖动,然后另外一个手指点进来,这时第一个手指离开了,如果想继续 拖动的话,就需要更换已保存的primaryPointer。这时会收到ACTION_POINTER_UP,需要在此做切换处理,继续 上面的代码片段,
secondPointer = INVALIDE_POINTER_ID;
case ACTION_POINTER_DOWN:
secondPointer = event.getPointerId(event.getActionIndex());
break;
case ACTION_POINTER_UP:
thisPointer = event.getPointerId(event.getActionIndex());
if (thisPointer == primaryPointer) {
primaryPointer = secondPointer;
}
secondPointer = INVALIDE_POINTER_ID;
break;
还有一点需要注意的是,不能简单的只用getPointerCount来作判断,就比如pointer 1先来,然后pointer 2来了,pointer 1又离开了,这时pointerCount仍是1,但是pointer已变化 了,事件的位置就变了,如果不按上述方法处理,将会发生跳变。
缩放(Zoom/Scale)
缩放手势是多点触控的一个非常典型的应用,因为单手无法做出比较合理的手势判断。SDK当中提供了一个用于识别缩放的手势识别器ScaleGestureDetector,它的使用方法与GestureDetector一样,创建对象,塞MotionEvent进去,然后注册listener即可。
但如果,用单独的detector不是很方便,比如已经自己实现了一套手势识别逻辑,现在只想加上Scale,或者其他原因不方便引入ScaleGestureDetector,那么就得自己去做了,也并不是很复杂。
主要思路就是,收集齐两个触控点,记录它们初始的位置,计算它们之间初始的距离,在ACTION_MOVE时,再计算新的距离,新旧距离之比既可当作缩放的比例:
primaryPointer = INVALIDE_POINTER_ID;
secondPointer = INVALIDE_POINTER_ID;
initialSpan = -1;
startPoint = null;
onTouchEvent(event) {
case ACTION_DOWN:
index = event.getActionIndex();
primaryPointer = event.getPointerId(index);
startPoint = Point(event.getX(index), event.getY(index));
break;
case ACTION_POINTER_DOWN:
index = event.getActionIndex();
secondPointer = event.getPointerId(index);
sp = Point(event.getX(index), event.getY(index));
initialSpan = distance(startPoint, sp);
break;
case ACTION_MOVE:
if (event.getPointerCount() > 1) {
primaryIndex = event.findPointerIndex(primaryPointer);
pp = Point(event.getX(primaryIndex), event.getY(primaryIndex));
secondIndex = event.findPointerIndex(secondPointer);
sp = Point(event.getX(secondIndex), event.getY(secondIndex));
thisDistance = distance(pp, sp);
if (thisDistance > ScaledSpan) {
scale = thisDistance / initialSpan;
be happy with scale;
}
}
break;
case ACTION_UP:
case ACTION_CANCEL:
case ACTION_POINTER_UP:
thisPointer = event.getPointerId(event.getActionIndex());
if (thisPointer == primaryPointer) {
primaryPointer = INVALIDE_POINTER_ID;
} else if (thisPointer == seocndPointer) {
secondPointer = INVALIDE_POINTER_ID;
}
break;
}
当然 ,还可以加一些阈值判断,比如当distance大于getScaledTouchSlop,才触发使用scale的逻辑。
参考资料
- Detecting gestures on Android via GestureDetector
- Handle multi-touch gestures
- Drag and scale
- Drag and drop
- MotionEvent
- Gestures and Touch Events
- android-gesture-detectors
- SwipeBackLayout
- GestureViews
- Sensey