多点触摸基本概念

导言

Android支持多点触摸事件,平时比较常见的可能就是放大和缩小手势,其次常见的可能就是自定义一些滑动视图,为了避免一些“意外”出现(比方说瞬移),还是考虑一下多点触摸

基本概念

Android对于事件的概念都封装在了MotionEvent这个类中,首先了解一些基本的信息

public static final int ACTION_MASK             = 0xff;
public static final int ACTION_POINTER_INDEX_MASK  = 0xff00;
public static final int ACTION_POINTER_INDEX_SHIFT = 8;

public final int getAction() {
    return nativeGetAction(mNativePtr);
}

public final int getActionMasked() {
    return nativeGetAction(mNativePtr) & ACTION_MASK;
}

public final int getActionIndex() {
    return (nativeGetAction(mNativePtr) & ACTION_POINTER_INDEX_MASK)
                >> ACTION_POINTER_INDEX_SHIFT;
}

对比上面的三个方法,举个例子看看:
getAction()本身返回的是一个32位的int,即
0000 0001 0000 0011
getActionMasked()进行了低8位与运算,从而结果只会剩下低8位
0000 0000 0000 0011
getActionIndex()进行了高8位与运算,然后再右移8位,相当于把高8位移到低8位,然后高8位填0
0000 0001 0000 0000
0000 0000 0000 0001

从上述计算可以看出,实际上getAction()返回的是ActionIndex和事件类型的组合
ActionIndex:
取值为0、1、2...N,在手指的移出后Index还会进行自动调整,始终保持0、1...手指数量-1这个序列,也就是说index=0只能标识有一个手指,但是不能说明这个手指一定是第一次按下的手指,同样也可以是从两根手指的情况下,松开第一次按下的手指从而剩下的那个手指
对应于这个还有一个概念

public final int getPointerId(int pointerIndex) {
    return nativeGetPointerId(mNativePtr, pointerIndex);
}

pointerId:
区别于ActionIndex,pointerId能够做到保证同一手指的值不变,也就是说可以通过pointerId来标识具体的手指

小结

方法 描述
getAction() 获得ActionIndex和事件类型组合的值,当且仅当只有一个手指,此时获得的是事件类型,那么也就不能处理多点触摸
getActionMasked() 获得事件类型
getActionIndex() 获得事件Index

个人来说,推荐在OnTouchEvent中使用getActionMasked()来判断事件类型,这样才是符合MotionEvent的设计原理

多点触摸实践

我们知道处理触摸事件的逻辑是在onTouchEvent(MotionEvent event)中处理
也就是说每一次只能处理单独的一个MotionEvent事件
这里考虑一个滑动视图,比方说
https://github.com/dda135/PullRefreshLayout这一个布局刷新自定义控件

首先明确几个概念:
1.滑动的时候拦截事件的可能是任何一根手指
2.滑动以最后一个按下来的手指为准
3.最后一个滑动的手指松开之后,后续的滑动应该尽量以这个手指之前按下的最后一个手指为准,如果这个手指之前已经松开,那么再往前追溯

概念明确之后,实现思路也就清晰了:
1.应该记录当前最后一个手指的pointerId
2.应该提供一个列表存储按照手指按下的顺序存储pointerId

private static final int INVALID_POINTID = -1;
//用于记录上一次事件的坐标
private Point mLastPoint;
//用于记录当前最后一个手指的pointerId
private int mActivePointId;
//用于按顺序存储按下手指的pointerId
private List<Integer> mPointIdList = new ArrayList<>();

@Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        ...
        int pointerIndex = event.getActionIndex();
        int pointerId = event.getPointerId(pointerIndex);
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_POINTER_DOWN:
                mActivePointId = pointerId;
                mLastPoint.set((int) event.getX(pointerIndex), (int) event.getY(pointerIndex));
                mPointIdList.add(pointerId);
                break;
            case MotionEvent.ACTION_MOVE:
                int activeIndex = event.findPointerIndex(mActivePointId);
                if(activeIndex < 0){
                    return false;
                }
                int x = (int) event.getX(activeIndex);
                int y = (int) event.getY(activeIndex);
                int deltaY = (y - mLastPoint.y);
                int dy = Math.abs(deltaY);
                int dx = Math.abs(x - mLastPoint.x);     
                if (dy > mTouchSlop && dy >= dx) {
                    canUp = mOption.canUpToDown();
                    canDown = mOption.canDownToUp();
                    canUpIntercept = (deltaY > 0 && canUp);
                    canDownIntercept = (deltaY < 0 && canDown);
                    return canUpIntercept || canDownIntercept;
                }
                return false;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mActivePointId = INVALID_POINTID;
                mPointIdList.clear();
                break;
            case MotionEvent.ACTION_POINTER_UP:
                resetTouch(event);
                break;
            default:
                break;
        }
        return false;
    }

    /**
     * 通过列表结构进行手指回溯操作
     * @param event 当前事件对象
     */
    private void resetTouch(MotionEvent event){
        int currentReleaseIndex = event.getActionIndex();
        int currentReleaseId = event.getPointerId(currentReleaseIndex);
        mPointIdList.remove((Integer) currentReleaseId);//先移除当前松开的手指
        while(mPointIdList.size() > 0){
            mActivePointId = mPointIdList.get(mPointIdList.size() - 1);
            int pointIndex = event.findPointerIndex(mActivePointId);
            if (pointIndex < 0){//当前Id无效,废弃
                mPointIdList.remove(mPointIdList.size() - 1);
                continue;
            }
            mLastPoint.set((int)event.getX(pointIndex),(int)event.getY(pointIndex));//当前活动手指变化,记录当前活动的最新坐标
            return;
        }
        mActivePointId = INVALID_POINTID;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        ...
        int pointIndex = event.getActionIndex();
        int pointId = event.getPointerId(pointIndex);
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_POINTER_DOWN:
                mActivePointId = pointId;
                mLastPoint.set((int)event.getX(pointIndex),(int)event.getY(pointIndex));
                mPointIdList.add(mActivePointId);
                break;
            case MotionEvent.ACTION_MOVE:
                int activePointIndex = event.findPointerIndex(mActivePointId);
                if(activePointIndex < 0){
                    return false;
                }
                isOnTouch = true;
                updatePos((int) (mOption.getMoveRatio() * (event.getY(activePointIndex) - mLastPoint.y)));
                mLastPoint.set((int)event.getX(activePointIndex),(int)event.getY(activePointIndex));
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                mActivePointId = INVALID_POINTID;
                mPointIdList.clear();
                ...
                break;
            case MotionEvent.ACTION_POINTER_UP:
                resetTouch(event);
                break;
            default:
                break;
        }
        return true;
    }

实际上思路很简单,当手指按下的时候(单指或多指),标记当前手指为活动手指,MOVE事件的时候以活动手指的移动为准,当有手指松开的时候,尝试寻找新的活动手指即可。

总结

Android源码的一些例子可以参考RecyclerView等滑动视图的源码,里面也有一些类似的操作,多点触摸实际上和单点触摸差不多,只要明确基本概念,处理起来也会相对轻松

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351

推荐阅读更多精彩内容

  • 开篇 最近在研究自定义View方面的知识。而自定义View中很重要的一块就是View的交互。这就牵涉到本系列文章要...
    张利强阅读 10,219评论 2 17
  • View的事件分发机制 View的事件分发机制简单来说就是将用户与手机屏幕的交互事件交由正确的控件进行处理,从而可...
    蕉下孤客阅读 850评论 0 4
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,907评论 25 707
  • 一、 Android分发机制概述: Android如此受欢迎,就在于其优秀的交互性,这其中,Android优秀...
    IT枫阅读 2,431评论 2 9
  • 先有电闪 还是先有雷鸣 我一直都没有弄清 隔着窗玻璃 看半空的金光四射 轰隆隆 轰隆隆 雨洒漫天 飞落弹起屋檐 ...
    14432c3ec397阅读 273评论 8 7