在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()中我就可以简单地实现多任务手势的业务需求啦。

@Overridepublic void onItemClick(AdapterView<?> arg0, View arg1, int arg2, long arg3) {// TODO Auto-generated method stubif (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的触摸事件 */@Overridepublic 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);}/** *  */@Overridepublic 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;@Overridepublic boolean onTouchEvent(MotionEvent ev) {if (this.getChildCount() < 4)return false;return super.onTouchEvent(ev);}@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {// TODO Auto-generated method stubif (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;} elsereturn 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;} elsereturn 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都支持录屏了:


---------------------------------------------------------------------

2014/8/6,修改补充:

原文中的关键句代码有所修改:

if (fatherScrolling) {return false;}if (viewPagerScrolling) {return super.dispatchTouchEvent(ev);}float dx = Math.abs(xMove - xDown), dy = Math.abs(yMove - yDown);if (dx > 3 && dx > dy && this.getChildCount() >= 4) {this.getParent().requestDisallowInterceptTouchEvent(true);viewPagerScrolling = true;} else if (dy > 3 && dy > dx) {fatherScrolling = true;return false;} elsereturn false;
在经过大量尝试和Log观察后,发现原有的逻辑很可能影响我们的用户体验。

原来的方法有可能造成不响应滑动的情况。

新的方法,当我们检测到有一个方向的偏移大于3,且大于另一者偏移,就确定要竖直滑动还是横向滑动了,完全取决用户在两个方向的偏移,即更倾向于哪种滑动方式。

更多相关文章

  1. Android(安卓)源码分析问题(三)—— 通过事件分发完美解决嵌套滑动
  2. Android高级界面组件之拖动条和评星条的功能实现
  3. Android(安卓)生命周期
  4. 搜集整理的一些博客导航
  5. Android(安卓)Rild模块源码分析
  6. Android活动生命周期
  7. Android锁屏实现与总结
  8. android listview 嵌套ListView,子lv高度的问题
  9. 第26章、OnKeyListener键盘事件(从零开始学Android)

随机推荐

  1. RSA加密解密,String转PublicKey、PrivateK
  2. android 源码编译 问题 列表
  3. 解决com.android.builder.internal.aapt.
  4. Android学习笔记(二)android studio基本控
  5. Android NDK 提供的交叉工具链手动编译源
  6. android 栈方式退出
  7. cocos2d-x android 调试
  8. Android MediaRecorder 小结
  9. Android app的电子书翻页卷曲功能
  10. 使用命令行ls命令的Android文件浏览控件,