Android滑动冲突处理

作者: 奔跑吧李博 | 来源:发表于2017-12-14 18:49 被阅读169次

    在我们平时项目开发的过程中,是不是会遇到滑动View之间的相互嵌套,比如外部的Scrollview或SwipeRefreshLayout嵌套内部的Viewpager或横向Recyclerview,如外部Viewpager嵌套内部Viewpager。这样往往就会造成滑动的冲突导致不流畅甚至根本滑不动。事件分发机制见我的另一篇Android事件分发机制,用事实说话

    滑动冲突产生的两个根本原因:
    • 外部滑动方向与内部方向不一致
    • 外部滑动方向与内部方向一致
      扎心了,一不一致都可能造成滑动冲突。第一种如ScrollView 嵌套ViewPager,第二种如ViewPager嵌套ViewPager。

    解决方案:

    外部拦截法:

    从父View着手,重写onInterceptTouchEvent方法,在父View需要拦截的时候拦截事件,不需要则不拦截返回false。伪代码如下:

        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
    
            switch (ev.getAction()){
                case MotionEvent.ACTION_DOWN:
                    break;
                case MotionEvent.ACTION_MOVE:
                    if(父View拦截条件){
                        return true;
                    }
                    break;
            }
    
            return super.onInterceptTouchEvent(ev);
        }
    
    下面我们来看一个真实的案例:

    SwipeRefreshLayout嵌套ViewPager,需要流畅横向滑动Viewpager,这种操作下通常会顺带牵连到SwipeRefreshLayout致其拉动,如示例:

    giphy.gif

    用外部拦截法自定义一个MySwipeRefreshLayout,在开始滑动的条件下,判断手势的横纵方向的距离大小,判断用户到底是要横向滑动还是纵向滑动。纵向滑动意为拉动刷新,就拦截事件,横向滑动就意为滑动viewpager,就不拦截事件。解决效果:

    giphy.gif

    MySwipeRefreshLayout代码如下:

    import android.content.Context;
    import android.support.v4.widget.SwipeRefreshLayout;
    import android.util.AttributeSet;
    import android.view.MotionEvent;
    import android.view.ViewConfiguration;
    
    public class MySwipeRefreshLayout extends SwipeRefreshLayout{
        private float startX;
        private float startY;
        private float mTouchSlop;
    
        public MySwipeRefreshLayout(Context context) {
            super(context);
    
            mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        }
    
        public MySwipeRefreshLayout(Context context, AttributeSet attrs) {
            super(context, attrs);
    
            mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        }
    
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
    
            switch (ev.getAction()){
                case MotionEvent.ACTION_DOWN:
                    startX = ev.getX();
                    startY = ev.getY();
                    break;
                case MotionEvent.ACTION_MOVE:
                    float distanceX = Math.abs(ev.getX() - startX);
                    float distanceY = Math.abs(ev.getY() - startY);
                    if(distanceX > mTouchSlop && distanceX > distanceY){  //判断为横向滑动
                        return false;
                    }
    
                    break;
            }
    
            return super.onInterceptTouchEvent(ev);
        }
    }
    

    getScaledTouchSlop()表示能触发滚动的最小距离,如果小于这个距离就不触发移动控件。

        /**
         * @return Distance in pixels a touch can wander before we think the user is scrolling
         */
        public int getScaledTouchSlop() {
            return mTouchSlop;
        }
    
    内部拦截法:(getParent().requestDisallowInterceptTouchEvent(true),请求父容器不拦截事件)

    从子View入手,重写子元素的dispatchTouchEvent方法,父View先不要拦截任何事件,所有的事件传递给子View,如果子View需要此事件就消费掉,不需要此事件的话就通过requestDisallowInterceptTouchEvent(true)方法交给父View处理。伪代码如下:

       @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
    
            switch (ev.getAction()){
                case MotionEvent.ACTION_DOWN:
                    getParent().requestDisallowInterceptTouchEvent(true);
                    break;
                case MotionEvent.ACTION_MOVE:
                    if(事件交给父View条件){
                        getParent().requestDisallowInterceptTouchEvent(false);
                    }else{
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
                    break;
            }
            return super.onInterceptTouchEvent(ev);
        }
    
    

    父View需要重写:默认不拦截子View事件

        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            if(ev.getAction() == MotionEvent.ACTION_DOWN){
                return false;
            }else{
                return true;
            }
        }
    

    案例:Viewpager嵌套多个ViewPager:需求是默认滑动内部Viewpager,当内部Viewpager滑动到首页或者末页时,就滑动外部Viewpager切换页面。解决效果:

    giphy.gif

    自定义子Viewpager:

    import android.content.Context;
    import android.support.v4.view.ViewPager;
    import android.util.AttributeSet;
    import android.view.MotionEvent;
    
    public class InerViewPager extends ViewPager{
        private int itemCount;
    
        public InerViewPager(Context context) {
            super(context);
        }
    
        public InerViewPager(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
    
            switch (ev.getAction()){
                case MotionEvent.ACTION_DOWN:
                    getParent().requestDisallowInterceptTouchEvent(true);
                    break;
                case MotionEvent.ACTION_MOVE:
                    itemCount = getAdapter().getCount();
                    if(getCurrentItem() == 0 || getCurrentItem() == itemCount - 1){
                        getParent().requestDisallowInterceptTouchEvent(false);
                    }else{
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
                    break;
            }
            return super.onInterceptTouchEvent(ev);
        }
    
    }
    

    这里举的是基本栗子,那如果产品经理的想象力丰富,难度系数5.0以上呢?要是开发中遇到较复杂的滑动冲突,我只能说来一个杀一个,来两个杀一双!

    相关文章

      网友评论

      • 奔跑吧李博:嗯,默认父view不拦截事件,当子view不需要使用时间时,就请求父view拦截事件
      • 碎碎想:getParent().requestDisallowInterceptTouchEvent(true)指是否禁用掉事件拦截的功能吧?默认是false指的是不禁用,所以事件拦截中主要靠onInterceptTouchEvent这个方法来决定

      本文标题:Android滑动冲突处理

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