整理之前的blog。
之前做一个自定义控件,很蛋疼的ScrollView里面套上ListView,而且要实现ScrollView和ListView的无缝滚动连接,就是ListView滚到头带动ScrollView ,同样的ScrollView滚到头带动ListView,滑动 ScrollView ,到某个临界点的时候(比如 ACTION_MOVE 到某个特殊点),ScrollView 不处理后续事件,转而将 MotionEvent 传递给另外一个 ListView (确切的说, ListView 是 ScrollView 的 Child View),希望能达到完美的滑动过渡效果。研究了很久的事件分发,这里记录一下,在网上查了很多资料,大部分都是讲ScrollView里面固定ListView的高度,而这边要实现的是滚动事件。
进入正题。关于touchEvent的分发 ,根据android的设计原则,是不推荐ScrollView里面嵌入ListView的,因为都是垂直滚动的控件。这边要介绍两个函数
onInterceptTouchEvent函数:
这是谷歌的官方注释,大体意思就是,这边函数决定这个viewGroup是否拦截这次触摸事件,当返回true时,就拦截到这次触摸事件,不再向Group内部的其他控件和View发送这次触摸事件。所以想要拦截触摸事件时就要用到这个函数,在ScrollView的源码中,这个值返回的是mIsBeingDragged,所以ScrollView在滑动时是返回True,也就意味着不再向子控件发送TouchEvent。
onTouchEvent函数:
这就是最基本的处理触摸事件的地方,触摸事件分三种 ACTION_DOWN , ACTION_MOVE ,ACTION_UP,代表手指的三种状态,点击,移动和抬起,这里就不多做介绍了,不懂得部分直接百度。这边就说一下返回True表明,捕获该事件,已经处理。若返回false就是不捕获,返回上层事件。
接下来讲一下android的触摸分发机制,知道了上面这两个函数有什么用还不够,还要了解他们的调用顺序,下面这张图也是网上看到的,言简意赅,分发的过程是一个U字行的过程。这边借用一下网上的图片。
如图所示,第一个处理触摸事件的其实是最底层的控件,如TextView之类的View类的控件,所以他们的调用顺序是最底层的View,然后他会去请求上层的ViewGroup的onIntercepetTouchEvent函数,若返回true则代表上层的ViewGroup捕获该事件,你就不需要处理了,若返回false就能够执行了,所以最先执行的其实是最外层的onInterceptTouchEvent函数。接着TextView处理onTouchEvent时,返回True则表示已经处理,false则返回上层。
按照这个思路去做的话,我们要实现的ListView就相当于最下面的TextView,只要在他的onTouchEvent里处理事件的分发就好了,由他来决定是否将触摸事件返回外层的ScrollView。但是!!这样只能实现从ListView将事件传送给ScrollView,并不能从ScrollView传送到ListView ,因为在事件分发时有以下原则:
Touch事件处理的几条基本原则:
1.如果在某个层级没有处理ACTION_DOWN,那么该层就再也收不到后续的Touch事件了,直到下ACTION_DOWN事件
说明:
a.某个层级没有处理某个事件指的是它以及它的子View都没有处理该事件
b.这条规则不适用于Activity层(它是顶层),他们可以收到每一个Touch事件。
c.没有处理ACTION_MOVE这类事件,不会有任何影响
2.如果ACTION_DOWN事件发生在某个View的范围之内,则后续的ACTION_MOVE,ACTION_UP和ACTION_CANCEL等事件都将发往该View,即使事件已经出界了
3.第一根按下的手指触发ACTION_DOWN事件,之后按下的手指触发ACTION_POINTER_DOWN事件,中间起来的手指触发ACTION_POINTER_UP事件,最后起来的手指触发ACTION_UP事件(即使它不是触发ACTION_DOWN事件的那根手指)。
4.pointer id可以跟踪手指,从按下的那个时刻起pointer id生效,直至起来的那一刻失效,这之间维持不变。
5.如果一个ACTION_DOWN事件被付View拦截了,则任何子View不会再收到任何Touch事件了(这符合第一点要求)
6.如果一个非ACTION_DOWN事件被父Vew拦截了,则那些上次处理了ACTION_DOWN事件的子View会收到一个ACTION_CANCEL事件,之后不会再收到任何Touch事件了,即使父View不再拦截后续的Touch事件。
7.如果父View决定处理Touch事件或者子View没有处理Touch事件,则父View按照普通View的处理方式处理Touch事件,否则它根本不处理Touch事件(它只负责分发)
8.如果父View在onInterceptTouchEvent中拦截事件,则onInterceptTouchEvent中不会再收到Touch事件了,事件被直接交给它自己处理
总结一下,就是onInterceptTouchEvent只能拦截ACTION_DOWN事件,意味着如果通过返回true来拦截子view的事件,接下来的ACTION_MOVE和ACTION_UP事件都是直接发送到onTouchEvent里面去处理,不再走onInterceptTouchEvent,所以在ACTION_MOVE的过程中是无法通过控制onInterceptTouchEvent来拦截事件的。其次,如果父View也就是我们的外层ScrollView,拦截了ACTION_MOVE事件,上次处理了ACTION_DOWN的子view都会收到一个ACTION_CANCEL事件,收到这个事件的View,在下次ACTION_DOWN事件之前,再也收不到touchEvent事件了。这个是相当坑爹的规则,之前想的很好都在ListView的ontouchEvent事件里做事件分发,因为他是第一个收到能处理的,结果只要外面的ScrollView开始滚动,他就会被Cancel掉,所以在父View滚动的时候是无法动态的把事件,在父View的TouchEvent返回false不处理,再通过系统的分发传回给ListView,因为ListView在这次touch中他已经被cancel掉了!!事件怎么都不会发过去。
于是在这里遇到的第一个瓶颈。
这里分析一下一次点击事件两个view所接收的事件。
ScrollView | ListView | |
---|---|---|
ACTION_DOWN | Incerpet收到返回false touchEvent处理 | Incerpet收到返回false touchEvent处理 |
ACTION_MOVE | Incerpet收到返回false touchEvent处理 移动之后Incerpet收到返回true touchEvent处理 Incerpet不再收到事件,touchEvent直接接收ACTION_MOVE事件 | Incerpet收到返回false touchEvent处理,外层移动之后 ,收到ACTION_CANCEL,不再接收任何消息 |
ACTION_UP | Incerper不接收事件 touchEvent直接接收ACTION_UP事件 | 不再接收任何消息 |
后来只能想其他的办法,在另外的地方做事件分发。
由上表分析,一次移动的过程中,只有一个函数是持续收到touchEvent函数的,那就是外层ScrollView的touchEvent函数。
所以这里就是突破口,将事件的分发在这里人工处理。
我们想要实现的效果是这样的:
Scrol[ListView]lView ListView在ScrollView 中间
外面是一个ScrollView,里面是一个ListView(ListView的底部其实和ScrollView是挨着的 这边画不出来),效果就是,初始化的时候ListView和ScrollView是置顶的,然后手指在ListView区域往上滑,因为ScrollView没有到底部,所以整个ScrollView会一起向上滑动,当ScrollView滑到底部时,无法在上滑时,则开始滑动内部的ListView,注意这里手指是没有离开屏幕的。同样的反过来,如果ListView没有到底部,在ListView里面开始向下滑,然后ListView里面到顶之后开始滑动ScrollView。
所以我们在ScrollView的touchEvent函数里面做手脚:
这里贴上代码:
final int action = ev. getAction(); final float y = ev. getY(); switch (action) { case MotionEvent .ACTION_DOWN : if (!mScroller .isFinished ()) { mScroller. abortAnimation(); } // Remember where the motion event started mLastMotionY = y; break; case MotionEvent .ACTION_MOVE : // Scroll to follow the motion event final int deltaY = ( int) ( mLastMotionY - y); mLastMotionY = y; if (deltaY < 0) { if (mListViewForScrollView != null && isAllowList) { if (!mListViewForScrollView .isReachTop ()) { MotionEvent temp = ev; if (mForOneDown ) { temp.setAction( MotionEvent.ACTION_DOWN ); mForOneDown = false ; } mListViewForScrollView.onTouchEvent (temp ); break; } } if (mInnerScrollView != null && isAllowScroll) { if (mInnerScrollView .getScrollY () != 0 ) { MotionEvent temp = ev; if (mForOneDown ) { temp.setAction( MotionEvent.ACTION_DOWN ); mForOneDown = false ; } mInnerScrollView.onTouchEvent (temp ); break; } } if (getScrollY () > mOffSet) { //预留一部分空间 scrollBy (0 , deltaY ); } } else if ( deltaY > 0 ) { if (mListViewForScrollView != null && isAllowList) { if (isReachBottom () ) { MotionEvent temp = ev; if (mForOneDown ) { temp.setAction( MotionEvent.ACTION_DOWN ); mForOneDown = false ; } mListViewForScrollView.onTouchEvent (temp ); break; } } if (mInnerScrollView != null && isAllowScroll) { if (isReachBottom () ) { MotionEvent temp = ev; if (mForOneDown ) { temp.setAction( MotionEvent.ACTION_DOWN ); mForOneDown = false ; } mInnerScrollView.onTouchEvent (temp ); break; } } final int bottomEdge = getHeight () - getPaddingBottom(); final int availableToScroll = getChildAt(0).getBottom() - getScrollY () - bottomEdge - mOffSet;//预留一部分空间 if (availableToScroll > 0) { scrollBy (0 , Math. min( availableToScroll, deltaY)); } } break; case MotionEvent .ACTION_UP : final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker .computeCurrentVelocity (1000 , mMaximumVelocity); int initialVelocity = (int) velocityTracker. getYVelocity(); if (mListViewForScrollView != null && isAllowList) { if (!mForOneDown ) { mForOneDown = true ; mListViewForScrollView.onTouchEvent (ev ); } } if (mInnerScrollView != null && isAllowScroll) { if (!mForOneDown ) { mForOneDown = true ; mInnerScrollView.onTouchEvent (ev ); } }
以上代码的意思就是在ScrollView的touchEvent中重写,将事件重新分发,这边在ScrollView也要重写,要添加一个ListView的引用。
主要处理还是在ACTION_MOVE和ACTION_UP里面。首先在ACTION_MOVE里面判断是往上滑还是往下滑,如果是往上滑,首先判断外面的ScrollView有没有到达底部,如果到达底部的话,就将touchEvent事件发送给ListView,这里要注意的是第一次发送给ListView时,要将MotionEvent事件的action种类从ACTION_MOVE变为ACTION_DOWN,因为根据设计的原则,之前ListView已经接收到了CANCEL事件,之后他是不会接受MOVE和UP事件的,所以这里要重新给List发送一个DOWN事件,相当于激活这个List,告诉他你要开始滑动了,接下来就直接给ListView发送MOVE事件了,此时ListView就开始滚动了,当ACTION_UP的时候同时要告诉ListView对你的ACTION也是UP了,如果不加的话就没有惯性效果。反过来向下滑也是相同的道理,只是将判断条件变为ListView是否滑动到顶部(这个地方也是个坑,下次再讲)。这样我们所要求的效果就实现了。