Android滑动冲突解决方法(一)

作者: IAM四十二 | 来源:发表于2016-03-30 23:17 被阅读16568次

    叙述

    滑动冲突可以说是日常开发中比较常见的一类问题,也是比较让人头疼的一类问题,尤其是在使用第三方框架的时候,两个原本完美的控件,组合在一起之后,忽然发现整个世界都不好了。

    关于滑动冲突

    滑动冲突分类###

    滑动冲突,总的来说就是两类。

    1. 同方向滑动冲突
      比如ScrollView嵌套ListView,或者是ScrollView嵌套自己

    2. 不同方向滑动冲突
      比如ScrollView嵌套ViewPager,或者是ViewPager嵌套ScrollView,这种情况其实很典型。现在大部分应用最外层都是ViewPager+Fragment 的底部切换(比如微信)结构,这种时候,就很容易出现滑动冲突。不过ViewPager里面无论是嵌套ListView还是ScrollView,滑动冲突是没有的,毕竟是官方的东西,可能已经考虑到了这些,所以比较完善。

    复杂一点的滑动冲突,基本上就是这两个冲突结合的结果。

    滑动冲突解决思路###

    滑动冲突,就其本质来说,两个不同方向(或者是同方向)的View,其中有一个是占主导地位的,每次总是抢着去处理外界的滑动行为,这样就导致一种很别扭的用户体验,明明只是横向的滑动了一下,纵向的列表却在垂直方向发生了动作。就是说,这个占主导地位的View,每一次都身不由己的拦截了这个滑动的动作,因此,要解决滑动冲突,就是得明确告诉这个占主导地位的View,什么时候你该拦截,什么时候你不应该拦截,应该由下一层的View去处理这个滑动动作。

    这里不明白的同学,可以去了解一下Android Touch事件的分发机制,这也是解决滑动冲突的核心知识。

    第二种滑动冲突,解决起来是比较简单的。这里就结合例子说一下。

    滑动冲突

    这里,说一下背景情况。之前做下拉刷新、上拉加载更多时一直使用的是PullToRefreshView这个控件,因为很方便,不用导入三方工程。在其内部可以放置ListView,GridView及ScrollView,非常方便,用起来可谓是屡试不爽。但是直到有一天,因项目需要,在ListView顶部加了一个轮播图控件BannerView(这个可以参考之前写的一篇学习笔记)。结果发现轮播图滑动的时候,和纵向的下拉刷新组件冲突了。

    如之前所说,解决滑动冲突的关键,就是明确告知接收到Touch的View,是否需要拦截此次事件。

    解决方法

    解决方案1,从外部拦截机制考虑###

    这里,相当于是PullToRefreshView嵌套了ViewPager,那么每次优先接收到Touch事件的必然是PullToRefreshView。因为正常情况下,父控件会优先接收到touch事件。这样就清楚了,看代码:

    在PullToRefreshView的onInterceptTouchEvent方法中:

        @Override
        public boolean onInterceptTouchEvent(MotionEvent e) {
            int y = (int) e.getRawY();
            int x = (int) e.getRawX();
            boolean resume = false;
            switch (e.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    // 发生down事件时,记录y坐标
                    mLastMotionY = y;
                    mLastMotionX = x;
                    resume = false;
                    break;
                case MotionEvent.ACTION_MOVE:
                    // deltaY > 0 是向下运动,< 0是向上运动
                    int deltaY = y - mLastMotionY;
                    int deleaX = x - mLastMotionX;
    
                    if (Math.abs(deleaX) > Math.abs(deltaY)) {
                        resume = false;
                    } else {
                    //当前正处于滑动
                        if (isRefreshViewScroll(deltaY)) {
                            resume = true;
                        }
                    }
                    break;
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_CANCEL:
                    break;
            }
            return resume;
        }
    

    这里最关键的代码就是这行

    if (Math.abs(deleaX) > Math.abs(deltaY)) {
                        resume = false;
                    }
    

    横向滑动距离大于纵向时,无须拦截这次滑动事件,滑动事件会传递到下一层的view,也就是这里的轮播图控件,这样横向滑动轮播图的时候,PullToRefreshView就不会有下拉的动作了。其实,就是这么简单,但前提是你必须明确了解Android Touch事件的传递机制,期间各个方法执行的顺序及意义。

    ps: 关于上文中提到的isRefreshViewScroll 方法代码(这个方法其实是PullToRefreshView这个控件自带的一个方法)

    /** * 是否应该到了父View,即PullToRefreshView滑动 
    * 
    * @param deltaY , deltaY > 0 是向下运动,< 0是向上运动
     * @return
     */
    private boolean isRefreshViewScroll(int deltaY) {
            if (mHeaderState == REFRESHING || mFooterState == REFRESHING) {
                return false;
            }
            // 对于ListView和GridView
            if (mAdapterView != null) {
                // 子view(ListView or GridView)滑动到最顶端
                if (deltaY > 0) {
                    View child = mAdapterView.getChildAt(0);
                    if (child == null) {
                        // 如果mAdapterView中没有数据,不拦截
                        return false;
                    }
                    if (mAdapterView.getFirstVisiblePosition() == 0
                            && child.getTop() == 0) {
                        mPullState = PULL_DOWN_STATE;
                        return true;
                    }
                    int top = child.getTop();
                    int padding = mAdapterView.getPaddingTop();
                    if (mAdapterView.getFirstVisiblePosition() == 0
                            && Math.abs(top - padding) <= 8) {// 这里之前用3可以判断,但现在不行,还没找到原因
                        mPullState = PULL_DOWN_STATE;
                        return true;
                    }
                } else if (deltaY < 0) {
                    View lastChild = mAdapterView.getChildAt(mAdapterView
                            .getChildCount() - 1);
                    if (lastChild == null) {
                        // 如果mAdapterView中没有数据,不拦截
                        return false;
                    }
                    // 最后一个子view的Bottom小于父View的高度说明mAdapterView的数据没有填满父view,
                    // 等于父View的高度说明mAdapterView已经滑动到最后
                    if (lastChild.getBottom() <= getHeight()
                            && mAdapterView.getLastVisiblePosition() == mAdapterView
                            .getCount() - 1) {
                        mPullState = PULL_UP_STATE;
                        return true;
                    }
                }
            }
            // 对于ScrollView
            if (mScrollView != null) {
                // 子scroll view滑动到最顶端
                View child = mScrollView.getChildAt(0);
                if (deltaY > 0 && mScrollView.getScrollY() == 0) {
                    mPullState = PULL_DOWN_STATE;
                    return true;
                } else if (deltaY < 0
                        && child.getMeasuredHeight() <= getHeight()
                        + mScrollView.getScrollY()) {
                    mPullState = PULL_UP_STATE;
                    return true;
                }
            }
            return false;
    

    解决方案2,从内容逆向思维分析###

    有时候,我们不想去修改或者是无法修改最先接收到Touch事件的View 时,比如这里我不想去修改PullToRefreshView的代码。就必须考虑从当前从Touch传递事件中最后的那个View逆向考虑。首先,由Android中View的Touch事件传递机制,我们知道Touch事件,首先必然由最外层View接收到,并很有可能被它拦截,如果无法更改这个最外层View,那么是不是就没辙了呢?其实不然,Android这么高大上的系统必然考虑到了这个问题,好了废话不说,先看代码

        private BannerView carouselView;
        private Context mContext;
    
        private PullToRefreshView refreshView;
        
    
        refreshView.setOnTouchListener(new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    carouselView.getParent().requestDisallowInterceptTouchEvent(false);
                    return false;
                }
            });
    
            carouselView.setOnTouchListener(new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    carouselView.getParent().requestDisallowInterceptTouchEvent(true);
                    int x = (int) event.getRawX();
                    int y = (int) event.getRawY();
    
                    switch (event.getAction()) {
                        case MotionEvent.ACTION_DOWN:
                            lastX = x;
                            lastY = y;
                            break;
                        case MotionEvent.ACTION_MOVE:
                            int deltaY = y - lastY;
                            int deltaX = x - lastX;
                            if (Math.abs(deltaX) < Math.abs(deltaY)) {
                                carouselView.getParent().requestDisallowInterceptTouchEvent(false);
                            } else {
                                carouselView.getParent().requestDisallowInterceptTouchEvent(true);
                            }
                        default:
                            break;
                    }
                    return false;
                }
            });
    

    首先说一下这个方法

    public abstract void requestDisallowInterceptTouchEvent (boolean disallowIntercept)
    

    Called when a child does not want this parent and its ancestors to intercept touch events with onInterceptTouchEvent(MotionEvent).
    This parent should pass this call onto its parents. This parent must obey this request for the duration of the touch (that is, only clear the flag after this parent has received an up or a cancel.

    Parameters
    disallowIntercept
    True if the child does not want the parent to intercept touch events.

    API里的意思很明确,子View如果不希望其父View拦截Touch事件时,可调用此方法。当disallowIntercept这个参数为true时,父View将不拦截。

    PS:这个方法的命名和其参数的使用逻辑,让我想到了一句很有意思的话,敌人的敌人就是朋友,真不知道Google的大神们是怎么想的,非要搞一个反逻辑。

    好了,言归正传。这里拦截直接也很明确,在carouselView的onTouch方法中每次进入就设定父View不拦截此次事件,然后在MOTION_MOVE时候,根据滑动的距离判断再决定是父View是否有权利拦截Touch事件(即滑动行为)。

    关键的处理逻辑就是这里:

    if (Math.abs(deltaX) < Math.abs(deltaY)) {
                                carouselView.getParent().requestDisallowInterceptTouchEvent(false);
                            } else {
                                carouselView.getParent().requestDisallowInterceptTouchEvent(true);
                            }
    

    这个结合上面对这个方法的解释,应该很好理解了,就不多做阐述了。


    好了,这里可以看到,解决这种滑动冲突的方法很简单,最根本的还是得充分了解Touch事件的传递机制,只有这样,才能明白该在哪里做什么事情。
    当然,横竖滑动的冲突很好理解,但同一方向的滑动冲突情况就有点复杂了,下次再说。

    相关文章

      网友评论

      • maxcion:想问一下,看了您的代码发现。外部拦截方案过去滑动距离是通过在down事件中记录起始位置。在move事件中相减获取活动距离。

        内部拦截方案获取滑动距离。是通过,在事件结束后记录last的位置。在move事件中相减。想知道为什么都是解决冲突却又这两种方案。 想知道为什么内部拦截方案要记录last的位置。谢谢了。
        IAM四十二:第一个问题:为什么又两种,这个文中说的很清楚了,外部拦截法有时候会有局限性和更强的耦合性。

        第二个问题:关于 last 记录上一次滑动位置的地方,这里你可以理解为这是伪代码,只是提供了一种思路,实际中你可能需要根据不同的场景在不同的位置去记录这个点,不一定在 最后或最前,甚至是有可能是在其他方法里。
      • 549c0e6ffcff:你好, 请问如果listview和Viewgroup (比如Scrollview和Coordinaterlayout) 不在同一个布局文件中, 要怎么处理滑动冲突呢? 谢谢! 比如Scrollview在Activity中, 而listview在Activity中的Fragment中.
        高级组装搬运工吴哥:android 的事件分发你要先了解一下! Fragment 也是通过addView()的形式添加到Activity的一个layout中,View是树形结构,由上到下,也是由里到外
      • 666swb:学习了,不错
      • b0560b63b4cc:isRefreshViewScroll(int deltaY)这个方法是怎么实现的,楼主能发出来么,着急着用1628075735@qq.com麻烦楼主能发一份这个类的源码给我吗,非常感谢。。。
        IAM四十二: @人心弗远 你的使用场景是什么样的?
        b0560b63b4cc:其实我用的是在Ultra-Pull-To-Refresh的基础上完善的版本:Common-Pull-To-Refresh,用的第二种方法还是不能解决滑动冲突的问题。
        IAM四十二:@b0560b63b4cc 早上才看到,你说的这个方法是这个控件原先就有的一个方法,文章已更新,如果还需要的话,可以查看。
      • 野狗道人闯红灯:写的很棒,解决了我的问题。
      • 3ee6e9ef0293:现在大部分应用最外层都是ViewPager+Fragment 的底部切换(比如微信)结构,这种时候,就很容易出现滑动冲突。不过ViewPager里面无论是嵌套ListView还是ScrollView,滑动冲突是没有的,毕竟是官方的东西,可能已经考虑到了这些,所以比较完善。

        这句话我觉得说不太合理,冲突产生的前提是必须在同一个xml的嵌套。viewpager+fragment的切换不存在冲突不是因为官方做了处理,而是viewpager一般在main的主页面,而fragment也有自己的界面。他们不是在同一个xml里面的。
        IAM四十二:@txfyteen 这里没有说清楚,ViewPager+Fragment当然是不会冲突,我的意思是在ViewPager里嵌套listview,scrollvie时不会冲突
      • Waikey:現在都用recycelview
      • zhuhf:很好,不过方案一文字描述有个点不是很准确,down事件不能称作拦截,因为它不能拦截。。
        IAM四十二:@hiphonezhu 嗯,感谢你的建议,那个注释那么写的确是有些误导人,action_down的确是无法被拦截的。已做出了更新。

      本文标题:Android滑动冲突解决方法(一)

      本文链接:https://www.haomeiwen.com/subject/kpdelttx.html