Android开发多手指触控事件处理

正文

多点触控,一直以来都是事件处理中比较晦涩的一个话题。其一是因为它的机制与我们常规思维有点不同,基二是因为我们用的比较少。那么作为一个有点追求的Android开发者,我们必须要掌握这些,这样可以提高代码的格调。

写这篇文章还是有点难度的,我反反复复修改了好多次,真的是删了又改,改了又删,只为把多点触控讲得明明白白。最后我决定把本文分为三部分进行讲解

  • 讲解多手指触摸的一些关键性概念。虽然这部分概念非常抽象,并且也无法用源码去解释(源码在底层),但是这部分概念是最关键的。如果你想掌握多点触控,必须理解并记住这些概念。
  • 讲解多手指触摸事件在ViewGroup是如何分发处理的。因为只有理解了这个,我们才能写出正确的多手指触摸事件的代码。
  • 通过一个例子讲解如何在滑动控件中支持多手指滑动。

好了,废话不多说了,让我们开始这次愉快的旅程吧。

触摸事件

首先我们从MotionEvent.getAction()讲起吧。很多地方把这个方法的返回值叫做触摸事件的类型,其实这个叫法是错误的,它的返回值不仅包含事件的类型,还包含手指的索引值。

假如MotionEvent.getAction()返回一个值,用十六进制表示为0X0100,这个值的高八位的值是01,用二进制表示就是0000 0001,它表示手指的索引,而低八位的值是00,用二进制表示就是0000 0000,它才表示事件的类型。

事件类型

那么我们怎么获取这个事件的类型呢,我想大家应该都想到了事件类型的掩码,MotionEvent.getActionMask()就是通过事件类型掩码获取事件类型的。

那么,为什么大家一直说MotionEvent.getAction()返回的就是事件类型呢?因为这是一个巧合,对于单手指操作,MotionEvent.getAction()的返回值中,高八位的索引值是0,因此它正好与事件类型的值一样。

对于支持多手指操作,MotionEvent.getAction()返回值的事件索引就不再一直是0了,它会随着手指的增加而改变,因此MotionEvent.getActionMask()才是返回事件类型的正确操作。

那么我们来看下,多手指触摸情况下所支持的事件类型

事件类型 事件说明
ACTION_DOWN 第一个手指按下
ACTION_POINTER_DOWN 其它手指按下
ACTION_MOVE 手指移动
ACTION_POINTER_UP 不是最后一个手指抬起
ACTION_UP 最后一个手指抬起

我们通过一个例子来解释下这几个事件的触发时机。

  • 当第一个手指按下的时候,此时触发的事件类型是ACTION_DOWN
  • 当有第二个,甚至更多的手指按下的时候就会触发ACTION_POINTER_DOWN事件。
  • 当任意一个手指滑动的时候,就会触发ACTION_MOVE事件。
  • 当不是最后一个手指抬起时,会触发ACTION_POINTER_UP事件。
  • 当最后一个手指择时,会触发ACTION_UP事件。

手指索引

MotionEvent.getAction()返回值中还有个神秘的手指索引,它可以通过MotionEvent.getActionIndex()获取。那么它有啥用呢?对于单手指,没有任何叼用,但是对于多手指,那它的作用就大了,这可以获取手指的触摸事件的信息,例如MotionEvent.getX(int pointerIndex)获取X坐标值。

手指ID

刚才在事件类型部分,不知大家有没有注意到,ACTION_MOVE是不区分手指的,那么我们怎么知道是哪个手指触发了ACTION_MOVE的呢?你是不是第一时间想到了手指索引?请你放弃这个想法!

人可以通过眼睛观察到手指的按下顺序,但是硬件和软件是无法做到的,而手指的索引在事件中可能会改变的。那么一个严峻的问题来了,如何跟踪一个手指呢?用PointerId!至于原理是什么,我也不太清楚。

那么怎么获取一个手指的PointerId呢?当遇到ACTION_DOWNACTION_POINTER_DOWN的时候,通过如下代码获取

// 获取手指的索引    
int pointerIndex = motionEvent.getActionIndex();
// 通过手指索引获取手指ID
int pointerId = motionEvent.getPointerId(pointerIndex);

在前面的手指索引部分,我们知道通过索引可能获取事件的信息,例如坐标值,如下代码

        // 获取手指索引
        int pointerIndex = event.getActionIndex();
        // 获取坐标值
        float x = event.getX(pointerIndex);
        float y = event.getY(pointerIndex);

然而在ACTION_MOVE事件中,我们要获取某个手指的坐标值,怎么办呢?首先我们要保存在ACTION_DOWNACTION_POINTER_DOWN中保存手指PointerId值,然后通过这个PointerId调用MotionEvent.findPointerIndex(int pointerId)获取手指索引值,最后通过索引值获取坐标值,代码如下

case MotionEvent.ACTION_MOVE:
    // 根据PointerId获取某个手指的索引    
    int pointerIndex = event.findPointerIndex(mPrimaryPointerId);
    // 获取坐标值
    float x = event.getX(pointerIndex);
    float y = event.getY(pointerIndex);
    break;

多手指事件处理

对于多手指触摸事件呢,其实比单手指只是多出了ACTION_POINTER_DOWNACTION_POINTER_UP两个事件,那么这两个事件在ViewGroup中是如何分发处理的呢?如果要用源码来分析呢,这篇文章的篇幅就太长了,但是呢,恰巧这两个事件与ACTION_MOVE的分发处理流程是一样的。如果你还不懂ACTION_MOVE是如何分发处理的,可以参考我之前写的ViewGroup事件分发和处理源码分析。

支持多手指的滑动控件

掌握了前面的基础知识后,我们现在就又到了喜闻乐见的实战环节,在这一部分,我们要使一个滑动控件支持多手指滑动。

在实现这个功能之前,我们要明确实现思路

  • 只有主手指能控制控件的滑动。
  • 如果有手指按下,就认为这个手指是主手指。
  • 当有手指抬起时,如果是主手指,那就必须重新找一个手指作为新的主手指。

首先我们需要一个可滑动的控件,这个控件取自手把手教你如何写事件处理的代码这篇文章的滑动控件,并且我需要大家对这篇文章的讲的事件处理能理解清楚,因为下面写的代码,我不会去解释这些基本知识。

我们前面说过,ACTION_POINTER_DOWNACTION_POINTER_UP的处理流程是和ACTION_MOVE一样的,那么要不要截断呢?那就要看当遇到这两个事件的时候我们要做什么。

根据实现思路中的第二条,如果有手指按下,就认为是主手指,因此在处理ACTION_POINTER_DOWN时候只是简单获取手指的PointerId,然后保存为主手指即可,所以不需要去截断。

根据实现思路的第三条,如果抬起的是主手指,那么就要重新找一个替代的手指作为主手指,所以也不需要去截断。

那么,在onInterceptTouchEvent()onTouchEvent()的处理方式是一样的,首先我们看下保存主手指的代码如下

    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                onPrimaryPointerDown(ev);
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                onPrimaryPointerDown(ev);
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_POINTER_DOWN:
                onPrimaryPointerDown(event);
                break;
        }
        return true;
    }
    /**
     * 当有新手指按下的时候,就认作是主手指,于是重新记录按下点的坐标,以及更新最新的X坐标。
     *
     * @param event 触摸事件。
     */
    private void onPrimaryPointerDown(MotionEvent event) {
        // 获取手指索引
        int pointerIndex = event.getActionIndex();
        // 通过手指索引获取手指ID
        mPrimaryPointerId = event.getPointerId(pointerIndex);
        // 通过手指索引保存坐标值
        mLastX = mStartX = event.getX(pointerIndex);
        mStartY = event.getY(pointerIndex);
    }    

然后,我们来看下当有主手指抬起时,如何寻找替代的主手指

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_POINTER_UP:
                onPrimaryPointerUp(ev);
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_POINTER_UP:
                onPrimaryPointerUp(event);
                break;
        }
        return true;
    }
    /**
     * 当主手指抬起时,寻找一个新的主手指,并且更新最新的X坐标值为新主手指的X坐标值。
     *
     * @param event
     */
    private void onPrimaryPointerUp(MotionEvent event) {
        // 获取抬起手指的索引值
        int pointerIndex = event.getActionIndex();
        // 通过索引值,获取抬起手指的ID
        int pointerId = event.getPointerId(pointerIndex);
        // 如果抬起手指的ID等于主手指的ID
        if (pointerId == mPrimaryPointerId) {
            // 寻找一个已经存在的手指索引
            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
            // 通过新的手指索引获取手指ID
            mPrimaryPointerId = event.getPointerId(newPointerIndex);
            // 通过新的手指索引获取坐标值
            mLastX = event.getX(newPointerIndex);
        }
    }    

把这些问题解决后,那么在处理滑动的代码的时候,就要通过这个主手指ID来获取坐标值,然后根据这些坐标值来决定滑动,我这里用部分代码来演示下

    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_MOVE:
                // 获取主手指的坐标值
                PointF primaryPointerPoint = getPrimaryPointerPoint(ev);
                // 根据坐标值判断是否需要滑动
                if (canScroll(primaryPointerPoint.x, primaryPointerPoint.y)) {
                    mBeingDragged = true;
                    getParent().requestDisallowInterceptTouchEvent(true);
                    // 执行一次滑动
                    performDrag(primaryPointerPoint.x);
                    mLastX = primaryPointerPoint.x;
                    // 可以滑动就截断事件
                    return true;
                }
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }
    /**
     * 获取主手指在某个事件触发时的坐标。
     *
     * @param event 触摸事件。
     * @return 如果成功,返回坐标点,否则返回null。
     */
    private PointF getPrimaryPointerPoint(MotionEvent event) {
        PointF pointF = null;
        if (mPrimaryPointerId != INVALID_POINTER_ID) {
            int pointerIndex = event.findPointerIndex(mPrimaryPointerId);
            if (pointerIndex != -1) {
                pointF = new PointF(event.getX(pointerIndex), event.getY(pointerIndex));
            }
        }
        return pointF;
    }    

总结

要掌握多手指滑动,必须先得掌握其关键的概念,有了这些概念我们就可以知道事件何时触发,怎么跟踪一个手指。然后我们需要掌握多手指事件的处理流程,巧合的是,只要知道ACTION_MOVE的处理流程就明白了多手指事件的流程。最后我们要掌握为一个滑动控件添加多手指支持的实现思路。

有了这三步,基本上就可以实现一个支持多手指滑动的控件。不过请注意我的措辞,是基本上,是基本上,是基本上!

最后,我默默地留下一个github地址,供大家参考。

以上就是Android开发多手指触控事件处理的详细内容,更多关于Android多手指触控的资料请关注脚本之家其它相关文章!

你可能感兴趣的:(Android开发多手指触控事件处理)