Android 多点触控

1.多点触控
多点触控 ( Multitouch,也称 Multi-touch ),即同时接受屏幕上多个点的人机交互操作,多点触控是从 Android 2.0 开始引入的功能,在 Android 2.2 时对这一部分进行了重新设计。

多点触控相关问题:
在引入多点触控之前,事件的类型很少,基本事件类型只有按下(down)、移动(move) 和 抬起(up),即便加上那些特殊的事件类型也只有几种而已,所以我们可以用几个常量来标记这些事件,在使用的时候使用 getAction() 方法来获取具体的事件,之后和这些常量进行对比就行了。
在 Android 2.0 版本的时候,开始引入多点触控技术,由于技术上并不成熟,硬件和驱动也跟不上,多数设备只能支持追踪两三个点而已,因此在设计 API 上采取了一种简单粗暴的方案,添加了几个常量用于多点触控的事件类型的判断。
①ACTION_POINTER_1_DOWN 第 2 个手指按下,已废弃,不推荐使用。
②ACTION_POINTER_2_DOWN 第 3 个手指按下,已废弃,不推荐使用。
③ACTION_POINTER_3_DOWN 第 4 个手指按下,已废弃,不推荐使用。
④ACTION_POINTER_1_UP 第 2 个手指抬起,已废弃,不推荐使用。
⑤ACTION_POINTER_2_UP 第 3 个手指抬起,已废弃,不推荐使用。
⑥ACTION_POINTER_3_UP 第 4 个手指抬起,已废弃,不推荐使用。

这些事件类型是用来判断非主要手指(第一个按下的称为主要手指)的按下和抬起,使用起来大概是这样子:
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_UP:
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_POINTER_1_DOWN:
break;
case MotionEvent.ACTION_POINTER_2_DOWN:
break;
case MotionEvent.ACTION_POINTER_3_DOWN:
break;
case MotionEvent.ACTION_POINTER_1_UP:
break;
case MotionEvent.ACTION_POINTER_2_UP:
break;
case MotionEvent.ACTION_POINTER_3_UP:
break;
}
看到这里可能会产生一些疑问?
(1)为什么没有 ACTION_POINTER_X_MOVE ?
在多指触控中所有的移动事件都是使用 ACTION_MOVE, 并没有追踪某一个手指的 move 事件类型。简单说,设备很难无歧义的实现单独追踪每一个手指。
要理解这个,首先要明白设备是如何识别多点触控的,设备没有眼睛,不能像我们人一样看到有几个手指(或者触控笔)在屏幕上。
目前大多数 Android 设备都是电容屏,它们感知触摸是利用手指(触控笔)与屏幕接触产生的微小电流变化,之后通过计算这些电流变化来得出具体的触摸位置,在多点触控中,当两个触摸点足够靠近时,设备实际上是无法分清这两个点的。因此当两个触摸点靠近(重合)后再分开,设备很可能就无法正确的追踪两个点了,所以也很难实现无歧义的追踪每一个点。
并且从软件上来说,事件的编号产生和复用也是一个大问题,例如下面的场景:
事件 手指数量 编号变化
一个手指按下(命名为A) 1 A手指的编号为0,id为0
一个手指按下(命名为B) 2 B手指的编号为1,id为1
A手指抬起 1 B手指编号变更为0,id不变为1
一个手指按下(命名为C) 2 C手指编号为0,id为0,B手指编号为1,id为1

注意观察上面编号和id的变化,有两个问题:
①B手指的编号变化了。
②A手指和C手指id是相同的(A手指抬起后,C手指按下替代了A手指)。

所以这就引出了一个问题:如果存在 ACTION_POINTER_X_MOVE,那么X应该用什么标志呢?编号会变化,id虽然不会变化,但id会被复用,例如A手指抬起后C手指按下,C手指复用了A手指的id。所以不论使用哪一个都不能保证唯一性。

当然了,解决问题最好的方式就是把问题抛出去,既然从硬件和软件上都不能保证唯一性和不变性,就不做区分了,因此所有的 move 事件都是 ACTION_MOVE, 具体是哪个手指产生的 move 用户可以结合其他事件(按下和抬起)来综合判断。

(2)超过4个手指怎么办?
2.0 兼容版,在2.2 之前的设计中,其提供的常量最多能判断四个手指的抬起和落下,当超过四个手指时怎么办呢?在 2.2 版本之前,由于没有 getActionMasked 方法,我们可以自己手动进行计算,例如下面这样 :
int action = event.getAction() & MotionEvent.ACTION_MASK;
int index = (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>MotionEvent.ACTION_POINTER_INDEX_SHIFT;
switch (action) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG,“第1个手指按下”);
break;
case MotionEvent.ACTION_UP:
Log.e(TAG,“最后1个手指抬起”);
break;
case MotionEvent.ACTION_POINTER_1_DOWN:
// 此时相当于 ACTION_POINTER_DOWN
Log.e(TAG,“第”+(index+1)+“个手指按下”);
break;
case MotionEvent.ACTION_POINTER_1_UP:
// 此时相当于 ACTION_POINTER_UP
Log.e(TAG,“第”+(index+1)+“个手指抬起”);
break;
}
在上面的例子中有几点比较关键:
①action 与 Index 的获得
Android中的事件一般用最后8位来表示事件类型,再往前8位来表示Index。例如多指触控的按下事件,其事件类型是 0x00000005, 其Index标志位是 0x00000005,随着更多的手指按下,其中变化的部分是 Index 标志位,最后两位是始终不变的,所以我们只要能将这两个分离开就行了。

取得事件类型(action):
int action = event.getAction() & MotionEvent.ACTION_MASK;
这个非常简单,ACTION_MASK=0x000000ff, 与 getAction() 进行按位与操作后保留最后8位内容(十六进制每一个字符转化为二进制是4位)。
例如:0x00000105 & 0x000000ff = 0x00000005

取得事件索引(index):
int index = (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>MotionEvent.ACTION_POINTER_INDEX_SHIFT;

ACTION_POINTER_INDEX_MASK = 0x0000ff00
ACTION_POINTER_INDEX_SHIFT = 8;
首先让 getAction() 与 ACTION_POINTER_INDEX_MASK 按位与之后,只保留 Index 那8位,之后再右移8位,最终就拿到了 Index 的真实数值。
例如:
0x00000105 & 0x0000ff00 = 0x00000100
0x00000100 » 8 = 0x00000001

②用 ACTION_POINTER_1_DOWN 代替 ACTION_POINTER_DOWN
这是因为在 2.0 版本的时候还没有 ACTION_POINTER_DOWN 的这个常量,但是它们两个点数值是相同的,都是 0x00000005,这个你可以查看官方文档或者源码,甚至你直接写 case 0x00000005 也行,抬起也是同理。

③只考虑兼容 2.2 以上的版本
当然了,如果你不需要兼容 2.0 版本,只需要兼容到 2.2 以上的话就很简单了,像下面这样:
int index = event.getActionIndex();
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
Log.e(TAG,“第1个手指按下”);
break;
case MotionEvent.ACTION_UP:
Log.e(TAG,“最后1个手指抬起”);
break;
case MotionEvent.ACTION_POINTER_DOWN:
Log.e(TAG,“第”+(index+1)+“个手指按下”);
break;
case MotionEvent.ACTION_POINTER_UP:
Log.e(TAG,“第”+(index+1)+“个手指抬起”);
break;
}

④index 和 pointId 的变化规则
在 2.2 版本以上,可以通过 getActionIndex() 轻松获取到事件的索引(Index),但是这个事件索引的变化还是有点意思的,Index 变化有以下几个特点:
(1)从 0 开始,自动增长。
(2)如果之前落下的手指抬起,后面手指的 Index 会随之减小。
(3)Index 变化趋向于第一次落下的数值(落下手指时,前面有空缺会优先填补空缺)。
(4)对 move 事件无效。

下面我们逐条解释一下具体含义。
(1)从 0 开始,自动增长。
手指按下 触发事件(数值)
第1个手指按下 ACTION_DOWN (0x00000000)
第2个手指按下 ACTION_POINTER_DOWN (0x00000105)
第3个手指按下 ACTION_POINTER_DOWN (0x00000205)
第4个手指按下 ACTION_POINTER_DOWN (0x00000305)
注意高8位的位置,数值随着手指按下而不断变大。

(2)如果之前落下的手指抬起,后面手指的 Index 会随之减小。
手指按下 触发事件(数值)
第1个手指按下 ACTION_DOWN (0x00000000)
第2个手指按下 ACTION_POINTER_DOWN (0x00000105)
第3个手指按下 ACTION_POINTER_DOWN (0x00000205)
第2个手指抬起 ACTION_POINTER_UP (0x00000106)
第3个手指抬起 ACTION_POINTER_UP (0x00000106)
注意最后两次触发的事件,它的 Index 都是 1,这样也比较容易解释,当原本的第 2 个手指抬起后,屏幕上就只剩下两个手指了,之前的第 3 个手指就变成了第 2 个,于是抬起时触发事件的 Index 为 1,即之前落下的手指抬起,后面手指的 Index 会随之减小。

(3)Index 变化趋向于第一次落下的数值(落下手指时,前面有空缺会优先填补空缺)。
这个就有点神奇了,通过上一条规则知道,某一个手指的 Index 可能会随着其他手指的抬起而变小,这次用 4 个手指测试一下Index的变化趋势。

  手指按下	               触发事件(数值)

第1个手指按下 ACTION_DOWN (0x00000000)
第2个手指按下 ACTION_POINTER_DOWN (0x00000105)
第3个手指按下 ACTION_POINTER_DOWN (0x00000205)
第2个手指抬起 ACTION_POINTER_UP (0x00000106)
第4个手指按下 ACTION_POINTER_DOWN (0x00000105)
第3个手指抬起 ACTION_POINTER_UP (0x00000206)

这个要和上一个对比着看,重点观察第 3 个手指所触发事件区别,在上一个示例中,随着第 2 个手指的抬起,第 3 个手指变化为第 2(01) 个,所以抬起时触发的是第 2 根手指的抬起事件。
但是,如果第 2 个手指抬起后,落在屏幕上另外一个手指会怎样?经过测试,发现另外落下的手指会替代之前第 2 个手指的位置,系统判定为 2(01),而不是顺延下去变成 3(02),并且原本第3个手指的index变为原来数值(02),但是如果继续落下其他的手指,数值则会顺延。
即手指抬起时的 Index 会趋向于和按下时相同,虽然在手指数量不足时,Index 会变小,但是当手指变多时,Index 会趋向于保持和按下时一样。

(4)对 move 事件无效。
这个也比较容易理解,我们所取得的 Index 属性实际上是从事件上分离下来的,但是 move 事件始终为 0x00000002,也就是说,在 move 时不论你移动哪个手指,使用 getActionIndex() 获取到的始终是数值 0。
既然 move 事件无法用事件索引(Index)区别,那么该如何区分 move 是那个手指发出的呢?这就要用到 pointId 了,pointId 和 index 最大的区别就是 pointId 是不变的,始终为第一次落下时生成的数值,不会受到其他手指抬起和落下的影响。

(5)pointId 与 index 的相同点:
①从 0 开始,自动增长。
②落下手指时优先填补空缺(填补之前抬起手指的编号)。
不同点:
Index 会变化,pointId 始终不变。

2.Move 相关事件
①actionIndex 与 pointerIndex
在 move 中无法取得 actionIndex 的,我们需要使用 pointerIndex 来获取更多的信息,例如某个手指的坐标:getX(int pointerIndex)、getY(int pointerIndex)。但是这个 pointerIndex 又是什么呢?和 actionIndex 有区别么?
实际上这个 pointerIndex 和 actionIndex 区别并不大,两者的数值是相同的,你可以认为 pointerIndex 是特地为 move 事件准备的 actionIndex。
②pointerIndex 与 pointerId
pointerIndex:用于获取具体事件,可能会随着其他手指的抬起和落下而变化
pointerId:用于识别手指,手指按下时产生,手指抬起时回收,期间始终不变

这两个数值使用以下两个方法相互转换:
getPointerId(int pointerIndex):获取一个指针(手指)的唯一标识符ID,在手指按下和抬起之间ID始终不变。
findPointerIndex(int pointerId):通过 pointerId 获取到当前状态下 pointIndex,之后通过 pointIndex 获取其他内容。

③ 遍历多点触控
比如遍历出多个手指的 move 事件:
switch (event.getActionMasked()) {
case MotionEvent.ACTION_MOVE:
for (int i = 0; i < event.getPointerCount(); i++){
Log.i(“TAG”, “pointerIndex=” + i + “, pointerId=”+event.getPointerId(i));
// TODO
}
}
通过遍历 pointerCount 获取到所有的 pointerIndex,同时通过 pointerIndex 来获取 pointerId,可以通过不同手指抬起和按下后移动来观察 pointerIndex 和 pointerId 的变化。

④在多点触控中追踪单个手指
要实现追踪单个手指还是有些麻烦的,需要同时使用上 actionIndex, pointerId 和 pointerIndex,例如,只追踪第2个手指,并画出其位置:
/**

  • 绘制出第二个手指第位置
    */
    public class MultiTouchTest extends CustomView {
    // 用于判断第2个手指是否存在
    boolean haveSecondPoint = false;
    // 记录第2个手指第位置
    PointF point = new PointF(0, 0);

    public MultiTouchTest(Context context) {
    this(context, null);
    }

    public MultiTouchTest(Context context, AttributeSet attrs) {
    super(context, attrs);
    mDeafultPaint.setAntiAlias(true); mDeafultPaint.setTextAlign(Paint.Align.CENTER);
    mDeafultPaint.setTextSize(30);
    }

    @override
    public boolean onTouchEvent(MotionEvent event) {
    int index = event.getActionIndex();
    switch (event.getActionMasked()) {
    case MotionEvent.ACTION_POINTER_DOWN:
    // 判断是否是第2个手指按下
    if (event.getPointerId(index)==1){
    haveSecondPoint = true;
    point.set(event.getY(), event.getX());
    }
    break;
    case MotionEvent.ACTION_POINTER_UP:
    // 判断抬起的手指是否是第2个
    if (event.getPointerId(index)==1){
    haveSecondPoint = false;
    point.set(0, 0);
    }
    break;
    case MotionEvent.ACTION_MOVE:
    if (haveSecondPoint) {
    // 通过 pointerId 来获取 pointerIndex
    int pointerIndex = event.findPointerIndex(1);
    // 通过 pointerIndex 来取出对应的坐标
    point.set(event.getX(pointerIndex), event.getY(pointerIndex));
    }
    break;
    }
    invalidate(); // 刷新
    return true;
    }

    @Override
    protected void onDraw(Canvas canvas) {
    canvas.save();
    canvas.translate(mViewWidth/2, mViewHeight/2);
    canvas.drawText(“追踪第2个按下手指的位置”, 0, 0, mDeafultPaint);
    canvas.restore();

     // 如果屏幕上有第2个手指则绘制出来其位置
     if (haveSecondPoint) {
         canvas.drawCircle(point.x, point.y, 50, mDeafultPaint);
     }
    

    }
    }
    这段代码也非常短,其核心就是通过判断数值为 1 的 pointerId 是否存在,如果存在就在 move 的时候取出其坐标,并绘制出来。

3.如何使用多点触控
多点触控应用还是比较广泛的,至少目前大部分的图片查看都需要用到多点触控技术(用于拖动和缩放图片)。
但是在某些看似不需要多触控的地方也需要对多点触控进行判断,只要是多点触控可能引起错误的地方都应该加上多点触控的判断。例如使用到 move 事件的时候,由于 move 事件可能由多个手指同时触发,所以可能会出现同时被多个手指控制的情况,如果不适当的处理,这个 move 就可能由任何一个手指触发。

举一个简单的例子:
如果我们需要一个可以用单指拖动的图片。假如我们不进行多指触控的判断,像下面这样:
/**

  • 一个可以拖图片动的 View
    */
    public class DragView1 extends CustomView {
    Bitmap mBitmap; // 图片
    RectF mBitmapRectF; // 图片所在区域
    Matrix mBitmapMatrix; // 控制图片的 matrix
    boolean canDrag = false;
    PointF lastPoint = new PointF(0, 0);

    public DragView1(Context context) {
    this(context, null);
    }

    public DragView1(Context context, AttributeSet attrs) {
    super(context, attrs);
    // 调整图片大小
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.outWidth = 960/2;
    options.outHeight = 800/2;

     mBitmap = BitmapFactory.decodeResource(this.getResources(), R.drawable.drag_test, options);
     mBitmapRectF = new RectF(0,0,mBitmap.getWidth(), mBitmap.getHeight());
     mBitmapMatrix = new Matrix();
    

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
    switch (event.getActionMasked()) {
    case MotionEvent.ACTION_DOWN:
    // 判断按下位置是否包含在图片区域内
    if(mBitmapRectF.contains( (int)event.getX(), (int)event.getY())){
    canDrag = true;
    lastPoint.set(event.getX(), event.getY());
    }
    break;
    case MotionEvent.ACTION_UP:
    canDrag = false;
    case MotionEvent.ACTION_MOVE:
    if (canDrag) {
    // 移动图片
    mBitmapMatrix.postTranslate( event.getX() - lastPoint.x, event.getY() - lastPoint.y);
    // 更新上一次点位置
    lastPoint.set(event.getX(), event.getY());
    // 更新图片区域
    mBitmapRectF = new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight()); mBitmapMatrix.mapRect(mBitmapRectF);
    invalidate();
    }
    break;
    }
    return true;
    }

    @Override
    protected void onDraw(Canvas canvas) {
    canvas.drawBitmap(mBitmap, mBitmapMatrix, mDeafultPaint);
    }
    }
    这个版本非常简单,如果正常使用(只使用一个手指)的话也不会出问题,但是当使用多个手指,且有抬起和按下的时候就可能出问题:在第二个手指按下,第一个手指抬起时,此时原本的第二个手指会被识别为第一个,所以图片会直接跳动到第二个手指位置。

为了不出现这种情况,我们可以判断一下 pointId 并且只获取第一个手指的数据,这样就能避免这种情况发生了,如下:
/**

  • 一个可以拖图片动的 View
    */
    public class DragView extends CustomView {
    Bitmap mBitmap; // 图片
    RectF mBitmapRectF; // 图片所在区域
    Matrix mBitmapMatrix; // 控制图片的 matrix
    boolean canDrag = false;
    PointF lastPoint = new PointF(0, 0);

    public DragView(Context context) {
    this(context, null);
    }

    public DragView(Context context, AttributeSet attrs) {
    super(context, attrs);
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.outWidth = 960/2;
    options.outHeight = 800/2;

     mBitmap = BitmapFactory.decodeResource(this.getResources(), R.drawable.drag_test, options);
     mBitmapRectF = new RectF(0,0,mBitmap.getWidth(), mBitmap.getHeight());
     mBitmapMatrix = new Matrix();
    

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
    switch (event.getActionMasked()) {
    case MotionEvent.ACTION_DOWN:
    case MotionEvent.ACTION_POINTER_DOWN:
    // 判断是否是第一个手指 && 是否包含在图片区域内
    if (event.getPointerId( event.getActionIndex())==0 && mBitmapRectF.contains((int)event.getX(), (int)event.getY())){
    canDrag = true;
    lastPoint.set(event.getX(), event.getY());
    }
    break;
    case MotionEvent.ACTION_UP:
    case MotionEvent.ACTION_POINTER_UP:
    // ▼ 判断是否是第一个手指
    if (event.getPointerId( event.getActionIndex())==0){
    canDrag = false;
    }
    break;
    case MotionEvent.ACTION_MOVE:
    // 如果存在第一个手指,且这个手指的落点在图片区域内
    if (canDrag) {
    // 注意 getX 和 getY
    int index = event.findPointerIndex(0);
    // Log.i(TAG, “index=”+index);
    mBitmapMatrix.postTranslate( event.getX(index)-lastPoint.x, event.getY(index)-lastPoint.y);
    lastPostTranslate(event.getX(index)-lastPoint.x, event.getY(index)-lastPoint.y);
    lastPoint.set(event.getX(index), event.getY(index));
    mBitmapRectF = new RectF(0,0,mBitmap.getWidth(), mBitmap.getHeight());
    mBitmapMatrix.mapRect( mBitmapRectF);
    invalidate();
    }
    break;
    }
    return true;
    }
    @Override
    protected void onDraw(Canvas canvas) {
    canvas.drawBitmap(mBitmap, mBitmapMatrix, mDeafultPaint);
    }
    }
    可以看到,比起上一个版本,只添加了少量代码,就变得更加“智能”了,可以准确识别某一个手指,不会因为手指抬起而认错手指:第一个手指抬起之后,图片不会跳跃到第二个手指的位置。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值