对于Android自定义控件开发,多点触控是一个必须要懂的知识点。因为在正常的情况下操作正常的控件,使用多指操作时,基本上都会出现问题。当需要对多指操作进行兼容时,就需要这方面的知识了。
本文选自《Android自定义控件高级进阶与精彩实例》一书,带你了解多点触控的基本知识。
—— 正文 ——
假如,我们做了这么一个功能,图像跟随手指移动。
在单指操作下,图像的移动非常流畅、正确,而如果我们使用两根手指的话,就会出现下面这种情况。
从效果图可以看出,在第2根手指放下,而第1根手指抬起时,图像会出现跳跃,直接从第1根手指的位置移动到了第2根手指的位置,这明显是不对的。这只是一个简单的例子,一般使用单指操作的控件改到多指操作的时候,都会出现问题。
这便是本文讲解多点触控的初衷。既然多点触控会造成这么多问题,那么下面就来详细了解它吧。
▼
单点触控与多点触控
1
单点触控
单点触控与多点触控是相对的,单点触控的意思是,我们只考虑一根手指的情况,而且仅处理一根手指的触摸事件,而多点触控是处理多根手指的触摸事件。
一般我们处理MotionEvent事件,通过Mo来获取事件类型,这就是单点触控。在单点触控中,会涉及对下面几个消息的处理。
除了消息外,我们也经常用下面这几个函数来获取手指的位置等信息,这些函数都没有参数,也都只有在单点触控时才能使用。
对于这几个函数的使用方法,这里就不再赘述了。可以看到,我们平常所处理的MotionEvent事件,以及常用的MotionEvent函数都只是针对单点触控的,那么哪些才是多点触控的事件和函数呢?
2
多点触控
首先,多点触控的消息类型只能通过getActionMasked来获取。因此,判断当前代码处理的是单点触控还是多点触控,单从获取消息类型的函数就可以看出。
说明:单点触控是通过getAction来获取当前事件类型的,而多点触控是通过getActionMasked来获取的。
多点触控涉及的消息类型与单点触控的不一样,它的消息类型如下。
比如以下图中的手指按下顺序,我们来看看其中的事件触发顺序。
在效果图中,先后有3根手指按下,按下顺序是1、2、3,抬起顺序是1、3、2,而事件触发顺序如下表。
这里需要注意,
第1根手指按下时,收到的消息是ACTION_DOWN;
随后的手指再按下时,收到的是ACTION_POINTER_DOWN;
当有手指抬起时,收到的是ACTION_POINTER_UP;
当最后一根手指抬起时,收到的是ACTION_UP。
对多点触控消息进行处理的代码如下:
1String TAG = "qijian"; 2@Override 3public boolean onTouchEvent(MotionEvent event) { 4 switch ()) { 5 case Mo: 6 Log.e(TAG,"第1根手指按下"); 7 break; 8 case Mo: 9 Log.e(TAG,"最后一根手指抬起"); 10 break; 11 case Mo: 12 Log.e(TAG,"又一根手指按下"); 13 break; 14 case Mo: 15 Log.e(TAG,"又一根手指抬起"); 16 break; 17 } 18 return true; 19} 20... 21 }
这里仅列出了手指按下和手指抬起所触发的消息类型,而在手指移动时,无论是单点触控还是多点触控,所触发的消息都是Mo。
在多点触控时,我们可以通过代码来获取当前移动的是哪根手指。
多点触控
1
识别按下的手指
上面讲解了在什么情况下会触发什么消息,但我们怎么来识别当前按下的是哪根手指呢?
在MotionEvent中有一个Pointer的概念:
一个Pointer就代表一个触摸点,每个Pointer都有自己的消息类型,也有自己的X坐标值。一个MotionEvent对象中可能会存储多个Pointer的相关信息,每个Pointer都有自己的PointerIndex和PointerId。在多点触控中,就是用PointerIndex和PointerId来标识用户手指的。
- PointerIndex表示当前手指的索引,PointerId是手指按下时分配的唯一id,用来标识这根手指。
- 每根手指从按下、移动到离开屏幕,PointerId是不变的,而PointerIndex则不是固定的。
通过下面这个例子,我们来了解一下PointerIndex与PointerId的区别。
可见同一根手指的id是不变的,而PointerIndex是会变化的,但总是以0、1或者0、1、2这样的形式出现,而不可能出现0、2这样间隔了一个数或者1、2这种没有0索引值的形式。
针对PointerIndex与PointerId,在MotionEvent类中经常使用下面这几个函数。
- public final int getActionIndex:
用于获取当前活动手指的PointerIndex值。
- public final int getPointerId(int pointerIndex):
用于根据PointerIndex值获取手指的PointerId,其中pointerIndex表示手指的PointerIndex值。
- public final int getPointerCount:
用于获取用户按下的手指个数,一般我们用它来遍历屏幕上的所有手指,遍历手指的代码如下:
1for (int i = 0; i < event.getPointerCount(); i++) { 2 int pointerId = event.getPointerId(i); 3}
前面讲过,PointerIndex是从0开始的,表示当前所有手指的索引,值从0到getPointerCount() − 1,不会出现不连续的数。因此,我们通过event.getPointerCount可以得到当前屏幕上的手指个数。然后从0开始遍历PointerIndex,同时我们还能通过int pointerId = event.getPointerId(i)来得到每根手指PointerIndex所对应的PointerId。
- public final int findPointerIndex(int pointerId):
用于根据PointerId反向找到手指的PointerIndex值。
由此,我们就知道了PointerIndex与PointerId的关系,以及它们相互之间的换算方法。下面再来看看通过PointerIndex和PointerId能得到什么。
2
获取手指位置信息
通过PointerIndex与PointerId,可以使用以下函数获得手指的位置信息。
- public final float getX(int pointerIndex):
根据PointerIndex得到对应手指的X坐标值,该函数的意义与单点触控里的getX函数相同。
- public final float getY(int pointerIndex):
同样地,根据PointerIndex得到对应手指的Y坐标值,该函数的意义与单点触控里的getY函数相同。
实例:追踪第2根手指
现在,我们将通过一个实例来学习上面讲到的函数。
这里实现的效果是:当用户按下第2根手指时,就开始追踪这根手指,无论其他手指是否抬起,只要这根手指没有抬起,就一直显示这根手指的位置,如下如。
从效果图可以看出,先后总共按下了3根手指,分别在左(第1根手指)、中(第2根手指)、右(第3根手指)。
抬起手指时,先抬起左侧第1根手指,然后抬起右侧第3根手指。可以看到,第2根手指的触摸点,我们使用白色圆圈显示,无论第3根手指是否按下,还是其他手指是否抬起,白色圆圈总是跟着第2根手指的移动来显示。这就实现了跟踪第2根手指轨迹的效果。
下面我们来看看这个效果是怎么实现的吧。
1
自定义View并初始化
布局很简单,就是一个全屏View,为了在View上画圆圈,我们必须自定义View,其中的初始化代码如下:
1public class MultiTouchView extends View { 2 // 用于判断第2根手指是否存在 3 private boolean haveSecondPoint = false; 4 // 记录第2根手指的位置 5 private PointF point = new PointF(0, 0); 6 private Paint mDefaultPaint = new Paint(); 7 8 public MultiTouchView(Context context) { 9 super(context); 10 init(); 11 } 12 13 public MultiTouchView(Context context, @Nullable AttributeSet attrs) { 14 super(context, attrs); 15 init(); 16 } 17 18 public MultiTouchView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 19 super(context, attrs, defStyleAttr); 20 init(); 21 } 22 23 private void init() { 24 mDe); 25 mDe(true); 26 mDe); 27 mDe(30); 28 } 29}
这样我们就自定义了一个View,很明显它内部不会再包裹其他的View控件,所以继承自View类即可。
我们定义了3个变量,其中:
- haveSecondPoint用于判断第2根手指是否按下。
- point用于记录第2根手指的位置。
- mDefaultPaint是画笔变量,用于画第2根手指位置处的白色圆圈。
2
onTouchEvent
然后,在用户按下手指时,需要加以判断,当前是第几根手指,然后获取第2根手指的位置,下面列出完整代码:
1public boolean onTouchEvent(MotionEvent event) { 2 int index = event.getActionIndex(); 3 4 switch ()) { 5 case Mo: 6 if (index) == 1) { 7 haveSecondPoint = true; 8 (), event.getY()); 9 } 10 break; 11 case Mo: 12 try { 13 if (haveSecondPoint) { 14 int pointerIndex = event.findPointerIndex(1); 15 (pointerIndex), event.getY(pointerIndex)); 16 } 17 } catch (Exception e) { 18 haveSecondPoint = false; 19 } 20 break; 21 case Mo: 22 if (index) == 1) { 23 haveSecondPoint = false; 24 } 25 break; 26 case Mo: 27 haveSecondPoint = false; 28 break; 29 } 30 31 invalidate(); 32 return true; 33}
获取当前活动手指的PointerIndex值:
1int index = event.getActionIndex();
我们知道,当第1根手指按下的时候触发的是ACTION_DOWN消息,随后的手指按下的时候触发的都是ACTION_POINTER_DOWN消息。因为我们要跟踪第2根手指,所以这里只需要识别ACTION_POINTER_DOWN消息即可:
1case Mo: 2 if (index) == 1) { 3 haveSecondPoint = true; 4 (), event.getY()); 5 } 6 break;
我们也知道PointerIndex是变化的,而PointerId是不变的,PointerId根据手指按下的顺序从0到1逐渐增加。因此,第2根手指的PointerId就是1。当(index) == 1时,就表示当前按下的是第2根手指,将haveSecondPoint设为true,并将得到的第2根手指的位置设置到point中。
到这里,大家可能会产生疑问,上面提到的多点触控获取手指位置都用的是event.getX(pointerIndex),而这里怎么直接用event.getX了呢?其实这里使用event.getX (pointerIndex)也是可以的,大家可以先记下这个问题,后面我们再详细讲解。
当手指移动时,会触发ACTION_MOVE消息:
1case Mo: 2 try { 3 if (haveSecondPoint) { 4 int pointerIndex = event.findPointerIndex(1); 5 (pointerIndex), event.getY(pointerIndex)); 6 } 7 } catch (Exception e) { 8 haveSecondPoint = false; 9 } 10 break;
需要注意,因为这里使用event.findPointerIndex(1)来强制获取PointerId为1的手指PointerIndex,在异常情况下可能出现越界,所以使用try…catch…来进行保护。
在这里,我们使用event.getX(pointerIndex)来获取指定手指的位置信息。同样地,这个问题也放在后面讲解。
当手指抬起时,会触发ACTION_POINTER_UP消息:
1case Mo: 2 if (index) == 1) { 3 haveSecondPoint = false; 4 } 5 break;
同样地,使用event.getPointerId(index)来获取当前抬起手指的PointerId,如果是1,那就说明是第2根手指抬起了,这时就把haveSecondPoint设为false。
当全部手指抬起时,会触发ACTION_UP消息:
1case Mo: 2 haveSecondPoint = false; 3 break;
在最后一根手指抬起时,把haveSecondPoint设为false,白色圆圈从屏幕上消失。
最后,调用invalidate();来重绘界面。
3
onDraw
在重绘界面时,主要是在point中存储的第2根手指的位置处画一个白色圆圈:
1protected void onDraw(Canvas canvas) { 2 3 canvas.drawColor); 4 if (haveSecondPoint) { 5 canvas.drawCircle, , 50, mDefaultPaint); 6 } 7 8 canvas.save(); 9 canvas.translate(getMeasuredWidth() / 2, getMeasuredHeight() / 2); 10 canvas.drawText("追踪第2个按下手指的位置", 0, 0, mDefaultPaint); 11 canvas.restore(); 12}
首先,为整个屏幕绘一层绿色,把上一屏的内容清掉:
1canvas.drawColor);
然后,如果第2根手指按下了,则在它的位置处画一个圆圈:
1if (haveSecondPoint) { 2 canvas.drawCircle, , 50, mDefaultPaint); 3}
最后,在布局的中间位置写上提示文字:
1canvas.save(); 2canvas.translate(getMeasuredWidth() / 2, getMeasuredHeight() / 2); 3canvas.drawText("追踪第2个按下手指的位置", 0, 0, mDefaultPaint); 4canvas.restore();
有关Canvas的操作及写字的操作,在《Android自定义控件开发入门与实战》一书中有详细章节讲述,这里就不再赘述了。
在写好控件以后,直接利用XML引入布局即可,这里不再展示,效果就是我们想要的样子。
关于作者
启舰
本名张恩伟,Android研发专家、CSDN博客专家、CSDN博客之星,《Android自定义控件入门与实战》《Android自定义控件高级进阶与精彩实例》作者,电子工业出版社博文视点优秀作者,曾就职于阿里巴巴,现就职于vivo。
图书推荐
▊《Android自定义控件高级进阶与精彩实例》
启舰 著
- 专注于介绍Android自定义控件进阶知识
- 通过精彩的案例对各种绘制、动画技术进行了糅合讲解
本书主要内容有3D特效的实现、高级矩阵知识、消息处理机制、派生类型的选择方法、多点触控及辅助类、RecyclerView的使用方法及3D卡片的实现、动画框架Lottie的讲解与实战等。
读者可以通过本书从宏观层面、源码层面对Android自定义控件建立完整的认识。