文档章节

自定义ViewGroup (2)支持滑动,并处理多指触摸可能产生的跳动问题

风荷举
 风荷举
发布于 2014/01/28 00:28
字数 2311
阅读 5025
收藏 31

昨天完成了一个支持设置margin,gravity,水平或者垂直排列的简单的自定义ViewGroup。但是它并不支持滑动,所以无法展现较多的内容。现在我们重写一下onTouchEvent(),来支持滑动。

重写onTouchEvent()以支持滑动:

要使View滑动,我们可以通过调用scrollTo()和scrollBy()来实现,这里需要注意的是:要使页面向左移动,需要增加mScrollX(就是向scrollBy传递一个正数),同样的,要使页面向上移动,需要增加mScrollY。

@Override
public boolean onTouchEvent(MotionEvent event) {
	final int action = event.getAction();

	if (BuildConfig.DEBUG)
		Log.d("onTouchEvent", "action: " + action);

	switch (action) {
	case MotionEvent.ACTION_DOWN:
		x = event.getX();
		y = event.getY();
		break;
	case MotionEvent.ACTION_MOVE:
		float mx = event.getX();
		float my = event.getY();

		//此处的moveBy是根据水平或是垂直排放的方向,
		//来选择是水平移动还是垂直移动
		moveBy((int) (x - mx), (int) (y - my));

		x = mx;
		y = my;
		break;
	
	}
	return true;
}

//此处的moveBy是根据水平或是垂直排放的方向,
//来选择是水平移动还是垂直移动
public void moveBy(int deltaX, int deltaY) {
	if (BuildConfig.DEBUG)
		Log.d("moveBy", "deltaX: " + deltaX + "    deltaY: " + deltaY);
	if (orientation == Orientation.HORIZONTAL) {
		if (Math.abs(deltaX) >= Math.abs(deltaY))
			scrollBy(deltaX, 0);
	} else {
		if (Math.abs(deltaY) >= Math.abs(deltaX))
			scrollBy(0, deltaY);
	}
}



好,现在我们再运行这段代码,就会发现View已经可以跟随手指移动了,但现在的问题是当手指离开屏幕后,View就立即停止滑动了,这样的体验就相当不友好,那么我们希望手指离开后,View能够以一定的阻尼满满地减速滑动。


借助Scroller,并且处理ACTION_UP事件

Scroller是一个用于计算位置的工具类,它负责计算下一个位置的坐标(根据时长,最小以最大移动距离,以及阻尼算法(可以使用自定义的Interpolator))。

Scroller有两种模式:scroll和fling。


  1. scroll用于已知目标位置的情况(例如:Viewpager中向左滑动,就是要展示右边的一页,那么我们就可以准确计算出滑动的目标位置,此时就可以使用Scroller.startScroll()方法
  2. fling用于不能准确得知目标位置的情况(例如:ListView,每一次的滑动,我们事先都不知道滑动距离,而是根据手指抬起是的速度来判断是滑远一点还是近一点,这时就可以使用Scroller.fling()方法)

现在我们改一下上面的onTouchEvent()方法,增加对ACTION_UP事件的处理,以及初速度的计算。


@Override
public boolean onTouchEvent(MotionEvent event) {
	final int action = event.getAction();

	if (BuildConfig.DEBUG)
		Log.d("onTouchEvent", "action: " + action);

	//将事件加入到VelocityTracker中,用于计算手指抬起时的初速度
	if (velocityTracker == null) {
		velocityTracker = VelocityTracker.obtain();
	}
	velocityTracker.addMovement(event);

	switch (action) {
	case MotionEvent.ACTION_DOWN:
		x = event.getX();
		y = event.getY();
		if (!mScroller.isFinished())
			mScroller.abortAnimation();
		break;
	case MotionEvent.ACTION_MOVE:
		float mx = event.getX();
		float my = event.getY();

		moveBy((int) (x - mx), (int) (y - my));

		x = mx;
		y = my;
		break;
	case MotionEvent.ACTION_UP:
		//maxFlingVelocity是通过ViewConfiguration来获取的初速度的上限
		//这个值可能会因为屏幕的不同而不同
		velocityTracker.computeCurrentVelocity(1000, maxFlingVelocity);
		float velocityX = velocityTracker.getXVelocity();
		float velocityY = velocityTracker.getYVelocity();

		//用来处理实际的移动
		completeMove(-velocityX, -velocityY);
		if (velocityTracker != null) {
			velocityTracker.recycle();
			velocityTracker = null;
		}
		break;
	return true;
}
我们在computeMove()中调用Scroller的fling()方法,顺便考虑一下滑动方向问题



private void completeMove(float velocityX, float velocityY) {
	if (orientation == Orientation.HORIZONTAL) {
		int mScrollX = getScrollX();
		int maxX = desireWidth - getWidth();// - Math.abs(mScrollX);

		if (Math.abs(velocityX) >= minFlingVelocity && maxX > 0) {
			
			mScroller.fling(mScrollX, 0, (int) velocityX, 0, 0, maxX, 0, 0);
			invalidate();
		}
	} else {
		int mScrollY = getScrollY();
		int maxY = desireHeight - getHeight();// - Math.abs(mScrollY);

		if (Math.abs(velocityY) >= minFlingVelocity && maxY > 0) {
			
			mScroller.fling(0, mScrollY, 0, (int) velocityY, 0, 0, 0, maxY);
			invalidate();
		}
	}
}
好了,现在我们再运行一遍,问题又来了,手指抬起后,页面立刻又停了下来,并没有实现慢慢减速的滑动效果。


其实原因就是上面所说的,Scroller只是帮助我们计算位置的,并不处理View的滑动。我们要想实现连续的滑动效果,那就要在View绘制完成后,再通过Scroller获得新位置,然后再重绘,如此反复,直至停止。

重写computeScroll(),实现View的连续绘制


@Override
public void computeScroll() {
	if (mScroller.computeScrollOffset()) {
		if (orientation == Orientation.HORIZONTAL) {
			scrollTo(mScroller.getCurrX(), 0);
			postInvalidate();
		} else {
			scrollTo(0, mScroller.getCurrY());
			postInvalidate();
		}
	}
}

computeScroll()是在ViewGroup的drawChild()中调用的,上面的代码中,我们通过调用computeScrollOffset()来判断滑动是否已停止,如果没有,那么我们可以通过getCurrX()和getCurrY()来获得新位置,然后通过调用scrollTo()来实现滑动,这里需要注意的是postInvalidate()的调用,它会将重绘的这个Event加入UI线程的消息队列,等scrollTo()执行完成后,就会处理这个事件,然后再次调用ViewGroup的draw()-->drawChild()-->computeScroll()-->scrollTo()如此就实现了连续绘制的效果。

现在我们再重新运行一下app,终于可以持续滑动了:),不过,当我们缓慢地拖动View,慢慢抬起手指,我们会发现通过这样的方式,可以使得所有的子View滑到屏幕之外,(所有的子View都消失了:()。

问题主要是出在completeMove()中,我们只是判断了初始速度是否大于最小阈值,如果小于这个最小阈值的话就什么都不做,缺少了边界的判断,因此修改computeMove()如下:

private void completeMove(float velocityX, float velocityY) {
	if (orientation == Orientation.HORIZONTAL) {
		int mScrollX = getScrollX();
		int maxX = desireWidth - getWidth();
		if (mScrollX > maxX) {
			// 超出了右边界,弹回
			mScroller.startScroll(mScrollX, 0, maxX - mScrollX, 0);
			invalidate();
		} else if (mScrollX < 0) {
			// 超出了左边界,弹回
			mScroller.startScroll(mScrollX, 0, -mScrollX, 0);
			invalidate();
		} else if (Math.abs(velocityX) >= minFlingVelocity && maxX > 0) {
			mScroller.fling(mScrollX, 0, (int) velocityX, 0, 0, maxX, 0, 0);
			invalidate();
		}
	} else {
		int mScrollY = getScrollY();
		int maxY = desireHeight - getHeight();

		if (mScrollY > maxY) {
			// 超出了下边界,弹回
			mScroller.startScroll(0, mScrollY, 0, maxY - mScrollY);
			invalidate();
		} else if (mScrollY < 0) {
			// 超出了上边界,弹回
			mScroller.startScroll(0, mScrollY, 0, -mScrollY);
			invalidate();
		} else if (Math.abs(velocityY) >= minFlingVelocity && maxY > 0) {
			mScroller.fling(0, mScrollY, 0, (int) velocityY, 0, 0, 0, maxY);
			invalidate();
		}
	}
}



ok,现在当我们滑出边界,松手后,会自动弹回。

处理ACTION_POINTER_UP事件,解决多指交替滑动跳动的问题

现在ViewGroup可以灵活的滑动了,但是当我们使用多个指头交替滑动时,就会产生跳动的现象。原因是这样的:

我们实现onTouchEvent()的时候,是通过event.getX(),以及event.getY()来获取触摸坐标的,实际上是获取的手指索引为0的位置坐标,当我们放上第二个手指后,这第二个手指的索引为1,此时我们同时滑动这两个手指,会发现没有问题,因为我们追踪的是手指索引为0的手指位置。但是当我们抬起第一个手指后,问题就出现了, 因为这个时候原本索引为1的第二个手指的索引变为了0,所以我们追踪的轨迹就出现了错误。

简单来说,跳动就是因为追踪的手指的改变,而这两个手指之间原本存在间隙,而这个间隙的距离就是我们跳动的距离。

其实问题产生的根本原因就是手指的索引会变化,因此我们需要记录被追踪手指的id,然后当有手指离开屏幕时,判断离开的手指是否是我们正在追踪的手指:

  1. 如果不是,忽略
  2. 如果是,则选择一个新的手指作为被追踪手指,并且调整位置记录。

还有一点就是,要处理ACTION_POINTER_UP事件,就需要给action与上一个掩码:event.getAction()&MotionEvent.ACTION_MASK 或者使用 event.getActionMasked()方法。

更改后的onTouchEvent()的实现如下:

@Override
public boolean onTouchEvent(MotionEvent event) {
	final int action = event.getActionMasked();

	if (velocityTracker == null) {
		velocityTracker = VelocityTracker.obtain();
	}
	velocityTracker.addMovement(event);

	switch (action) {
	case MotionEvent.ACTION_DOWN:
		// 获取索引为0的手指id
		mPointerId = event.getPointerId(0);
		x = event.getX();
		y = event.getY();
		if (!mScroller.isFinished())
			mScroller.abortAnimation();
		break;
	case MotionEvent.ACTION_MOVE:
		// 获取当前手指id所对应的索引,虽然在ACTION_DOWN的时候,我们默认选取索引为0
		// 的手指,但当有第二个手指触摸,并且先前有效的手指up之后,我们会调整有效手指

		// 屏幕上可能有多个手指,我们需要保证使用的是同一个手指的移动轨迹,
		// 因此此处不能使用event.getActionIndex()来获得索引
		final int pointerIndex = event.findPointerIndex(mPointerId);
		float mx = event.getX(pointerIndex);
		float my = event.getY(pointerIndex);

		moveBy((int) (x - mx), (int) (y - my));

		x = mx;
		y = my;
		break;
	case MotionEvent.ACTION_UP:
		velocityTracker.computeCurrentVelocity(1000, maxFlingVelocity);
		float velocityX = velocityTracker.getXVelocity(mPointerId);
		float velocityY = velocityTracker.getYVelocity(mPointerId);

		completeMove(-velocityX, -velocityY);
		if (velocityTracker != null) {
			velocityTracker.recycle();
			velocityTracker = null;
		}
		break;
	
	case MotionEvent.ACTION_POINTER_UP:
		// 获取离开屏幕的手指的索引
		int pointerIndexLeave = event.getActionIndex();
		int pointerIdLeave = event.getPointerId(pointerIndexLeave);
		if (mPointerId == pointerIdLeave) {
			// 离开屏幕的正是目前的有效手指,此处需要重新调整,并且需要重置VelocityTracker
			int reIndex = pointerIndexLeave == 0 ? 1 : 0;
			mPointerId = event.getPointerId(reIndex);
			// 调整触摸位置,防止出现跳动
			x = event.getX(reIndex);
			y = event.getY(reIndex);
			if (velocityTracker != null)
				velocityTracker.clear();
		}
			break;
		}
	return true;
}



好了,现在我们用多个手指交替滑动就很正常了。

不过当我们想为咱们的自定义的ViewGroup设置onClick和onLongClick事件时,发现并不支持。更奇怪的是当我们为子View设置了事件之后(例如click事件),我们的ViewGroup居然不能正常滑动了

上面第一个问题,我们需要在ACTION_UP中加一些处理,而第二个问题就需要重写onInterceptTouchEvent()方法,关于onInterceptTouchEvent()和onTouchEvent()之间的事件传递流程,就在明天的博客中再写吧:)




© 著作权归作者所有

风荷举
粉丝 11
博文 24
码字总数 24803
作品 0
朝阳
程序员
私信 提问
加载中

评论(2)

shikh
shikh
楼主有没有demo啊。一些初始数据不知道设多少
Summersize
Summersize
楼主的博文写的很棒,解释的很详细清楚,不知道何时我才能写出这样的代码啊~~
自定义ViewGroup (1)支持margin,gravity以及水平,垂直排列

最近在学习android的view部分,于是动手实现了一个类似ViewPager的可上下或者左右拖动的ViewGroup,中间遇到了一些问题(例如touchEvent在onInterceptTouchEvent和onTouchEvent之间的传递流程...

风荷举
2014/01/27
0
3
自定义ViewGroup (3) 与子View之间 Touch Event的拦截与处理

在昨天的博客(自定义ViewGroup(2))中,我们解决了多个手指交替滑动带来的页面的跳动问题。但同时也还遗留了两个问题。 我们自定义的这个ViewGroup本身还不支持onClick, onLongClick事件。 ...

风荷举
2014/01/29
0
1
Android6.0触摸事件分发机制解读

本篇博文是Android触摸事件分发机制系列博文的第一篇,带领大家从全局掌握Android触摸事件分发机制。特别声明的是,本源码解读是基于最新的Android6.0版本。 (一)Android6.0源码解读之Vie...

mynameishuangshuai
2016/10/24
0
0
1 View & 事件机制 & 滑动处理

1 位置参数与坐标系 坐标系 屏幕坐标系 父视图坐标系 自身坐标系 获取视图坐标 相对于屏幕坐标系: getLocationOnScreen() 相对于父视图坐标系(原始位置): getTop()/getLeft()/getRight()/ge...

画格子
2018/01/02
0
0
ViewGroup源码解读

我们之前刚刚分析完事件传递机制和view的源码,如果没有看过的,建议看完View的事件拦截机制浅析以及View的事件源码解析。这次我们来分析下viewgroup的。 可能有人会想,怎么又是源码分析,肯...

我就是马云飞
2017/11/16
0
0

没有更多内容

加载失败,请刷新页面

加载更多

微信小程序和百度小程序开发的一些不同点

1: initActive从onload放到onready中 2: bindtap='{{childTickeData.freeadd?"childticket":""}}',语法错误。会导致页面加载不出来,而且也不报任何错。 3:使用搬家工具,支付api名称转换......

醉雨
23分钟前
1
0
最近执行过的SQL语句查询

SELECT TOP 1000 ST.text AS '执行的SQL语句' , QS.execution_count AS '执行次数' , QS.total_elapsed_time / 10000 AS '耗时' , QS.total_logical_reads AS '逻辑读取次数' , QS.total_lo......

神手--追魂
24分钟前
2
0
从濒临解散到浴火重生,OceanBase 这十年经历了什么?

阿里妹导读:谈及国产自研数据库,就不得不提 OceanBase。与很多人想象不同的是,OceanBase 并非衔着金钥匙出生的宠儿。相反,它曾无人看好、困难重重,整个团队甚至数度濒临解散。 从危在旦...

阿里云官方博客
28分钟前
1
0
阿里开发者招聘节 | 面试题02-04:给定一个二叉搜索树(BST),找到树中第K小的节点

为帮助开发者们提升面试技能、有机会入职阿里,云栖社区特别制作了这个专辑——阿里巴巴资深技术专家们结合多年的工作、面试经验总结提炼而成的面试真题这一次将陆续放出(面试题官方参考答案...

阿里云云栖社区
44分钟前
3
0
使用Redis SETNX 命令实现分布式锁

基于setnx和getset http://blog.csdn.net/lihao21/article/details/49104695 使用Redis的 SETNX 命令可以实现分布式锁,下文介绍其实现方法。 SETNX命令简介 命令格式 SETNX key value 将 ke...

彬彬公子
45分钟前
2
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部