事故现场

  最近在对公司项目中的控件进行优化改造,其中一个是能够上拉和下拉的弹性ScrollView。

  发现没有,当使用一个手指的时候感觉还不错。但是当我想用两个手指交替不断下拉想要把视图内容往下“扒”的时候就办不到了,因为他在onTouchEvent()方法里只是最简单的实现了下拉的逻辑而没有涉及到多指触控,而我想要的效果是像QQ空间或者微信朋友圈那样的。


预备知识

为了将控件改造成能够支持多点触控的,首先我们需要了解Android中关于多点触控的基础知识。这里我推荐先阅读GcsSloop的两篇文章
安卓自定义View进阶-MotionEvent详解
安卓自定义View进阶-多点触控详解
  我现在假设你已经看了上面的那两篇文章,下面我来划重点:
1.多点触控时必须使用getActionMasked()来获取事件类型
2.Pointer:
MotionEvent中引入了Pointer的概念,一个pointer就代表一个触摸点,每个pointer都有自己的事件类型,也有自己的横轴坐标值。一个MotionEvent对象中可能会存储多个pointer的相关信息,每个pointer都会有一个自己的id和index。pointer的id在整个事件流中是不会发生变化的,但是index会发生变化
3.PointerId:
每根手指从按下、移动到离开屏幕,每个手指都会拥有一个固定PointerId.PointerId的值,一般用它来区分是哪根手指
4.PointerIndex:
每根手指从按下、移动到离开屏幕,每根手指在每一个事件的Index可能是不固定的,因为受到其它手指的影响
5.PointerId和PointerIndex的变化规律
关于变化规律这里,可以看GcsSloop的第二篇文章,这里我不再赘述,只举一个实际的例子:

事件 PointerId PointerIndex
依次按下三根手指 三根手指的id依次为0、1、2 三根手指的index依次为0、1、2
抬起第二根手指 第一根手指的id为0,第三根手指的id为2 第一根手指的index为0,第三根手指的index变为1
抬起第一根手指 第三根手指的id为2 第三根手指的index变为0

可见同一根手指的id是不会变化的,而index是会变化的,但总是以0、1或者0、1、2这样的形式出现,而不可能出现0、2这样间隔了一个的或者1、2这样的没有0索引在内的形式

6.多点触控相关事件

事件 简介
ACTION_DOWN 第一个手指初次接触到屏幕时触发
ACTION_MOVE 手指在屏幕上滑动时触发,会多次触发。
ACTION_UP 最后一个手指离开屏幕时触发
ACTION_POINTER_DOWN 有非主要的手指按下(即按下之前已经有手指在屏幕上)
ACTION_POINTER_UP 有非主要的手指抬起(即抬起之后仍然有手指在屏幕上)

7.多点触控相关的方法:

方法 简介
getActionMasked() 与 getAction() 类似,多点触控需要使用这个方法获取事件类型
getActionIndex() 获取该事件是哪个指针(手指)产生的
getPointerId(int pointerIndex) 获取一个指针(手指)的唯一标识符ID,在手指按下和抬起之间ID始终不变
getX(int pointerIndex) 获取某一个指针(手指)的X坐标
getY(int pointerIndex) 获取某一个指针(手指)的Y坐标
findPointerIndex(int pointerId) 通过PointerId获取到当前状态下PointIndex,之后通过PointIndex获取其他内容
getPointerCount() 获取在屏幕上手指的个数

如何使用多点触控

对于多点触控的处理,一般是这样:
记录活动手指的id(mActivePointerId),通过此id获取move事件的坐标

  1. 在手指按下的时候,记录下activePointerId
  2. 第二根手指按下的时候,更新activePointerId(我们让第二根手指作为活动手指,忽略第一个手指的move)
  3. 当其中一根手指抬起时,如果是第一根手指,那么不做处理,如果是第二根手指抬起,也就是活动手指抬起的话,将活动手指改回第一根
    我这里还是举一个GcsSloop文章中的例子:
    如果我们需要一个可以用单指拖动的图片。假如我们不进行多指触控的判断,像下面这样:
    没有针对多指触控处理版本:
public class DragViewSingleTouch extends View {String TAG = "DragViewSingleTouch";Bitmap mBitmap;         // 图片RectF mBitmapRectF;     // 图片所在区域Matrix mBitmapMatrix;   // 控制图片的 matrixboolean canDrag = false;PointF lastPoint = new PointF(0, 0);private Paint mDeafultPaint;public DragViewSingleTouch(Context context) {this(context, null);}public DragViewSingleTouch(Context context, @Nullable AttributeSet attrs) {super(context, attrs);mDeafultPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);// 调整图片大小BitmapFactory.Options options = new BitmapFactory.Options();options.outWidth = 960/2;options.outHeight = 800/2;mBitmap = BitmapFactory.decodeResource(this.getResources(), R.drawable.poly_test, options);mBitmapRectF = new RectF(0,0,mBitmap.getWidth(), mBitmap.getHeight());mBitmapMatrix = new Matrix();}@Overridepublic 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;}@Overrideprotected void onDraw(Canvas canvas) {canvas.drawBitmap(mBitmap, mBitmapMatrix, mDeafultPaint);}}

这个版本非常简单,当然了,如果正常使用(只使用一个手指)的话也不会出问题,但是当使用多个手指,且有抬起和按下的时候就可能出问题

  注意在第二个手指按下,第一个手指抬起时,此时原本的第二个手指会被识别为第一个,所以图片会直接跳动到第二个手指位置。原因是event.getX()和event.getY中没有传入pointerIndex的参数, 那么默认追踪的就是pointerIndex = 0的手指,当第二个手指按下,第一个手指抬起的时候,触发了move事件,event.getX()和event.getY()此时是获取第二个手指的数据,而lastPoint.x和lastPoint.y并没有在第二个手指按下的时候进行更新,记录的是第一个手指抬起时候的坐标,和evet.getX()、event.getY()有较大的距离, 所以postTranslate了很大一段距离, 发生了跳动的情况。
  为了不出现这种情况,我们可以判断一下 pointId 并且只获取第一个手指的数据,这样就能避免这种情况发生了,如下。
针对多指触控处理后版本:

public class DragViewUpGrade extends View {String TAG = "DragViewUpGrade";Bitmap mBitmap;         // 图片RectF mBitmapRectF;     // 图片所在区域Matrix mBitmapMatrix;   // 控制图片的 matrixboolean canDrag = false;PointF lastPoint = new PointF(0, 0);public DragViewUpGrade(Context context) {this(context, null);}public DragViewUpGrade(Context context, AttributeSet attrs) {super(context, attrs);mDeafultPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);BitmapFactory.Options options = new BitmapFactory.Options();options.outWidth = 960/2;options.outHeight = 800/2;mBitmap = BitmapFactory.decodeResource(this.getResources(), R.drawable.poly_test, options);mBitmapRectF = new RectF(0,0,mBitmap.getWidth(), mBitmap.getHeight());mBitmapMatrix = new Matrix();}@Overridepublic 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;}private Paint mDeafultPaint;@Overrideprotected void onDraw(Canvas canvas) {canvas.drawBitmap(mBitmap, mBitmapMatrix, mDeafultPaint);}
}

这个也是GcsSloop的代码,因为这里只追踪第一根手指(pointerId = 0的手指),第二根手指的活动全都无视,所以不会再出现跳动的情况

  但是我觉得依旧不够,因为我想要的是当第二根手指放下的时候就可以靠第二根手指来移动图片,而不是无视第二根手指。这是就产生了最终的版本,是我在GcsSloop的代码基础上加以改进的,如下:

public class DragViewFinal extends View {String TAG = "DragViewFinal";Bitmap mBitmap;         // 图片RectF mBitmapRectF;     // 图片所在区域Matrix mBitmapMatrix;   // 控制图片的 matrixboolean canDrag = false;PointF lastPoint = new PointF(0, 0);private Paint mDeafultPaint;public DragViewFinal(Context context) {this(context, null);}public DragViewFinal(Context context, AttributeSet attrs) {super(context, attrs);mDeafultPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);// 调整图片大小BitmapFactory.Options options = new BitmapFactory.Options();options.outWidth = 960 / 2;options.outHeight = 800 / 2;mBitmap = BitmapFactory.decodeResource(this.getResources(), R.drawable.poly_test, options);mBitmapRectF = new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight());mBitmapMatrix = new Matrix();}private int mActivePointerId;/*** A null/invalid pointer ID.*/private final int INVALID_POINTER = -1;// 记录活动手指的id(activePointerId),通过此ID获取move事件的坐标。// 在手指按下的时候,记录下activePointerId// 第二根手指按下的时候,更新activePointerId。(我们让第二根手指作为活动手指,忽略第一个手指的move)// 当其中一根手指抬起时,如果是第一根手指,那么不做处理,如果是第二根手指抬起,也就是活动手指抬起的话,将活动手指改回第一根。@Overridepublic boolean onTouchEvent(MotionEvent event) {final int action = event.getActionMasked();final int actionIndex = event.getActionIndex();switch (action) {case MotionEvent.ACTION_DOWN:// 判断按下位置是否包含在图片区域内if (mBitmapRectF.contains((int) event.getX(), (int) event.getY())) {mActivePointerId = event.getPointerId(0);Log.d("ACTION_DOWN", "mActivePointerId = " + mActivePointerId);canDrag = true;lastPoint.set(event.getX(0), event.getY(0));}break;case MotionEvent.ACTION_POINTER_DOWN:// 将新落下来那根手指作为活动手指mActivePointerId = event.getPointerId(actionIndex);lastPoint.set(event.getX(actionIndex), event.getY(actionIndex));Log.d("ACTION_POINTER_DOWN", "mActivePointerId = " + mActivePointerId);break;case MotionEvent.ACTION_POINTER_UP:if (mActivePointerId == event.getPointerId(actionIndex)) { // 如果松开的是活动手指, 让还停留在屏幕上的最后一根手指作为活动手指// This was our active pointer going up. Choose a new// active pointer and adjust accordingly.// pointerIndex都是像0, 1, 2这样连续的final int newPointerIndex = actionIndex == 0 ? 1 : 0;mActivePointerId = event.getPointerId(newPointerIndex);lastPoint.set(event.getX(newPointerIndex), event.getY(newPointerIndex));Log.d("ACTION_POINTER_UP", "松开的是活动手指");}Log.d("ACTION_POINTER_UP", "mActivePointerId = " + mActivePointerId);break;case MotionEvent.ACTION_UP: // 代表用户的最后一个手指离开了屏幕mActivePointerId = INVALID_POINTER;canDrag = false;Log.d("ACTION_UP", "mActivePointerId = " + mActivePointerId);case MotionEvent.ACTION_MOVE:if (mActivePointerId == INVALID_POINTER) {Log.e("ACTION_MOVE", "Got ACTION_MOVE event but don't have an active pointer id.");return false;}if (canDrag) {final int pointerIndex = event.findPointerIndex(mActivePointerId);mBitmapMatrix.postTranslate(event.getX(pointerIndex) - lastPoint.x, event.getY(pointerIndex) - lastPoint.y);// 更新上一次点位置lastPoint.set(event.getX(pointerIndex), event.getY(pointerIndex));// 更新图片区域mBitmapRectF = new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight());mBitmapMatrix.mapRect(mBitmapRectF);invalidate();}break;}return true;}@Overrideprotected void onDraw(Canvas canvas) {canvas.drawBitmap(mBitmap, mBitmapMatrix, mDeafultPaint);}
}

注意MotionEvent.ACTION_POINTER_DOWN和MotionEvent.ACTION_POINTER_UP事件:在新的一根手指落下来的时候,将这根新的手指作为活动手指,记录它的pointerId并且更新lastPoint的坐标;在其中一根手指抬起的时候进行判断,如果抬起的是非活动手指,那就不要管,如果抬起的时候活动手指,那就把其他的手指作为活动手指。选择新的活动手指的时候,我这里简单粗暴的用了final int newPointerIndex = actionIndex == 0 ? 1 : 0这样的形式,因为我暂时想不到更好的方法了,如果你有,请告诉我。效果如下:

多点触控了解的差不多了,接下来我就对那个只支持单点触控的弹性ScrollView进行改进了,但是因为这里涉及到了一些其他的知识,就不贴代码了,但对于多点触控的处理还是核心的那几步。其实写代码都是那样,关键在于理解,理解了之后便能够灵活运用,万变不离其宗。这里只贴出一个改进后的控件的效果:


github地址

最后是国际惯例,给出demo的github地址MutiTouchDemo

作者:mundane
链接:https://www.jianshu.com/p/f8ef2685716d
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

Android多点触控最佳实践相关推荐

  1. Android多点触控详解

    本文转载自GcsSloop的 安卓自定义View进阶-多点触控详解 的文章 Android 多点触控详解,在前面的几篇文章中我们大致了解了 Android 中的事件处理流程和一些简单的处理方案,本次带 ...

  2. 模拟Android多点触控

    Android多点触控 Android多点触控 多点触控实现思路 第一种adb shell input方式 第二种adb shell sendevent方式 多点触控实现思路   经过资料的查询,要在 ...

  3. Android多点触控揭秘

    本文原创,转载请注明:http://blog.csdn.net/cloudzfy1/article/details/6582707 Google 暑期大学生博客分享大赛 - 2011 Android ...

  4. Android多点触控技术

    1 简介 Android多点触控在本质上需要LCD驱动和程序本身设计上支持,目前市面上HTC.Motorola和Samsung等知名厂商只要使用电容屏触控原理的手机均可以支持多点触控Multitouc ...

  5. Android 多点触控消息捕获与处理

    1 简介 Android多点触控在本质上需要LCD驱动和程序本身设计上支持,目前市面上HTC.Motorola和Samsung等知名厂商只要使用电容屏触控原理的手机均可以支持多点触控Multitouc ...

  6. Android多点触控之——MotionEvent(触控事件)

    今天晚上刚学习了一个多点触控的小程序,后面想对其做一个定制.在写的时候遇到很多问题,于是乎就查了一下API文档,又到网上查了一下高手的文章,最后自己又实践了一下.终于把多点触控事件监听的大概原理给弄清 ...

  7. Android多点触控之ZoomImageView完全解析

    ZoomImageView是一个类似photoview的图片预览控件,实现了对图片的手势放大缩小平移,以及双击放大缩小解决和viewpager滑动冲突等功能,主要是通过GestureDetector, ...

  8. Android 多点触控 MotionEvent详解

    相关API 介绍 MotionEvent.getY() 和 MotionEvent.getRawY() 的区别 getY 表示触摸事件在当前的View内的Y 坐标, getRawY表示触摸事件在整个屏 ...

  9. Android多点触控MultiTouch浅析

    申明: 参考:http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2013/0226/914.html 下面实现如何通过应用层支持多点触控操作, ...

  10. android 多点触控缩放,Android多点触控(图片的缩放Demo)

    本文主要介绍Android的多点触控,使用了一个图片缩放的实例,来更好的说明其原理.需要实现OnTouchListener接口,重写其中的onTouch方法. 实现效果图: 源代码: 布局文件: ac ...

最新文章

  1. gdal 1.9+python 2.7开发环境配置
  2. 量子计算竞速时代,如何拨动时间的指针
  3. Java学习笔记_字符串/静态static
  4. win10你的组织已关闭自动更新问题怎么解决?
  5. SQL JOIN--初级篇
  6. Spring Security(3)
  7. 玩物得志:效率为王!如何构建大数据平台?
  8. UvaLA 4670 Dominating Patterns
  9. android 分屏切换流程,一种切换分屏模式和多窗口模式的方法与流程
  10. SpringBoot知识点整理
  11. 在OCI中为计算实例添加第二块网卡
  12. win2012服务器 注册表,Windows Server2012删除或添加开机启动项的方法
  13. 关于android 在黑屏情况下wifi下载速度慢的问题的解决。
  14. 重拾编程之路--jeetcode(java)--ZigZag Conversion
  15. kali安装keylogger_小白日记48:kali渗透测试之Web渗透-XSS(二)-漏洞利用-键盘记录器,xsser...
  16. 智控网络——智谋云价签,与智慧门店同飞跃、共变革
  17. Apache-mina学习笔记,非常全都资料,附带大量实例
  18. mysql数据转化为rdf_灏嗗叧绯绘暟鎹浆鎹负 RDF 鏍煎紡
  19. 2021年01月虹科Pico汽车示波器简报
  20. 输入10个人的名字,按从大到小排序输出

热门文章

  1. h5-吸顶效果的实现方法
  2. 如何制作ISO镜像文件
  3. Tor出现需要控制密码的解决办法
  4. 树莓派建立无线热点AP (方便直接连接遥控机器人/摄像头)
  5. Adobe flash player ActiveX、NPAPI、PPAPI 的区别
  6. 自学三个月编写简单走迷宫游戏
  7. unity漂移 unity3d教程 // WheelCollider
  8. getchar函数详解看这一篇就够了-C语言(函数功能、使用、返回值)
  9. 基于不确定性量化的非精确概率可靠性理论
  10. mysql登陆案例_Mysql用户登陆验证过程 案例