android滑动组件嵌套一般思路,多任务手势思路,触摸传递思路,【例】listview嵌套viewpager

在android UI开发中,我们经常会遇到这种需求:

两个支持滑动的组件,比如listview嵌套多个listview,listview的item是一个viewpager或gallary?亦或是scrollview嵌套scrollview等等。

一般情况下,你还可能需要支持如下几种功能:

¤ 两层组件都可以滑动

¤ 不让两个组件同时滑动,或者让两个组件同时滑动并可以自己调节

¤ 不影响底层view的子view和嵌套view的子view的点击事件


实现上述功能时,我们也经常遇到一些问题:

¤ 点击事件被屏蔽

¤ view的滑动不流畅(这里不讨论)

¤ view的滑动条件逻辑不符合设计

¤ 不知道怎么重写函数满足逻辑


这里笔者介绍一下解决这类需求的一般思路,以及一个支持多任务手势的listview和viewpager的需求案例。


触摸传递思路:

如果说,需求要把两个可以滑动的view嵌套在一起,那么要注意一些问题

¤ 像listview和scrollview,viewpager等可以滑动的组件,都是有自己的滑动规则的,我们最好不去重写怎么滑动它们(即最好不要去监听触摸的坐标用代码去滑动它们)。我们只要把我们需要的非滑动业务写好就可以了,当然我们也不能阻断默认滑动规则的执行


¤ viewgroup、view的事件分发传递机制需要特别清楚,你要知道,listview继承自viewgroup,当一串触摸事件发生时,当前activity收到这个事件,dispatch给顶层viewgroup,顶层viewgroup先是调用dispatchTouchEvent,该函数内部先在onInterceptTouchEvent函数决定是否要截断,如果选择截断,执行自己的onTouchEvent,子view不会接受到这个TouchEvent;如果不截断,该viewGroup会分发给所有点击范围内的子view(如果你不想分发给点击范围内的子view,你需要重写更多dispatchEvent的部分),即调用子view的dispatchTouchEvent,只要子view中有一个返回true(代表子View消费了这个事件),则该viewGroup不会执行这个TouchEvent,如果没有返回true,则调用该viewGroup的OnTouchEvent(),如果它返回false,说明没有消费掉这个事件,接着调用onClick,如果还是没有消费,则该viewgroup的dispatch函数会返回false。

这一套逻辑可能会很复杂,除却view没有onInterceptTouch以外,可以这么总结:

每个view收到事件,如果通过判断不决定阻断该事件,判断是否有一个子view要消费这个事件,如果没有,则执行onTouchEvent,即尝试自己消费,如果自己不消费,该view的dispatch就返回了false,表示该view包括该view的分支下没有消费该事件。


¤ 要非常明确业务需求,因为它要明确地写成代码形式,还要写在正确的地方(有时你可能会犹豫为了执行父view的一个多任务手势,应该在父view截断还是子view的dispatch返回一个false)

¤ 子view垄断父view事件:this.getParent().requestDisallowInterceptTouchEvent(true);该方法禁止父view阻断事件,即一定可以接受到事件,记得完成一套触摸时关闭这个。


多任务手势思路:

相信对读者来说,设计一个view的多任务手势并不是非常困难(重写onTouchEvent,记录按下、移动、抬起的坐标做一些相应的运算),但是这个问题放到viewq嵌套上,你可以考量了。你可能会遇到这样一些问题

¤ 如果子view消费了touchEvent,父view的任何行为不会被调用。

解决:

1.如果你希望父子view同时消费这个事件,你需要重写父view的dispatch并强行调用onTouchEvent。

2.如果这种情形,你不希望子view消费这个事件,有两个方案:重写父view的intercept,检查手势,阻断这个事件;重写子view的dispatch,检查手势,返回false。一个是父view强制阻断,一个是子view强制不消费。

3.如果你当前状态还并不明确应该由哪个view来消费这个事件,你大可以放任不管,直到判断出需要阻断或者消费(因为你业务需求对这种状态没有明确定义,就不需要去定义怎么处理了)。


一个简单的案例,附部分源码:

需求描述:

listview嵌套viewpager,listview的每一个item由xml布局定义,布局中包括一个viewpager和其他部分。

支持的操作:

¤ viewpager可以左右滑动,listview可以上下滑动,不会同时在滑动中,自然地,只有一开始点到的那个viewpager可以滑动,不会滑动其他viewpager。

¤ listview支持双指缩小操作。

¤ listview支持itemClick操作(不点击到viewpager中的图片)。

¤ viewpager中的图片支持点击操作。


先是listview的部分:

onItemClick定义了 我自己的业务事件,当点击每一个item并且没有被viewpager的图片view消费时触发。

在dispatchTouchEvent()中判断如果当前手指数大于等于2,即双指操作时,强行调用onTouchEvent,并返回,此时不会调用super.dispatchTouchEvent(),即事件不会走到子view里面去。

在onTouchEvent()中我就可以简单地实现多任务手势的业务需求啦。

@Override
	public void onItemClick(AdapterView<?> arg0, View arg1, int arg2, long arg3) {
		// TODO Auto-generated method stub
		if (mSet.get(arg2).size() > 1)
			this.downGranularity(arg2);
	}

	/**
	 * 自定义listview 用于事件分发的处理。
	 * @author ipip
	 *  2014年8月4日上午10:46:53
	 */
	private class MyListView extends ListView {
		public MyListView(Context context) {
			super(context);
			this.setDivider(null);
			// TODO Auto-generated constructor stub
		}

		/**
		 * 处理listView的触摸事件
		 */
		@Override
		public boolean onTouchEvent(MotionEvent ev) {
			switch (ev.getAction() & MotionEvent.ACTION_MASK) {
			case MotionEvent.ACTION_DOWN:
			case MotionEvent.ACTION_POINTER_DOWN:
				if (ev.getPointerCount() == 2)
					dst = measureFingers(ev);
				break;
			case MotionEvent.ACTION_UP:
			case MotionEvent.ACTION_POINTER_UP:

				if (ev.getPointerCount() == 2 && ndst < dst) {
					upGranularity();
					dst = -1;
				}
				break;

			case MotionEvent.ACTION_MOVE:
				if (ev.getPointerCount() >= 2) {
					ndst = measureFingers(ev);
				}
				break;
			}
			if (ev.getPointerCount() >= 2)
				return true;
			return super.onTouchEvent(ev);
		}
		/**
		 * 
		 */
		@Override
		public boolean dispatchTouchEvent(MotionEvent ev) {
			if (ev.getPointerCount() >= 2) {
				return onTouchEvent(ev);
			}
			return super.dispatchTouchEvent(ev);
		}
	}

接下来是viewpager的代码:

先解释一下我的onTouchEvent(),由于我的viewpager一行可以容纳4张图片(在适配器中重写getPageWidth()),所以当图片数小于4时,我不处理滑动事件,否则会出现图片瞬移闪烁的现象(其实这个挺有趣的,还不清楚什么原理)。

在dispatch中,首先判断是否双指操作。接着是记录第一次操作的位置以及每次移动时的判断。

public class MyViewPager extends ViewPager {

	public MyViewPager(Context context) {
		super(context);
		// TODO Auto-generated constructor stub
	}

	public MyViewPager(Context context, AttributeSet attrs) {
		super(context, attrs);
	}

	private float xDown;// 记录手指按下时的横坐标。
	private float xMove;// 记录手指移动时的横坐标。
	private float yDown;// 记录手指按下时的纵坐标。
	private float yMove;// 记录手指移动时的纵坐标。
	private boolean viewPagerScrolling = false;
	private boolean fatherScrolling = false;

	@Override
	public boolean onTouchEvent(MotionEvent ev) {
		if (this.getChildCount() < 4)
			return false;
		return super.onTouchEvent(ev);
	}

	@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		// TODO Auto-generated method stub
		if (ev.getPointerCount() >= 2)
			return false;

		switch (ev.getAction() & MotionEvent.ACTION_MASK) {
		case MotionEvent.ACTION_DOWN:
		case MotionEvent.ACTION_POINTER_DOWN:
			xDown = ev.getRawX();
			yDown = ev.getRawY();
			fatherScrolling = false;
			break;
		case MotionEvent.ACTION_MOVE:
			xMove = ev.getRawX();
			yMove = ev.getRawY();
			if (fatherScrolling) {
				return false;
			}
			if (viewPagerScrolling) {
				return super.dispatchTouchEvent(ev);
			}

			if (Math.abs(yMove - yDown) < 10 && Math.abs(xMove - xDown) > 3) {
				this.getParent().requestDisallowInterceptTouchEvent(true);
				viewPagerScrolling = true;
			} else if (Math.abs(yMove - yDown) >= 10) {
				fatherScrolling = true;
				return false;
			} else
				return false;
			break;
		case MotionEvent.ACTION_UP:
		case MotionEvent.ACTION_POINTER_UP:
			viewPagerScrolling = false;
			if (ev.getPointerCount() == 1)
				this.getParent().requestDisallowInterceptTouchEvent(false);
			break;
		}
		return super.dispatchTouchEvent(ev);
	}
}

上面的关键句:

			if (fatherScrolling) {
				return false;
			}
			if (viewPagerScrolling) {
				return super.dispatchTouchEvent(ev);
			}

			if (Math.abs(yMove - yDown) < 10 && Math.abs(xMove - xDown) > 3) {
				this.getParent().requestDisallowInterceptTouchEvent(true);
				viewPagerScrolling = true;
			} else if (Math.abs(yMove - yDown) >= 10) {
				fatherScrolling = true;
				return false;
			} else
				return false;

如果在该点有明显的横向移动,并且竖直方向一定在给定数值内,我判定我要执行viewpager的横向滑动操作,此时,我赋值viewPagerScrolling为true,那么接下来的所有move都会默认调用super.dispatchTouchEvent(),并且拒绝父view的阻断,即会应用默认的滑动方式直到一串事件的结束。如果竖直方向移动超过范围,并且之前横向移动不明显,那么我判定父view的listview要滑动,此时赋值fatherScrolling为ture,那么之后每次move都会返回false,即不消费之后所有事件。

当然了,在所有手指抬起后,这些状态被重置了。

之后,viewpager的每一个imageView都可以简单地设置一个onClickListener(),父view和爷view不会阻断它的点击事件。

简单叙述一下为什么:
¤ itemClick和imageView的click为什么没有被阻断?

前提是祖辈view们没有阻断它的事件,即祖辈view不会消费down-up,不会消费down-move-up。简单地说就是,像按下立即抬起这样的click的事件,两层view都不会将它消费掉,可以以默认方式顺利地传给子view。

¤ 为什么要在viewpager中判断是否有一定的横向移动?

如果直接让viewpager消费这个事件,父view便没有机会消费该事件了,我只有以一定的移动为基础,才能判断到底是竖着滑还是横着滑。

你会说我可以先让子view消费,当竖直方向移动太多就转交给listview消费。那么其实我还是要判断横向移动的距离,如果横向移动了100px,然后因为竖直移动了15px就不让viewpager消费跟业务需求不同,既然同样要计算横向距离,最好的方法应该就是先等待,时机成熟时锁定消费该事件的view直到事件链结束。

¤ 为什么在listview在dispatch中判断是否双指而不是在intercept?

呀,其实都可以的。。。

以下是效果图,用新版adt的Screen Recording录屏,好像从kitkat开始的adt都支持录屏了:

android滑动组件嵌套一般思路,多任务手势思路,触摸传递思路,【例】listview嵌套viewpager,,5-wow.com

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。