转载

关于android的ScrollView里面嵌入ListView的原理

整理之前的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是否滑动到顶部(这个地方也是个坑,下次再讲)。这样我们所要求的效果就实现了。

原文  http://danny-lau.com/2016/12/02/listview_in_scollview/
正文到此结束
Loading...