【当前热闻】安卓自定义View进阶——多点触控详解
本文转载自GcsSloop的 安卓自定义View进阶-多点触控详解 的文章
Android 多点触控详解,在前面的几篇文章中我们大致了解了 Android 中的事件处理流程和一些简单的处理方案,本次带大家了解 Android 多点触控相关的一些知识。
多点触控( Multitouch,也称 Multi-touch),即同时接受屏幕上多个点的人机交互操作,多点触控是从 Android 2.0 开始引入的功能,在 Android 2.2 时对这一部分进行了重新设计。
(资料图)
在本文开始之前,先回顾一下 MotionEvent详解 中提到过的内容:
Android 将所有的事件都封装进了 Motionvent中。我们可以通过复写 onTouchEvent或者设置 OnTouchListener来获取 View 的事件。多点触控获取事件类型请使用 getActionMasked()。追踪事件流请使用 PointId。
多点触控相关的事件:
事件简介
ACTION_DOWN第一个手指 初次接触到屏幕时触发。
ACTION_MOVE手指 在屏幕上滑动时触发,会多次触发。
ACTION_UP最后一个手指 离开屏幕时触发。
ACTION_POINTER_DOWN有非主要的手指按下(即按下之前已经有手指在屏幕上)。
ACTION_POINTER_UP有非主要的手指抬起(即抬起之后仍然有手指在屏幕上)。
以下事件类型不推荐使用---以下事件在 2.2 版本以上被标记为废弃---
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 个手指抬起,已废弃,不推荐使用。
多点触控相关的方法:
方法简介
getActionMasked()与 getAction()类似,多点触控需要使用这个方法获取事件类型。
getActionIndex()获取该事件是哪个指针(手指)产生的。
getPointerCount()获取在屏幕上手指的个数。
getPointerId(int pointerIndex)获取一个指针(手指)的唯一标识符ID,在手指按下和抬起之间ID始终不变。
findPointerIndex(int pointerId)通过PointerId获取到当前状态下PointIndex,之后通过PointIndex获取其他内容。
getX(int pointerIndex)获取某一个指针(手指)的X坐标
getY(int pointerIndex)获取某一个指针(手指)的Y坐标
回顾完毕,开始正文。
一、多点触控相关问题
在引入多点触控之前,事件的类型很少,基本事件类型只有按下(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)1A手指的编号为0,id为0
一个手指按下(命名为B)2B手指的编号为1,id为1
A手指抬起1B手指编号变更为0,id不变为1
一个手指按下(命名为C)2C手指编号为0,id为0,B手指编号为1,id为1
注意观察上面编号和id的变化,有两个问题,1、B手指的编号变化了。2、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方法,我们可以自己自己手动进行计算,例如下面这样 :
String TAG = "Gcs";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;}
在上面的例子中有几点比较关键:
2.1、action 与 Index 的获得
我们在 MotionEvent详解 中了解过,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)
// 获取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
2.2、用 ACTION_POINTER_1_DOWN 代替 ACTION_POINTER_DOWN
这是因为在 2.0 版本的时候还没有 ACTION_POINTER_DOWN 的这个常量,但是它们两个点数值是相同的,都是 0x00000005,这个你可以查看官方文档或者源码,甚至你直接写 case 0x00000005也行,抬起也是同理。
2.3、只考虑兼容 2.2 以上的版本
当然了,如果你不需要兼容 2.0 版本,只需要兼容到 2.2 以上的话就很简单了,像下面这样:
String TAG = "Gcs";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;}
3. index 和 pointId 的变化规则
在 2.2 版本以上,我们可以通过 getActionIndex() 轻松获取到事件的索引(Index),但是这个事件索引的变化还是有点意思的,Index 变化有以下几个特点:
1、从 0 开始,自动增长。 2、如果之前落下的手指抬起,后面手指的 Index 会随之减小。 3、Index 变化趋向于第一次落下的数值(落下手指时,前面有空缺会优先填补空缺)。 4、对 move 事件无效。
下面我们逐条解释一下具体含义。
3.1、从 0 开始,自动增长。
这一条非常简单,也很容易理解,而且在 MotionEvent详解 中讲解 getAction() 与 getActionMasked() 也简单说过。
手指按下触发事件(数值)
第1个手指按下ACTION_DOWN (0x00000000)
第2个手指按下ACTION_POINTER_DOWN (0x00000105)
第3个手指按下ACTION_POINTER_DOWN (0x00000205)
第4个手指按下ACTION_POINTER_DOWN (0x00000305)
注意加粗的位置,数值随着手指按下而不断变大。
3.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.3、Index 变化趋向于第一次落下的数值(落下手指时,前面有空缺会优先填补空缺)。
这个就有点神奇了,通过上一条规则,我们知道,某一个手指的 Index 可能会随着其他手指的抬起而变小,这次我们用 4 个手指测试一下 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)
第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 会趋向于保持和按下时一样。
3.4、对 move 事件无效。
这个也比较容易理解,我们所取得的 Index 属性实际上是从事件上分离下来的,但是 move 事件始终为 0x00000002,也就是说,在 move 时不论你移动哪个手指,使用 getActionIndex()获取到的始终是数值 0。
既然 move 事件无法用事件索引(Index)区别,那么该如何区分 move 是那个手指发出的呢?这就要用到 pointId 了,pointId 和 index 最大的区别就是 pointId 是不变的,始终为第一次落下时生成的数值,不会受到其他手指抬起和落下的影响。
3.5、pointId 与 index 的异同。
相同点不同点
1. 从 0 开始,自动增长。2. 落下手指时优先填补空缺(填补之前抬起手指的编号)。1. Index 会变化,pointId 始终不变。
4. Move 相关事件
4.1 actionIndex 与 pointerIndex
在 move 中无法取得 actionIndex 的,我们需要使用 pointerIndex 来获取更多的信息,例如某个手指的坐标:
getX(int pointerIndex)getY(int pointerIndex)
但是这个 pointerIndex 又是什么呢?和 actionIndex 有区别么?
实际上这个 pointerIndex 和 actionIndex 区别并不大,两者的数值是相同的,你可以认为 pointerIndex 是特地为 move 事件准备的 actionIndex。
4.2 pointerIndex 与 pointerId
类型简介
pointerIndex用于获取具体事件,可能会随着其他手指的抬起和落下而变化
pointerId用于识别手指,手指按下时产生,手指抬起时回收,期间始终不变
这两个数值使用以下两个方法相互转换。
方法简介
getPointerId(int pointerIndex)获取一个指针(手指)的唯一标识符ID,在手指按下和抬起之间ID始终不变。
findPointerIndex(int pointerId)通过 pointerId 获取到当前状态下 pointIndex,之后通过 pointIndex 获取其他内容。
4.3 遍历多点触控
先来一个简单的,遍历出多个手指的 move 事件:
String TAG = "Gcs";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 的变化。
4.4 在多点触控中追踪单个手指
要实现追踪单个手指还是有些麻烦的,需要同时使用上 actionIndex, pointerId 和 pointerIndex,例如,我们只追踪第2个手指,并画出其位置:
/** * 绘制出第二个手指第位置 */public class MultiTouchTest extends CustomView {String TAG = "Gcs"; // 用于判断第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 的时候取出其坐标,并绘制出来。
二、如何使用多点触控
多点触控应用还是比较广泛的,至少目前大部分的图片查看都需要用到多点触控技术(用于拖动和缩放图片)。
但是在某些看似不需要多触控的地方也需要对多点触控进行判断,只要是多点触控可能引起错误的地方都应该加上多点触控的判断。例如使用到 move 事件的时候,由于 move 事件可能由多个手指同时触发,所以可能会出现同时被多个手指控制的情况,如果不适当的处理,这个 move 就可能由任何一个手指触发。
举一个简单的例子:
如果我们需要一个可以用单指拖动的图片。假如我们不进行多指触控的判断,像下面这样:
没有针对多指触控处理版本:
/** * 一个可以拖图片动的 View */public class DragView1 extends CustomView {String TAG = "Gcs"; 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 {String TAG = "Gcs"; 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); 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); }}
可以看到,比起上一个版本,只添加了少量代码,就变得更加“智能”了,可以准确识别某一个手指,不会因为手指抬起而认错手指。
重点注意最后,第一个手指抬起之后,图片并没有跳跃到第二个手指的位置。
上面的两个对比示例都精简到了极致,其核心依旧是正确的追踪某一个手指,建议大家自己写一遍体会一下。
我感觉很多人看到这里依旧是不明所以的,一些简单的东西还好弄,但是复杂一些,如同时处理多个手指的数值就有些困难了,假如说你之前没有接触过多点触控的处理,此时让你实现用两个手指来缩放图片还是有些困难的。
因为这不仅要追踪两个手指的位置,还要根据位置变化来计算缩放比例和缩放中心,单单这两个非常简单的数学问题就能难倒一大批人。
当然了,很多麻烦问题都有简单的解决方案,假如说我们真的要实现一个可以用两个或者多个手指缩放的控件,何必要自己算呢,可以尝试一下 Android 自带的解决方案:手势检测(GestureDetector、ScaleGestureDecetor),不仅能自动帮你计算好缩放比例和缩放中心,而且还可以检测出 单击、长按、滑屏 等不同的手势,不过这就不是本篇的事情了,以后有时间会写一下有关手势检测的用法(继续挖坑)。
三、总结
前段时间因为各种事情比较忙,这篇文章也没时间去写,所以就一直拖到了现在,期间收到不少读者催更,实在是抱歉了。今后在会尽量保证稳定更新的,争取尽快把自定义View系列这一个大坑填完。 ˊ_>ˋ
关于多点触控,个人认为还算一个比较重要的知识点。尤其是随着 Android 的发展,很多炫酷的交互操作可能会需要用户进行拖拽操作。在进行这类操作的时候进行一下手指的判断还是相当重要的。
本文中需要注意的几个知识点:
如何兼容 2.0 版本的多点触控(目前大部分都不需要兼容 2.0 了吧)。actionIndex、pointIndex 与 pointId 的区别和用法。如何在多点触控中正确的追踪一个手指。
标签:
相关推荐:
精彩放送:
- []箭在弦上电视剧全集48大结局 电视剧箭上一共多少集?
- []【天天速看料】飞信公众平台密码怎么修改?电脑版密码修改教程
- []世界观焦点:三星q40配置怎么样?三星q40评测及报价
- []有线路由器和无线路由器有什么区别?哪个更实用?
- []淘宝刷单怎么做?淘宝刷单技巧
- []你的无线网络被蹭网了吗?怎么防止别人蹭网?
- []全球视讯!上海商学院是985还是211?哪个校区好?
- []【当前热闻】安卓自定义View进阶——多点触控详解
- []当前热讯:手机hd怎么关闭?关闭魅族手机Flyme7.1系统教程
- []每日热闻!《POPPIN滑步基本教学》单臂关节逆向传递手法
- []【天天热闻】如何玩转苹果手机?iPhone手机使用技巧
- []【世界时快讯】微信号怎么注册?微信使用方法有哪些?
- []每日观察!绍兴上虞区2.68亿元挂牌一宗商住地 预计3月31日出让
- []全球视点!衢州江山市1.67亿挂牌一宗商住地 其中2万平建面用于异地搬迁安居房
- []华润杭州上城区四堡七堡项目拟建8幢高层住宅 毛坯均价不高于6万元/平米
- []环球观速讯丨1赔1.5什么意思
- []招商银行信用卡年费,有以下八种
- []视焦点讯!TD早报 | 韩国多家航司将陆续恢复赴华航班;2022中国飞行员报告出炉
- []贷款买车流程和注意事项
- []环球今日报丨车辆增值税怎么算
- []3月13日重点数据和大事件前瞻
- []各大银行存款定期利率表2022最新版,各银行存款利率一览
- []世界热点!vnd是什么货币
- []全球简讯:普通人如何投资黄金
- []天天报道:什么是做空
- []黑天鹅事件是什么意思
- []每日观察!刺猬简笔画幼儿_刺猬简笔画
- []当前观察:失散25年的一家人终于团聚!
- []天天新动态:企业所得税怎么征收2021
- []怎么查公积金贷款额度,有以下四个方法
- []天天关注:征信报告在哪里可以打,有以下两种方法
- []几点开盘 股票开盘时间是多少
- []银行3年定期存款利率2022,五大行利率一览
- []“惠购湖北”消费券将于3月16日起投放 总额5亿元
- []每日报道:急需用钱又贷不了款怎么办
- []焦点!银行卡怎么解除限额,有以下两种方法
- []95595是什么银行,中国光大银行客服电话
- []讯息:工商银行几点上班
- []恩格尔系数是什么
- []福州闽侯第一高楼力争2026年底开放运营 将建超200米酒店地标
- []中电光谷与上海璨仓签合作协议 共同打造金桥未来车创新园
- []【世界快播报】外汇市场的特点
- []热门:氯喹是哪个上市公司生产的
- []【天天播资讯】2020版5元纸币首发冠号是什么
- []世界微动态丨中金发债中签后多久上市
- []基建龙头股票有哪些2022,十大优质股票一览
- []万份收益是什么意思
- []世界快看点丨工行利息2022年最新利率表,工行利率一览
- []建行转农行需要手续费吗
- []外地人在上海交社保,方法有以下两个
- []股票ipo上市是什么意思呢
- []全球今日报丨中国建筑1-2月地产合约销售额535亿 比上年同期增长71.1%
- []上交所总经理蔡建春:下一步房地产调控政策要更大力度拯救企业
- []视讯!华发股份前2月销售额为249.76亿 2月单月大涨292.93%
- []完成置入新能源资产后 广宇发展2022年归母净利涨逾300%至6.33亿
- []欧派家居累计回购23.9万股 涉资2999.06万元
- []【新视野】全国城市排名2021最新排名GDP,TOP20强榜单
- []热文:2022端午节股市放假几天,3天
- []当前快讯:劳动保险基金是什么
- []环球滚动:降准和降息的区别
- []【天天热闻】川宁生物业绩快报:2022年净利润同比增长270%
- []各银行房贷利率2020
- []世界动态:公司怎么开通社保账户,有以下三个步骤
- []世界微资讯!金融危机对老百姓的影响是什么
- []当前热点-自然灾害保险公司赔吗
- []北交所的交易方式
- []天天简讯:银行年利率2020
- []冰雹给车砸了保险赔吗
- []全球热议:什么叫运费险,一种运费保险
- []环球观点:沪惠保怎么买
- []环球今头条!创业板交易新规
- []天天热点!余额宝为什么没收益了,有以下三点
- []天天资讯:医药电商概念股票龙头一览表,2023医药电商相关上市公司有哪些
- []环球焦点!邮政储蓄10万无息贷款,条件共有以下8点
- []股市端午节放假安排2022,6月3日至6月5日休市
- []抚州市临川区唱凯镇观前小学附属幼儿园
- []税优识别码在保单上怎么找到,一般是在右上角
- []微头条丨一边喊着涨价,一边推特价房……广州楼市“小阳春”是真的吗?
- []当前快看:TPP协议是什么
- []哪个银行有硬币兑换机
- []十一高速公路免费吗2022,免费
- []银行u盾怎么用
- []全球视点!如何申购新股票
- []世界快报:端午节高速公路免费吗2022,不免费
- []什么是中小板
- []全球观察:期货怎么买卖操作
- []每日视点!什么是货币型基金
- []2022年人民银行逾期贷款利率,贷款逾期怎么办
- []环球简讯:招行私人银行客户标准
- []讯息:国内旅游者个人保险有什么?
- []超5成商家2022年订单量同比翻倍,飞猪租车:2023年减免商家年费、服务投入加倍
- []一般的车险怎么买,有以下三点
- []WORLD MADAM世界夫人2022中国区年度颁奖盛典圆满收官,赵迪摘得总冠军桂冠
- []汇款怎么汇款
- []一瓶广州好水,“广州矿泉”正式发布
- []公益电影再出精品 《恩情》报答催人泪下
- []通讯!国内最大的养老院上市公司,龙头股一览
- []科创50ETF是什么
- []焦点讯息:增发是利好还是利空?
- []全球热资讯!央行mlf操作是什么意思
- 世界观天下!徐佳雯名字打分_徐佳雯
- 天天通讯!2022五一股市休市几天,休市五天
- 浦发硅谷银行紧急回应!破产风暴直击硅谷 最黑暗一天?世界首富要出手?
- 热门:布林中轨是什么线,布林线中间的线
- 热议:刷屏!董明珠:不退休不会卖股票 鼓励员工“砸锅卖铁”买格力股票 买不了我兜底!
- 天天资讯:室温能否超导 很快见分晓!顶级专家密集发声
- 前沿资讯!什么是投连保险
- 热文:如何网上买基金
- 【环球播资讯】招商银行信用卡分期手续费
- 世界聚焦:货车贷款怎么贷,有以下六步
- 股票下跌放量是好还是坏,一般是不好的
- 宜信贷款无力偿还了怎么办已经逾期了,有以下三种处理方式
- 世界微速讯:什么原因导致黄金下跌,分为以下五点
- 股市为什么最近大跌,有以下五种原因
- 硅谷银行宣布破产 多家机构点评:大概率不会演变成更广泛的危机事件
- 今日热讯:新能源龙头股票有哪些,十大优质龙头股名单
- 长城汽车王者归来全新Hi4引领全民电四驱时代
- 世界微动态丨超九成正收益!这类基金产品年内“大回血”
- 微资讯!左耳最后张漾为什么恨许_左耳最后张漾对李珥说了什么
- 看热讯:4-1,2-2!豪门悲喜夜,曼联争冠入正轨,枪手酿反击:造53年纪录
- 赴华:中日澳往来航班数量激增,中美增班短期无望
- LOL全明星首日,LPL两战皆败,如何评价选手欢乐,网友不爽的现象?
- 每日热讯!全国政协委员霍颖励:建议增加居民住房用地供给
- 10万级理财神器,全新一代GS3·影速并非虚有其表
- 天天新资讯:顶流基金经理的“小动作”:朱少醒、刘彦春、谢治宇、刘格菘、冯明远“隐形重仓股”揭秘
- 中农七朵打好云南鲜蒜“特色牌” 助力产业兴村民富
- Windows11 25309新功能开启
- 天天要闻:聚飞光电:公司严格按照法律法规履行信息披露义务,关于公司2022年业绩信息
- 天天时讯:掌握新世代居住风向 东四环红盘传奇再起新风潮
- 观察:帮助用户巧妙地管理资金问题并享受更大的财务授权
- 资讯:滴滴企业版上线机酒预订;OTA巨头对ChatGPT保持观望 | 大公司简报
- 东风风神皓瀚实车图首次曝光!宇宙造型美学搅局紧凑SUV市场
- 【环球热闻】这个板块被主力罕见爆买35亿 ETF份额创3年新高!近期火爆的基建却被机构甩卖
- 凭实力造就新标杆 东风马赫动力燃擎车谷
- 当前关注:楚江新材:截至2023年3月10日,公司股东人数为58,168人
- 热资讯!巧夺天工·向美而生 2023金隅天坛整装新品发布会圆满落幕
- 环球通讯!万里扬: 公司与长江绿色发展基金在新能源储能电站的投资、运营和项目等方面开展全方面合作
- 阿威十八式各指叫什么_阿威十八式各指怎样叫
- 亚太市场差旅需求复苏超欧美;Expedia子公司前高管创立差旅公司 | 一周商旅动态
- 资讯推荐:博菲电气:目前公司产品未应用于储能上
- 【环球热闻】下毒
- 焦点短讯!桂浩明:A股市场上消费板块为何预期很热但表现一般
- 旭辉纾困时刻 境外债重组方案中的行动与态度
- 每日热闻!远洋集团逆市增长股价升超9% 穆迪肯定其充足流动性
- 当前要闻:生活是什么歌完整版_生活是什么排比句
- 绿城服务预期2022年权益股东应占溢利下降30%至40%
- 微头条丨国美零售预计2022年归母净亏损达到170亿-190亿
- 世界今日讯!海利得:公司正积极深入推进光伏反射膜的研发与市场开拓,相关工作均在顺利进行中
- 每日资讯:楼市调查 | 桂语兰庭正式拿证 绿城深圳首秀热度尚待验证
- 热点聚焦:公募最新“打法”曝光!新基金快速建仓 老基金调仓换股
- 今日热议:M2增速创7年新高 2月新增信贷、社融规模增量均超预期 影响多大?
- 广州成为近期热度上涨最快的国内旅游目的地
- 天天快看点丨美债持有国排名2022,前五美债持有国名单一览
- 疫情补助金怎么领取
- 个人所得税退税步骤,有以下六步
- 美国加息的影响,有以下两点
- 环球新动态:中报预披露什么意思
- 热门:筑友智造预计2022年转盈为亏?归母净亏损约1.5亿港元
- 全球即时:易点云第三次港交所递交招股书?2022年净亏损6.12亿
- 下周看点:前两月经济数据将公布 互联互通股票标的扩容
- 水鸭汤的做法怎么炖_水鸭汤的做法
- 微资讯!银行账户情况说明范文_银行账户情况说明范本
- 全球信息:2022房贷退税是怎么回事,可以抵扣个税
- 今热点:离岸资产是什么意思
- 简讯:拼多多天天领现金是真的吗 拼多多是不是可以天天领现金
- 天天快资讯丨中国第一枚邮票
- 环球最资讯丨佣金是什么?
- 全球百事通!ST新规解读,有以下七种
- 环球时讯:中国城市gdp排名2021,中国百强城市gdp一览
- 天天观焦点:余额宝靠谱吗 余额宝靠不靠谱
- 最新:腾讯上市了吗
- 当前热点-最新各银行存款利率是多少,各行定期利率一览
- 汉宇集团:3月9日公司高管文红减持公司股份合计8.3万股
- 乐歌股份:3月9日公司高管朱伟减持公司股份合计2万股
- 东山精密:截至2023年3月10日,公司股东总户数为46,131户
- 宁波色母:3月9日公司高管赵茂华、祖万年减持公司股份合计14.71万股
- 淘宝运费险怎么退 淘宝运费险的退款流程
- 世界gdp排名全国2022,各国gdp排名一览
- 最新银行存款利率表2022,各大银行存款利率一览
- 中国银行卡号开头
- 播报:活期存款利息 活期存款的利率是多少
- 全球快讯:橙天嘉禾预期2022年净亏损减少80% 上年亏损额3.14亿港元
- 【聚看点】百隆东方:3月10日公司高管张奎减持公司股份合计10万股
- 绿景中国:已向受托人汇出资金 偿还3月10日到期票据
- 环球快播:康泰生物:上述情况不属实,公司生产经营一切正常
- 天天速讯:深圳首个不限购区域来了!业内称“没有官方文件,但外地人早就不限购了”
- 格力地产1483万股员工持股解锁 占总股本0.79%
- 天天快看点丨玉龙股份:3月9日公司高管卢奋奇、梁海涛、刘锋玉增持公司股份合计8.45万股
- 天地源2022年营收105.5亿?合同销售金额108亿
- 热消息:赛特新材:3月9日公司高管严浪基减持公司股份合计6250股
- 【天天播资讯】银行金条规格
- 什么是运费险怎么使用
- 今日热文:怎么可以全部提取公积金,有以下四步
- 天天即时看!支付宝转账手续费 支付宝转账的手续费
- 现钞买入价和现汇买入价的区别,有以下三点
- 天天时讯:*ST海伦收到拟处罚告知书 索赔案征集已启动
- 全球热议:多氟多:目前公司六氟磷酸锂销量和产销率位于行业前列
- 新资讯:神州高铁:截至2023年2月28日,公司股东人数为91621
- 天天观焦点:聚焦中概 | 中概教育股普跌,京东绩后跌超8%
- 卡小逗上海首店日销咖啡1200+杯,松弛感+实力派打动咖啡之城