多手指触控,其实也不是很难

多点触控,一直以来都是事件处理中比较晦涩的一个话题。其一是由于它的机制与咱们常规思惟有点不一样,基二是由于咱们用的比较少。那么做为一个有点追求的Android开发者,咱们必需要掌握这些,这样能够提升代码的逼格。java

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

  1. 讲解多手指触摸的一些关键性概念。虽然这部分概念很是抽象,而且也没法用源码去解释(源码在底层),可是这部分概念是最关键的。若是你想掌握多点触控,必须理解并记住这些概念。
  2. 讲解多手指触摸事件在ViewGroup是如何分发处理的。由于只有理解了这个,咱们才能写出正确的多手指触摸事件的代码。
  3. 经过一个例子讲解如何在滑动控件中支持多手指滑动。

好了,废话很少说了,让咱们开始此次激情的旅程吧。github

触摸事件

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

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

事件类型

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

那么,为何你们一直说MotionEvent.getAction()返回的就是事件类型呢?由于这是一个巧合,对于单手指操做,MotionEvent.getAction()的返回值中,高八位的索引值是0,所以它正好与事件类型的值同样。spa

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

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

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

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

  1. 当第一个手指按下的时候,此时触发的事件类型是ACTION_DOWN
  2. 当有第二个,甚至更多的手指按下的时候就会触发ACTION_POINTER_DOWN事件。
  3. 当任意一个手指滑动的时候,就会触发ACTION_MOVE事件。
  4. 当不是最后一个手指抬起时,会触发ACTION_POINTER_UP事件。
  5. 当最后一个手指择时,会触发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事件分发和处理源码分析

支持多手指的滑动控件

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

在实现这个功能以前,咱们要明确实现思路

  1. 只有主手指能控制控件的滑动。
  2. 若是有手指按下,就认为这个手指是主手指。
  3. 当有手指抬起时,若是是主手指,那就必须从新找一个手指做为新的主手指。

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

咱们前面说过,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地址,供你们参考。

相关文章
相关标签/搜索