滑动冲突研究之ScrollView+ListView

来源:互联网 发布:如何在淘宝外卖开店 编辑:程序博客网 时间:2024/06/14 21:22

有一定经验的Android开发者应该都遇到过类似的需求,看图
scrollView+ListView

简单来说就是外层一个ScrollView,在内部又需要一个ListView来展示数据,在滑动时ListView上部的控件需要先收起来。
现在为了达到这种效果,主流是两种做法:
1.编写一个UnscrollListView,即不可滑动的ListView,然后将其嵌套在ScrollView中,这样就避免了滑动冲突的问题了。但是这样ListView的缓存优势就没了,需要将Adapter中的item全部绘制出来,对内存的影响是不可忽视的。
2.采用CoordinatorLayout相关的布局和控件,这个是Google推出的Design包里的东西,确实很好用,功能非常强大,而且对传统的控件几乎是一种颠覆,具体就不详细说了,但是也有一些限制,比如ListView必须要实现NestedScrollingChild接口,当然可以直接采用RecyclerView来代替。不过如果是在旧页面中修改的话,这工作量是挺大的,而且还需要额外引入各种Support包,导致apk体积变大。

如果是新界面或者新应用,建议直接采用CoordinatorLayout,以后就再也不怕UI给你提样式需求了。

所以,博主决定以身犯险,来探一探究竟能不能把方案1给优化一下。。。

1.初定方案

首先,我们的需求是在方案1的基础上做一些优化,最好是不需要改动ListView,将冲突在ScrollView中解决掉。因为ListView我们用得很多,也经常需要对它进行一些改动,ScrollView相对来说会稳定很多,基本就是充当一个容器的作用。
所以,我们决定修改ScrollView来解决冲突的问题。

在这里先简单说下滑动冲突的解决方案,一般来说可以分为外部解决内部解决两种方式。
外部解决,就是说在Parent中处理冲突,即重写父View的onInterceptTouchEvent方法,该方法就是判断父View是否需要拦截Event传递,我们的任务就是编写代码来设定什么时候要拦截,什么时候不拦截。
内部解决,是在child中去处理冲突,这个一般会在子View的onTouchEvent方法中去处理,因为ViewGroup有一个叫做requestDisallowInterceptTouchEvent的方法,可以设置父View是否拦截事件。

那结合我们上面的分析,我们就选择外部解决方案了。

2.处理滑动冲突

首先,需要新建一个ModifierScrollView,继承自ScrollView,然后重写onInterceptTouchEvent方法。
代码如下

    @Override    public boolean onInterceptTouchEvent(MotionEvent ev) {        super.onInterceptTouchEvent(ev);//ScrollView在该方法中进行了一些赋值操作        boolean isIntercept = false;        switch (ev.getAction()){            case MotionEvent.ACTION_DOWN:                y = ev.getRawY();                isIntercept = false;                break;            case MotionEvent.ACTION_MOVE:                float deltaY = y - ev.getRawY();                if (Math.abs(deltaY) > touchSlop && listener != null){                    if(listener.canScroll(deltaY)){ //ScrollView是否需要拦截                        isIntercept = true;                    }else{                        isIntercept = false;                    }                }                break;            case MotionEvent.ACTION_UP:                isIntercept = false;        }        return isIntercept;    }

大家看这个代码很简单,其实这是外部解决滑动冲突的一个公式,在这里感谢一下《Android开发艺术探索》,此书做了很完整的总结。
isIntercept为false就不拦截,若为ture则拦截事件。
之所以要在ACTION_DOWN中将isIntercept设为false,是因为如果ACTION_DOWN中拦截了事件,那么后续的事件都不会再去调用onInterceptTouchEvent方法判断,而是默认将事件拦截掉。具体后续会说到。

大家注意这句listener.canScroll(deltaY),在这里定义了一个接口,让外部去判断是否让ScrollView滑动,这样有更好的兼容性。
具体canScroll()方法的代码如下:

@Override            public boolean canScroll(float deltaY) {                if (deltaY > 0){ //上滑                    if (scrollView.isHeaderShow()){                        return true;                    }else{                        return false;                    }                }else{ //下滑                    if (listView.getFirstVisiblePosition() == 0){                        View child = listView.getChildAt(0);                        if (child.getTop() == 0){                            return true;                        }                    }                    return false;                }            }

这个方法是在具体的Activity中重写的,这里是对ListView的滑动冲突处理。
从代码可以看出,分为上滑和下滑两种情况:

上滑:
如果ScrollView的headerView还显示在屏幕上,则scrollView拦截事件
否则,不拦截事件

下滑:
如果ListView的第一条Item的top是0,则scrollView拦截事件
否则不拦截。

到这里,滑动冲突似乎已经解决掉了?
还有一点很重要,就是在ScrollView中的ListView必须通过代码来设置具体的高度,否则ScrollView是无法滚动的。这个应该好理解。

3.验证分析

将项目编译运行,发现结果并不是我想要的。
虽然ListView和ScrollView都可以正常滑动,但是博主想要的是连续的滑动,比如说一开始ListView想下滑动,当ListView滑动到顶部时,手指继续下滑则ScrollView继续下滑。
而按照上述的代码,ListView滑动到顶部时,必须抬起手指再次滑动,ScrollView才会下滑。
其实,需要解决的核心问题是,在ACTION_MOVE过程中,切换事件拦截者。

顺着上面的canScroll方法捋一捋思路,会发现这个需求已经包含在代码逻辑中,但是实际效果却是必须抬起手指才能切换事件拦截者,这是为什么呢?

于是,楼主去看了下ScrollView究竟是如何调用onInterceptTouchEvent方法的,调用时发生在ViewGroup的dispatchTouchEvent方法中,核心代码片段如下:

    final boolean intercepted;    if (actionMasked == MotionEvent.ACTION_DOWN                    || mFirstTouchTarget != null) {        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;        if (!disallowIntercept) {        //此处调用拦截方法,前面有3个条件需要满足              intercepted = onInterceptTouchEvent(ev);              ev.setAction(action); // restore action in case it was changed        } else {              intercepted = false;        }     } else {        intercepted = true;     }

可以看到要想调用onInterceptTouchEvent()需要满足至少两个条件
1.ACTION_DOWN 或者 mFirstTouchTarget != null
2.disallowIntercept为false
下面分别对它们进行说明,
ACTION_DOWN就不必多说了,就是手指按下的动作。
mFirstTouchTarget是什么呢?
从源码中可以发现,当某一个事件被子View消费掉那么mFirstTouchTarget就会被赋值,并且如果ViewGroup拦截了事件则会将mFirstTouchTarget设置为null。
至于disallowIntercept,就是requestDisallowInterceptTouchEvent()方法设置的。

好了,接下来可以开始分析问题了,我们还是分两种情况来分析:

a.从ListView滑动切换到ScrollView滑动
此时的手指状态是ACTION_MOVE所以ACTION_DOWN的条件是无法满足的,那么mFIrstTouchTarget呢?ListView是ScrollView的子View,并且ListView处理滑动即消费了Touch事件,所以mFirstTouchTarget不为null,条件满足。
之所以没有顺利切换,是因为如果ViewGroup不拦截事件,会将disallowIntercept(其实是一个FLAG)设置为true。此时我们只需更改这个Flag即可达到目的。
不过,还是会有一个bug,就是ScrollView的滑动起始点是在ACTION_DOWN时记录的,在ACTION_MOVE时切换到ScrollView滑动,那么ScrollView会出现跳变。暂且放一边。

b.从ScrollView滑动切换到ListView滑动
首先,ACTION_DOWN条件是无法满足的。
然后,因为ScrollView将事件拦截了,子View无法消费事件,所以mFirstTouchEvent==null。
这… 就难办了,难道人为的给它设置一个对象?这种做法显然太不优雅了。
其实分析到这里,博主已经确认不可能在项目中使用这种方案了,宁愿花点力气去引入Design包中的控件。

4.进一步方案

由于从上面的a情况中知道,由于ACTION_DOWN记录了ScrollView的起始点,在ACTION_MOVE状态切换回ScrollView滑动,这个滑动距离是ACTION_MOVE的y值 - ACTION_DOWN的y值,所以ScrollView会出现跳变。
而在b情况中,mFirstTouchTarget为null,导致无法进入onInterceptTouchEvent方法重新判断。
综合上面两点,我们提出一个大胆的思路:模拟ACTION_DOWN事件
即当ACTION_MOVE到达某种条件时,强行将ACTION_MOVE更改为ACTION_DOWN。
这样一来,onInterceptTouchEvent方法就可以进入了,就能正确的分配拦截的权力了。
当然,这样导致的后果,博主还未深究,暂时没有不良反应。不过,博主强烈不建议大家这样做。

具体做法就是,在自定义的ScrollView中,重写dispatchTouchEvent方法,然后对ACTION_MOVE进行处理,在需要切换事件拦截者时,将ACTION_MOVE更改为ACTION_DOWN,同时修改disallowIntercept标志。代码如下:

@Override    public boolean dispatchTouchEvent(MotionEvent ev) {        if (ev.getAction() == MotionEvent.ACTION_MOVE){            if (isDangerPoint && scrollDirectState > 0 ){                isDangerPoint = false;                ev.setAction(MotionEvent.ACTION_DOWN);                requestDisallowInterceptTouchEvent(false);            }else if (isChildScrollTop() && !isChildScrollTop){                isChildScrollTop = true;                ev.setAction(MotionEvent.ACTION_DOWN);                requestDisallowInterceptTouchEvent(false);            }        }        return super.dispatchTouchEvent(ev);    }
0 0
原创粉丝点击