在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,布布扣,bubuko.com