【朝花夕拾】Android自定义View篇之(九)多点触控(下)实践出真知
前言
转载请声明,转自【https://www.cnblogs.com/andy-songwei/p/11158972.html】谢谢!
在上一篇文章中,已经总结了MotionEvent以及多点触控相关的基础理论知识和常用的函数。本篇将通过实现单指拖动图片,多指拖动图片的实际案例来进行练习并实现一些效果,来理解前面的理论知识。要理解本文的代码,需要先掌握上一篇的理论知识,事件处理基础,以及一定的自定义View基础,这些我也在本系列文章的前几篇中讲过,有兴趣的可以按照本系列的顺序依次阅读学习,相信您一定会有不小的收获。
本文的主要内容如下:

一、实现单指拖动图片
要实现单指拖动图片,大致思路就是监控手指的ACTION_MOVE事件。手指移动过程中,获取事件的坐标,让图片根据坐标的变化来进行移动。具体代码实现如下,先自定义一个View,在其中处理单指拖动逻辑。
public class SingleTouchDragView extends View {
private static final String TAG = "songzheweiwang";
private Bitmap mBitmap;
private RectF mRectF;
private Matrix mMatrix;
private Paint mPaint;
private PointF mLstPointF;
private boolean mCanDrag = false;
public SingleTouchDragView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
mPaint = new Paint();
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.dog);
mRectF = new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
mMatrix = new Matrix();
mLstPointF = new PointF();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//判断按下位置是否在图片区域内
if (mRectF.contains(event.getX(), event.getY())) {
mCanDrag = true;
mLstPointF.set(event.getX(), event.getY());
}
break;
case MotionEvent.ACTION_UP:
mCanDrag = false;
case MotionEvent.ACTION_MOVE:
if (mCanDrag) {
//移动图片
mMatrix.postTranslate(event.getX() - mLstPointF.x, event.getY() - mLstPointF.y);
//更新触摸位置
mLstPointF.set(event.getX(), event.getY());
// 更新图片区域
mRectF = new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
mMatrix.mapRect(mRectF);
//刷新
invalidate();
}
break;
}
//注意这里需要返回true,因为当前自定义view继承自基类View,默认是无法消费触摸事件的
return true;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(mBitmap, mMatrix, mPaint);
}
}
代码逻辑比较简单,关键处也有这注释说明,这里就不多说了。使用该自定义View的布局如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"> <com.example.demos.customviewdemo.SingleTouchDragView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
用一根手指在图片上进行拖动,效果如下左图所示,图片随着手指在滑动:

效果很完美,但上述代码存在一个问题,就是在单指操作的情况下,可以正常被拖动,但是如果是多指操作的时候,就会混乱了。右图为两根手指滑动的图片的效果,因为两根手指都在移动, 导致ACTION_MOVE事件中,一会儿以第一根手指的触摸点为坐标,一会儿又以第二根手指的触摸点为坐标,这就导致图片频繁跳跃。
二、实现多指操作时只有第一根手指可以拖动图片
这一节我们在上述代码基础上,实现第一根手指在拖动图片时,另一根手指继续按下并拖动时无效,也就是第二根手指无法拖动,对第一根手指没有干扰。由于是多点触控,需要使用getActionMasked()来获取事件,并监听ACTION_POINTER_DOWN和ACTION_POINTER_UP事件。
public class MultiTouchDragView extends View {
private static final String TAG = "songzheweiwang";
private Bitmap mBitmap;
private RectF mRectF;
private Matrix mMatrix;
private Paint mPaint;
private PointF mLstPointF;
private boolean mCanDrag = false;
public MultiTouchDragView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
mPaint = new Paint();
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.dog);
mRectF = new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
mMatrix = new Matrix();
mLstPointF = new PointF();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
//pointerId为0的手指(即我们定义的第一根手指)按下在指定区域内
if (event.getPointerId(event.getActionIndex()) == 0 && mRectF.contains(event.getX(), event.getY())) {
mCanDrag = true;
//getX()和getY()没有传入参数时,默认传入的0
mLstPointF.set(event.getX(), event.getY());
}
break;
case MotionEvent.ACTION_MOVE:
if (mCanDrag) {
int pointerIndex = event.findPointerIndex(0);//第一根手指的pointerId为0
//这里需要注意,多手指频繁按下和抬起时可能会出现pointerIndex为-1的情况,如不处理,后面会报错
if (pointerIndex == -1) {
break;
}
mMatrix.postTranslate(event.getX(pointerIndex) - mLstPointF.x,
event.getY(pointerIndex) - mLstPointF.y);
mLstPointF.set(event.getX(pointerIndex), event.getY(pointerIndex));
mRectF = new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
mMatrix.mapRect(mRectF);
invalidate();
}
break;
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_UP:
if (event.getPointerId(event.getActionIndex()) == 0) {
mCanDrag = false;
}
break;
}
return true;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(mBitmap, mMatrix, mPaint);
}
}
由于我们要实现的效果是只有第一根手指可以拖动图片,所以在第29行和52行中,根据pointerId是否为0来判断是否需要更新界面。上一篇文章中说过,在处理多点触控事件时,要用pointerId来跟踪手指事件。由于第一根手指的pointerId为0,所以通过pointerId是否为0来判断是否为第一根手指。当有多根手指在屏幕上时,第一根手指抬起再按下,它仍然被认为是第一跟手指,此时触发的是ACTION_POINTER_DOWN事件,所以第一根手指按下,ACTION_POINTER_DOWN和ACTION_DOWN都有可能触发。如果判断是第一根手指按下了,就记录下它按下时的坐标,并设置mCanDrag为true,表示可以滑动。而手指抬起时,可能是最后一根抬起的手指,也可能不是,所以ACTION_POINTER_UP和ACTION_UP也都可能触发。如果检测到第一根手指抬起了,就设置mCanDrag为false,表示图片不能够再滑动了。在ACTION_MOVE事件中,第37行是固定使用,都需要根据findPointerIndex(int pinterId)来得到pointerIndex,因为获取指定手指事件坐标的函数传入的参数都是它。结合代码中的注释,剩下的逻辑应该就比较容易看懂了。
效果图如下,用两根手指来依次按下并拖动图片:

我们发现,只有第一根手指在滑动时,图片才会跟着移动,第二根手指(右边的手指)的滑动无效,完美!!!
三、实现两根手指共同拖动图片
上面实现的效果还不够,用户在使用中,第二根手指滑动时也能接替第一根手指继续滑动。基本思路大致是,记录当前活动手指的pointerId,ACTION_MOVE中以活动手指为基础来确定滑动操作。仍然在上述代码基础上修改来实现。
public class MultiTouchDragView2 extends View {
private static final String TAG = "songzheweiwang";
private Bitmap mBitmap;
private RectF mRectF;
private Matrix mMatrix;
private Paint mPaint;
private PointF mLstPointF;
private boolean mCanDrag = false;
private int mActivePointerId;
private final int INVALID_POINTER = -1;
public MultiTouchDragView2(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
mPaint = new Paint();
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.dog);
mRectF = new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
mMatrix = new Matrix();
mLstPointF = new PointF();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int actionIndex = event.getActionIndex();
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
//getX()和getY()没有传入参数时,默认传入的0
if (mRectF.contains(event.getX(), event.getY())) {
mActivePointerId = 0; //第一根手指按下时,pointerId和pointerIndex都为0
mCanDrag = true;
mLstPointF.set(event.getX(), event.getY());
}
break;
case MotionEvent.ACTION_POINTER_DOWN:
//有新落下的手指,则将新落下的手指作为活动手指,保存下活动手指的坐标
mActivePointerId = event.getPointerId(actionIndex);
mLstPointF.set(event.getX(actionIndex), event.getY(actionIndex));
break;
case MotionEvent.ACTION_MOVE:
if (mActivePointerId == INVALID_POINTER) {
break;
}
if (mCanDrag) {
//这里根据活动手指的pointerId来找到pointerIndex,而不再是固定的手指的pointerId了
int pointerIndex = event.findPointerIndex(mActivePointerId);
if (pointerIndex == -1) {
break;
}
mMatrix.postTranslate(event.getX(pointerIndex) - mLstPointF.x,
event.getY(pointerIndex) - mLstPointF.y);
mLstPointF.set(event.getX(pointerIndex), event.getY(pointerIndex));
mRectF = new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
mMatrix.mapRect(mRectF);
invalidate();
}
break;
case MotionEvent.ACTION_POINTER_UP:
//如果当前抬起的手指为活动手指,那么活动手指就传给留下的手指中pointerIndex最前面的一个
if (mActivePointerId == event.getPointerId(actionIndex)) {
int newPointerIndex = actionIndex == 0 ? 1 : 0;
mActivePointerId = event.getPointerId(newPointerIndex);
mLstPointF.set(event.getX(newPointerIndex), event.getY(newPointerIndex));
}
break;
case MotionEvent.ACTION_UP:
//最后一根手指也抬起来了
mActivePointerId = INVALID_POINTER;
mCanDrag = false;
break;
}
return true;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(mBitmap, mMatrix, mPaint);
}
}
由于需要依据活动的手指来拖动图片,所以需要实时记录下活动手指的坐标,如第40、54、65行所示。依然用两根手指依次拖动图片,效果如下所示:

现在可以看到,两根手指轮流正常拖动图片了,毫无违和感。
结语
到目前为止,多点触控相关的内容,我想讲的已经讲完了,上一篇讲理论,这一篇讲案例,难点其实主要就是pointerIndex和pointerId的理解和使用。希望通过这两篇文章,对读者理解多点触控有所帮助。由于文中代码结构比较简单,就没有必要提供源码了,读者自己建立好项目,把这些代码依次拷贝过去就可以了,非常简单。还有就是笔者比较穷,使用的免费软件,所以文中gif图上都打了水印,以后挣钱了也去享受一下付费服务,把水印给去掉。文中如果有描述不准确或不妥的地方,欢迎来拍砖,万分感谢,Bye!!!
参考文章
【朝花夕拾】Android自定义View篇之(九)多点触控(下)实践出真知的更多相关文章
- 【朝花夕拾】Android自定义View篇之(八)多点触控(上)MotionEvent简介
前言 在前面的文章中,介绍了不少触摸相关的知识,但都是基于单点触控的,即一次只用一根手指.但是在实际使用App中,常常是多根手指同时操作,这就需要用到多点触控相关的知识了.多点触控是在Android2 ...
- 【朝花夕拾】Android自定义View篇之(六)Android事件分发机制(中)从源码分析事件分发逻辑及经常遇到的一些“诡异”现象
前言 转载请注明,转自[https://www.cnblogs.com/andy-songwei/p/11039252.html]谢谢! 在上一篇文章[[朝花夕拾]Android自定义View篇之(五 ...
- 【朝花夕拾】Android自定义View篇之(四)自定义View的三种实现方式及自定义属性使用介绍
前言 转载请声明,转自[https://www.cnblogs.com/andy-songwei/p/10979161.html],谢谢! 尽管Android系统提供了不少控件,但是有很多酷炫效果仍然 ...
- Android自定义View(CustomCalendar-定制日历控件)
转载请标明出处: http://blog.csdn.net/xmxkf/article/details/54020386 本文出自:[openXu的博客] 目录: 1分析 2自定义属性 3onMeas ...
- Android自定义View(三、深入解析控件测量onMeasure)
转载请标明出处: http://blog.csdn.net/xmxkf/article/details/51490283 本文出自:[openXu的博客] 目录: onMeasure什么时候会被调用 ...
- (一)自定义ImageView,初步实现多点触控、自由缩放
真心佩服那些一直专注于技术共享的大神们,正是因为他们无私的分享精神,我才能每天都有进步.近日又算是仔细学了android的自定义控件技术,跟着大神的脚步实现了一个自定义的ImageView.里面涉及到 ...
- 【朝花夕拾】Android自定义View篇之(一)View绘制流程
前言 转载请申明转自[https://www.cnblogs.com/andy-songwei/p/10955062.html]谢谢! 自定义View.多线程.网络,被认为是Android开发者必须牢 ...
- 【朝花夕拾】Android自定义View篇之(五)Android事件分发机制(上)Touch三个重要方法的处理逻辑
前言 转载请注明,转自[https://www.cnblogs.com/andy-songwei/p/10998855.html]谢谢! 在自定义View中,经常需要处理Android事件分发的问题, ...
- 【朝花夕拾】Android自定义View篇之(十一)View的滑动,弹性滑动与自定义PagerView
前言 由于手机屏幕尺寸有限,但是又经常需要在屏幕中显示大量的内容,这就使得必须有部分内容显示,部分内容隐藏.这就需要用一个Android中很重要的概念——滑动.滑动,顾名思义就是view从一个地方移动 ...
随机推荐
- Angular route传参
从 router-link-page1 跳转 router-link-page2 和 router-link-page3 通过自定义路由 设置router-link-page2的路由后有3个参数,pa ...
- 关于JSON 字段数据的直接查询
最新的pgSQL 对json的支持在进一步加强!虽然我也学了那么点皮毛,但是json数据对于WEB的开发确实很重要,苦苦学习了很长一段时间,不断的关系PGSQL的动向! 好在翻看很多高人的例子和介绍, ...
- WPF - 图形设计器(Diagram Designer)
原文:WPF - 图形设计器(Diagram Designer) OpenExpressApp计划中包括建模工具,计划是采用MetaEdit+模型来作为元模型,使用codeproject的<WP ...
- Go 语言如果按这样改进,能不能火过 Java?
据 InfoWorld 消息,为改进 Go 语言的开发工具,Go 可能会获得自己的语言服务器,类似于 Microsoft 和 Red Hat 的语言服务器协议. 消息是从 Go 语言开发者的讨论组中流 ...
- 基于IOCP的高速文件传输代码
//服务端: const //transmit用的参数 TF_USE_KERNEL_APC = $20; //命令类型 CMD_CapScreen = ...
- VS2013环境里安装QT插件-“X86”与目标计算机类型“x64”冲突
在VS2013环境里搭载QT老是出现模块计算机类型“X86”与目标计算机类型“x64”冲突 2.解决方案2.1 项目右键,属性>配置管理选择>x64,没有的话新建:2.2 项目右键,属性 ...
- Oracle序列使用:建立、删除、使用
Oracle序列使用:建立.删除 在开始讲解Oracle序列使用方法之前,先加一点关于Oracle client sqlplus的使用,就是如果执行多行语句的话一定要加“/”才能表示结束,并执行!本篇 ...
- 设置windows2008系统缓存大小限制,解决服务器运行久了因物理内存耗尽出僵死(提升权限后,使用SetSystemFileCacheSize API函数,并将此做成了一个Service)
声明: 找到服务器僵死的原因了,原因是虚拟内存设置小于物理内存. 只要虚拟内存设置为系统默认大小就不会出生僵死的现象了. 当时因为服务器内存48G,系统默认虚拟内存大小也是48G, 觉得太占硬盘空间, ...
- cStor云存储、cProc云处理、cVideo云视频、cTrans云传输,云创个人网盘
http://www.cstor.cn,微信公众号:cstor_cn. 云创大数据是国际上云计算产品线齐全的企业之一,针对爆炸式增长的大数据需求,研发了自主知识产权的cStor云存储.cPr ...
- c# html网页源代码浏览器显示
//环境VS2008,WIN7SP1 //背景:人人网自动登陆,需要把读取到的html源代码显示出来, //test.txt 为html源代码 private void Form1_Load(obje ...