登录

去注册

登录

注册

去登录

注册

每日一问 你那么多手指在触摸屏幕,你叫我怎么选?

xiaoyang   2019-11-08   收藏

在早期,非常多博客在讲解和控件交互的时候,只会关注:

ACTION_DOWN , ACTION_MOVE , ACTION_UP, ACTION_CANCEL

这样的控件在一个手指交互的时候基本没有问题,但是一旦两个手指甚至多指操作,一个支持上下滑动的控件就会有跳跃感。

那么今天的问题是:

  1. 支持多个手指以上的操作,还应该关注哪些事件?
  2. Google 官方的控件,比如 ScrollView,ViewPager 这些都是支持多指操作的,那么多个手指时,如何判断哪一个是 active pointer(需要考虑一个接一个按下;一个接一个抬起)。
  3. 一个未支持多指的控件,如何快速的支持?

以上问题,知道任意一个都可以回答。

另外,我们的问答数量已经突破了 50+,现在已经独立为 tab 啦,抬头即可看间。
本站始终追求非常高质量的提问,保证大多数问题能寻找答案的伙伴有所收获,么么哒,这个问题我觉得可以挂 5 天。

6

上个月看到有群友在群里说要去洗牙,我就问,洗牙会不会很伤牙龈,牙齿之类的呀? 后来百度了解了一下才知道洗牙对牙齿的损害几乎是可以忽略不计的,而且洗牙带来的好处也有很多。所以今天就请假去洗牙了。。。洗完之后觉得挺舒服的,感觉良好。


之前做过一个小游戏,里面就有支持多指操作的需求。

那么,要支持多指操作,应该关注哪些事件?

在处理触摸事件的时候,除了基本的DOWNMOVEUPCANCEL之外,还需要关注POINTER_DOWNPOINTER_UP这两个ACTION。

什么情况下的ACTION会是这两个呢?
先看一下它的文档描述:

    /**
     * A non-primary pointer has gone down.
     */
    public static final int ACTION_POINTER_DOWN     = 5;

    /**
     * A non-primary pointer has gone up.
     */
    public static final int ACTION_POINTER_UP       = 6;

emmm,也就是非主要的指针(手指)按下或抬起的意思了。
这个 “非主要” 怎么理解?
系统它是这样判定的:

  • 当有手指按下时,如果检测到已经有手指按下了,而且还没抬起,那么,这次按下的手指,就是非主要的;
  • 抬起也是同理:当检测到有手指抬起,如果此时还有别的手指没抬起,那么这次的抬起就是非主要的;

有一点要注意的就是,想要支持接收这两个ACTION,必须把event.getAction()改为event.getActionMasked(),当然了,也有event.getAction() & MotionEvent.ACTION_MASK`这种写法,它们的效果是一样的。


官方控件是怎么处理多指操作的呢?

就拿ScrollView来说吧。
它有一个叫mActivePointerId的东西,用来记录当前活跃的指针,没错,同一时间内就算多只手指在做滑动手势,控件也只会响应mActivePointerId所对应的指针坐标。
ScrollView如何管理mActivePointerId?
先看下它(精简后)的onInterceptTouchEvent方法:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        final int action = ev.getAction();
        switch (action & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN:
                //ACTION_DOWN的时候,证明现在还没有已按下且还没抬起的手指
                //所以当前活跃的指针ID就是索引值为0的指针ID
                mActivePointerId = ev.getPointerId(0);
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                //最后一根手指抬起或收到取消事件,则重置当前活跃的指针ID为无效
                mActivePointerId = INVALID_POINTER;
                break;
            case MotionEvent.ACTION_POINTER_UP:
                //非主要手指抬起
                onSecondaryPointerUp(ev);
                break;
        }
    }

    private void onSecondaryPointerUp(MotionEvent ev) {
        final int pointerIndex = ev.getActionIndex();
        final int pointerId = ev.getPointerId(pointerIndex);
        //如果抬起的那根手指,刚好是当前活跃的手指,那么
        if (pointerId == mActivePointerId) {
            //另选一根手指,并把它标记为活跃(皇帝驾崩,太子登基)
            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
            mActivePointerId = ev.getPointerId(newPointerIndex);
            //把上一次记录的坐标,更新为新手指的当前坐标
            mLastMotionX = ev.getX(newPointerIndex);
            mLastMotionY = ev.getY(newPointerIndex);
        }
    }

可以看到:

  • 当第一根手指按下时,会把它记录下来。

  • 最后一根抬起时,会把mActivePointerId重置为无效。

  • 非主要手指抬起时,会先判断是不是当前活跃的手指抬起,如果不是的话,无需理会。如果是的话,就要重新指定另外一根手指作为活跃指针,并把原来的坐标改为新的活跃手指的坐标。(这样的话,即使活跃手指抬起了,也能无缝转换到另外一根手指上)

好,现在来看看onTouchEvent方法:

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        final int actionMasked = ev.getActionMasked();
        switch (actionMasked) {
            case MotionEvent.ACTION_DOWN:
                //只有一只手指按下,常规方法获取坐标
                mLastMotionY = (int) ev.getY();
                //记录当前活跃指针ID
                mActivePointerId = ev.getPointerId(0);
                break;
            case MotionEvent.ACTION_MOVE:
                //根据之前记录的活跃指针ID来获取这个指针的索引
                final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
                //索引无效
                if (activePointerIndex == -1) {
                    break;
                }
                //根据对应的索引来获取坐标值
                final int y = (int) ev.getY(activePointerIndex);

                ......

                //记录上一次的坐标值
                mLastMotionY = y;
                break;
            case MotionEvent.ACTION_UP:
                //标记为无效
                mActivePointerId = INVALID_POINTER;
                break;
            case MotionEvent.ACTION_CANCEL:
                //标记为无效
                mActivePointerId = INVALID_POINTER;
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                //如果有新的手指按下,就直接把它当作当前活跃的指针
                final int index = ev.getActionIndex();
                mActivePointerId = ev.getPointerId(index);
                //并且刷新上一次记录的旧坐标值
                mLastMotionY = (int) ev.getY(index);
                break;
            case MotionEvent.ACTION_POINTER_UP:
                //调用刚刚的方法来处理非主要手指抬起事件
                onSecondaryPointerUp(ev);
                break;
        }
    }

大致逻辑也跟onInterceptTouchEvent差不多,只是多了ACTION_MOVEACTION_POINTER_DOWN的处理。
可以看到,ACTION_MOVE的时候,获取事件坐标点(它这里只获取了y坐标没有x,是因为ScrollView只需要处理垂直滑动)时,并不是使用event.getX()/event.getY(),而是调用了一个有参数的getY(int pointerIndex) 方法,这个方法会返回指定指针索引所对应的触摸点坐标。
ACTION_POINTER_DOWN的时候,会直接拿这个新按下的非主要手指来当作当前活跃的手指,并且更新坐标值。

好,来捋一下总体的流程:

  1. ACTION_DOWN时(此前没有手指按下且没抬起),直接拿当前指针ID当作活跃ID(情窦初开)

  2. ACTION_POINTER_DOWN时,又把新按下的手指当作活跃ID(喜新厌旧)

  3. ACTION_MOVE时,会根据记录的活跃ID,来获取到对应的触摸点坐标;

  4. ACTION_POINTER_UP时,如果刚好抬起的是当前活跃的手指,则指定另一根未抬起的手指当作活跃手指(李代桃僵)

  5. ACTION_UP时,证明最后一根手指已抬起,重置活跃ID为无效;

  6. ACTION_CANCEL,收到取消动作,处理方式和ACTION_UP一样;


如何快速支持多指操作?

看了下@找瓶子的汤猿同学提到的GestureDetector的源码,发现已经是支持了多指操作的,直接使用就行了,这是成本最低的方法;
还有就是,仿照ScrollView写一个咯,跟着上面分析过的流程来做,没有难度的。

回复
陈小缘 : @小名盼盼 

有关系,我们一般在进行多指触控的时候,都是两只手进行的(托手机那只手只有一只拇指是空闲的),如果不洗牙,久而久之牙齿就可能会有健康问题,比如说牙痛,一旦出现牙痛的时候,你就可能要用一只手去捂着,所以这  ...查看更多

2019-11-05 回复
小名盼盼 : @陈小缘 

所以洗牙和多指触控是没关系的吧~

2019-11-05 回复
陈小缘 : @鸿洋 

哈哈哈,我错了,有难度,有难度。。。

2019-11-05 回复
鸿洋 : @陈小缘 

哈哈 小缘这个没有难度的,刺痛了我...

2019-11-05 回复
3

尝试抛砖引玉哈

  • 1 还应该关注的事情
    多手指的操作方式:比如接力操作(比如一根手指往上划定的时候另一个手指按下继续滑动,同一时间只有最后按下的手指起作用,listview),协作(比如捏撑,多手指滚动之类,常规操作可以复写GestureDetector),互相不干扰(比如手写板,两根手指滑动就是两条线),决定了你后续的
  • 2 (多)手指(这里就写两个指头)按下会触发(重写onTouch注意要用event.getActionMasked()),以及index会复用,但是id不会(按下两个手指,id=0,1,index=0,1,,抬起第一个手指,id 1,index=0),所以要记录id,再用id找index确定手指移动
    列举上述接力操作(最新的手指起作用),其余操作也一样,注意确定ponterId就好了
    定义trackingPointerId

    1. action_down ():trackingPointerId=event.getPointerId(0);
    2. move:trackingPointerId = event.getPointerId(event.getActionIndex());
    3. ponter_down(第二个手指头):trackingPointerId = event.getPointerId(event.getActionIndex());
    4. move(任意一个手指触发):index = event.findPointerIndex(trackingPointerId),然后 event.getX(index)
    5. ponter_up(抬起一个手指,还有手指触摸view):判断离开的手指index然后找到剩余的index最大手指的id
    6. action_up(最后一根指头抬起)
  • 3 能想到的是常规操作可以复写GestureDetector来做

回复
0

P(x, y, index, id)
x, y 是坐标
index 是第几个手指, 这个值会变,会变。 作用:可以用来 遍历
id 不会变,每一个 point 都有一个唯一的 id

ACTION_DOWN, ACTION_UP, ACTION_POINT_DOWN, ACTION_POINT_UP,均可以用 even.getActionIndex() 获得 index。(event.getPointerId(indext) 获得 id, getX(index), getY(index) 获得 x, y)

 // getX() 就是 getX(0)
 *public final float getX() {
    return nativeGetAxisValue(mNativePtr, AXISX, 0, HISTORYCURRENT);

}

 public final float getX(int pointerIndex) {
    return nativeGetAxisValue(mNativePtr, AXISX, pointerIndex, HISTORYCURRENT);

}*

为何 ACTION_MOVE 不能获得 index?
能获取,但值是 0,当你移动一个手指时,你能保证另外的手指没有抖动么,那么如何才能得到用户认为的那个动的手指呢? 没办法, 所以返回 0

ACTION_POINT_DOWN, ACTION_POINT_UP 是第一手指之后的 DOWN 和 最后一个手指之前的 UP,用来支持多指 touch 的

要提的一点是,evnet.getAction() 不支持多指,event.getActionMasked() 才支持多点触摸,才会有 ACTION_POINT_DOWN, ACTION_POINT_UP

最后,便于理解,举个栗子
对于整个view的一系列事件
ACTION_DOWN P(x, y, 0, 0)
ACTION_MOVE P(x, y, 0, 0)
ACTION_MOVE P(x, y, 0, 0)
ACTION_MOVE P(x, y, 0, 0)
ACTION_POINT_DOWN P(x, y, 0, 0), P(x, y, 1, 1)
ACTION_MOVE P(x, y, 0, 0), P(x, y, 1, 1)
ACTION_MOVE P(x, y, 0, 0), P(x, y, 1, 1)
ACTION_POINT_UP P(x, y, 0, 0), P(x, y, 1, 1) // 抬起第一个手指
ACTION_MOVE P(x, y, 0, 1) // 第二个手指 index 变为 0, id 不变
ACTION_MOVE P(x, y, 0, 1)
ACTION_UP P(x, y, 0, 1)

原理理解了,项目中具体的逻辑就可以处理了

回复
xiang787777656@qq.com : @xiang787777656@qq.com 

接管型: 如图片跟随手指移动,首先会跟随第一个手指移动,当第二个手指按下时接管图片的控制权,随后图片跟随第二个手指移动。 实现:记录当前控制图片移动的手指 id ,ACTION_POINT_DOWN   ...查看更多

2019-11-08 回复
0

这个问题我觉得可以挂 5 天 这就是你懒得更新的理由吗

回复
鸿洋 : @DaveBoy 

哈哈,已经更新50多个啦,不少了噢~

2019-11-05 回复
0
  • 对于第一个问题,我觉得还需要考虑多指的不同作用,甚至考虑多指不同方向的滑动事件问题
  • 对于2,3技术问题只能看大佬的了QAQ
回复
菡萏香消翠叶残 : @陈小缘 

my dear dalao please daidaiwo

2019-11-05 回复
陈小缘 : @菡萏香消翠叶残 

我已计划做一个伪推送功能(轮询)的WanAndroid问答客户端,会周期性地抓取帐号回答过的问题(或关注的问题),并对比上次缓存的回复来实现(大概在下一篇文章写完之后开始)。  ...查看更多

2019-11-05 回复
菡萏香消翠叶残 : @鸿洋 

我突然觉得似乎缺少一个通知的功能,比如自己回复了一个问题或者回复了别人,要是自己忘了看,过几天都不知道自己回了那个了,现在问题不多还能找到,问题多了估计就找不到了,当然只是建议。  ...查看更多

2019-11-05 回复
鸿洋 : @菡萏香消翠叶残 

这个问题,就是常规控件哈,不是大家所理解的多指缩放什么的... 正常的下上滑动也应该支持多指~~

2019-11-04 回复

删除留言

确认删除留言,会导致相关评论丢失?

取消 确定