android开发游记:多点触控解析与运用

原创 2016年05月30日 14:31:13

andorid 自2.0以后加入了对多点触控的支持,而多点触控的使用,在多数应用中是用不到的,或者用不用区别不大,但是在某些需要拖拽的控件中加入多点触控的支持会有更好的用户体验,最常见的比如下拉刷新,美团、京东、百度糯米、阿里旅行等应用都没有处理多点触控的情况,因此在拉动的时候用2只手连续拉动就会出现页面闪动的情况。毕竟只有少数闲得蛋疼的用户才会这样去拖拽,处不处理区别不大,但是在一些更加复杂的拖拽场合中多点触控的处理就很有必要了,比如qq的下拉抢红包。带着学习的态度,还是很有必要掌握的。

我们在触摸事件中可以得到MotionEvent对象,
先介绍MotionEvent的几个常用方法:

获取当前屏幕触摸点个数

getPointerCount()
这个方法可以获取触摸点个数,一般来说处理两个点就行了,3点的情况实在太少

获取的触摸状态

getActionMasked()

这个方法用于获取触摸事件的触摸状态,我们平时使用的getAction()方法是无法捕获多点触控的情况的,使用getActionMasked()就额外可以捕获到ACTION_POINTER_DOWNACTION_POINTER_UP事件

多点触控的特殊事件

MotionEvent.ACTION_POINTER_DOWN:
当屏幕上已经有一个点被按住,此时再按下其他点时触发。
MotionEvent.ACTION_POINTER_UP:
当屏幕上有多个点被按住,松开其中一个点时触发(即非最后一个点被放开时)。

每个触点分类处理

对触点进行分类处理是多点触控的核心思想,这里提供了3个方法帮助我们获取我们想要的触点:

//获取当前触点的index下标
getActionIndex() 
//获取触点唯一ID
getPointerId() 
//通过触点的ID获取index下标
findPointerIndex() 

我们得到的MotionEvent对象维护了一个有序的触点序列,这个序列记录了当前屏幕中的所有触点,我们可以通过index下标获取指定触点的信息,比如MotionEvent.getX()可以获取触摸点的X坐标,它还提供了一个> 重载方法MotionEvent.getX(index),获取指定触点的X坐标。

而这个有序序列的位置并不是不变的,根据用户触摸的顺序,序列的顺序是不断变化的,也就是说你使用一个下标在同一个触摸事件中两次获取到的不一定是同一个触点,为了解决这个问题系统为每个触点关联了唯一的ID,而这个ID是固定不变的,我们获取指定触点的思路就变成了 :1.使用ID获取index,2.再用index获取触点信息

如下通过一个ID获取指定触点的XY坐标:

//MotionEvent ev
int pointerIndex = ev.findPointerIndex(ev, mActivePointerId);
float x = ev.getX(pointerIndex);
float y = ev.getY(pointerIndex);

上面的代码还可以写成:

//MotionEvent ev
int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
float x = MotionEventCompat.getX(ev, pointerIndex);
float y = MotionEventCompat.getY(ev, pointerIndex);

MotionEventCompat是v4中引入的一个兼容低版本(API 8)的类,用于兼容低版本的MotionEvent,使用方法一> 致,后面的介绍都统一使用MotionEventCompat

示例

为了更好地理解多点触控原理,我们来做一个例子:自定义一个View可以双手连续拖拽,松开后弹回到原位

效果图:(注意这是两只手连续拖拽)

这里写图片描述

附上下载地址: demo下载地址

首先创建一个TestView继承自View:

    public class TestView extends View{
        public TestView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    }

重写dispatchTouchEvent,在里面调用一个用于处理多点触控的自定义方法dealMulTouchEvent

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        dealMulTouchEvent(event);
        return super.dispatchTouchEvent(event);
    }

多点触控分类处理

使用getActionMasked获取触摸事件进行分类处理:

    public void dealMulTouchEvent(MotionEvent ev) {
        final int action = MotionEventCompat.getActionMasked(ev);
        switch (action) {
            case MotionEvent.ACTION_DOWN: 
                break;
            case MotionEvent.ACTION_MOVE: 
                break;
            case MotionEvent.ACTION_UP:
                break;
            case MotionEvent.ACTION_CANCEL:  
                break;
            case MotionEvent.ACTION_POINTER_DOWN: 
                break;
            case MotionEvent.ACTION_POINTER_UP: 
                break;
        }
    }

首先,在ACTION_DOWN的时候(第一只手按下),我们获取触点的x,y坐标,保存在mLastX和mLastY中,并获取当前触点ID,记作活动点,保存在一个类变量中:

    case MotionEvent.ACTION_DOWN: {
         final int pointerIndex = MotionEventCompat.getActionIndex(ev);
         final float x = MotionEventCompat.getX(ev, pointerIndex);
         final float y = MotionEventCompat.getY(ev, pointerIndex);
         // 记录开始拖拽时的坐标 (按下的坐标)
         mLastX = x;
         mLastY = y;
         // 记录当前触摸点的ID(活动点的ID)
         mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
         break;
    }

接着,在ACTION_MOVE的时候,我们通过活动点ID获取活动点的坐标,与上一次的坐标mLastX、mLastY进行差运算,计算横纵轴的拖拽距离dx、dy:

    case MotionEvent.ACTION_MOVE: {
         // 获取活动点坐标
         final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
         final float x = MotionEventCompat.getX(ev, pointerIndex);
         final float y = MotionEventCompat.getY(ev, pointerIndex);
         // 计算拖拽距离
         dy = y - mLastY;
         dx = x - mLastX;
         // 更新上次保存的坐标
         mLastX = x;
         mLastY = y;
         break;
    }

当用户另一只手再次按下时(触发第二个触点),触发ACTION_POINTER_DOWN事件,如果触发的点不是活动点的话,就更新mLastX 、mLastY,并设其为活动点:

    case MotionEvent.ACTION_POINTER_DOWN: {
        //获取再次按下的触点的ID
        final int pointerIndex = MotionEventCompat.getActionIndex(ev);
        final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
        //如果触发的点不是活动点的话,就更新mLastX/mLastY,并设其为活动点
        if (pointerId != mActivePointerId) {
            mLastX = MotionEventCompat.getX(ev, pointerIndex);
            mLastY = MotionEventCompat.getY(ev, pointerIndex);
            mActivePointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
        }
        break;
    }

当用户两只手都在触摸事件中,这时松开其中一种手(不管是不是活动点的那一只手),触发ACTION_POINTER_UP事件,判断,如果松开的手是活动点的那一只手,就更新mLastX 、mLastY,并把另一触点设为活动点:

    case MotionEvent.ACTION_POINTER_UP: {
        //获取松开的触点的ID
        final int pointerIndex = MotionEventCompat.getActionIndex(ev);
        final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
        //如果松开的是活动点,则把另一个点设为活动点,并更新mLastX/mLastY
        if (pointerId == mActivePointerId) {
            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
            mLastX = MotionEventCompat.getX(ev, newPointerIndex);
            mLastY = MotionEventCompat.getY(ev, newPointerIndex);
            mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
        }
        break;
    }

当用户松开另一只手时(此时只有一只手再触摸),触发ACTION_UP,我们清理活动点,设置为“无”
ACTION_CANCEL 也一同处理

    case MotionEvent.ACTION_UP:
    case MotionEvent.ACTION_CANCEL:
         mActivePointerId = MotionEvent.INVALID_POINTER_ID;
         break;

以上就是多点触控的核心代码了。

总结一下思路:
在ACTION_DOWN中设置初始的活动点和位置信息,在多点按下和多点松开的事件ACTION_POINTER_DOWN、ACTION_POINTER_UP中根据用户按下和松开的情况,更新活动点和位置信息,始终保证最后按下和最后松开的点为活动点,在ACTION_MOVE中始终获取活动点的位置信息来计算拖拽距离,最后在ACTION_UP和ACTION_CANCEL中清除活动点。

剩下的就是拖拽和复位的功能:

重新onTouchEvent,在ACTION_MOVE中进行拖拽,在ACTION_UP和ACTION_CANCEL进行复位:

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE:
                doMove();
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                resetPosition();
                break;
        }
        return true;
    }

拖拽和复位

最后给出拖拽和复位的方法,拖拽是通过layout方法来进行位移,复位则是利用位移动画来完成,没什么难点,不作介绍了。

    //拖拽方法
    private void doMove(){
        if (mRect.isEmpty()) {
            mRect.set(getLeft(), getTop(), getRight(), getBottom());
        }
        int movedx = (int) (dx/2);
        int movedy = (int) (dy/2);
        if (movedy!=0) {
            layout(getLeft() + movedx, getTop() + movedy, getLeft()+getWidth()+movedx, getTop() + getHeight() + movedy);
        }
    }

    //复位方法
    private void resetPosition() {
        Animation animation = new TranslateAnimation(getLeft()-mRect.left, 0, getTop()-mRect.top,0);
        animation.setDuration(200);
        animation.setFillAfter(true);
        startAnimation(animation);
        layout(mRect.left, mRect.top, mRect.right, mRect.bottom);
        mRect.setEmpty();
    }

下载

最后附上demo下载地址:

demo下载地址

版权声明:本文为博主原创文章,未经博主允许不得转载。

相关文章推荐

Android解决RecyclerView嵌套RecyclerView滑动卡顿

Android 利用RecyclerView仿淘宝订单页面实现,解决RecyclerView嵌套RecyclerView滑动卡顿 不上图,就是浪费感情 问题: 最近在项目中碰到一个问题,类似...

RecyclerView--实现 ListView,GridView,瀑布流 效果

RecyclerView 是Google推出的最新的 替代ListView、GridView的组件,RecyclerView是用来显示大量数据的容器,并通过有限数量的子View,来提高滚动时的性能。 ...

Recycleview实现复杂页面 三种以上布局 瀑布流 多布局 scrollview嵌套recyclerView 显示不全 滑动冲突 之进阶终极篇

Recycleview实现复杂页面 三种以上布局 瀑布流 多布局 scrollview嵌套recyclerView 显示不全 滑动冲突 之进阶终极篇

android开发游记:RecycleView 实现复杂首页布局三种方式

做过电商类应用的朋友可能都会遇到一个比较头疼的问题:复杂的首页布局如何实现。参考百度糯米,美团,bilibili等应用,都会发现其首页的布局相对复杂,例如下图bilibili的首页(第二张是demo实...

android开发游记:RecyclerView无法添加onItemClickListener最佳的高效解决方案

自从RecyclerView发布以来,由于其高度的可交互性被广泛使用。但是RecyclerView确没有像ListView一样提供onItemClickListener却让人比较难过,网上搜索了一番有...

Android仿淘宝订单页面实现

一般电商项目会涉及到的订单管理模块,类似淘宝样式的订单主要是讲一下订单页面的实现。当然实现的方法有很多,我知道的有两种方法:一种是采用listview嵌套listview的方式,这种方式需要重写lis...

RecycleView嵌套RecycleView

思路:RecycleView嵌套一个RecycleView首先要确定一个rootRecycleView这个rootRecycleView是要包含多个子RecycleView,在写rootRecycle...
  • lzjqcc
  • lzjqcc
  • 2016-11-16 23:06
  • 1938
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)