自定义 View 嵌套 ScrollView 产生滑动冲突的解决方案

主声明:

转载请在开头附加本文链接及作者信息,并标记为转载。本文由博主 威威喵 原创,请多支持与指教。

本文首发于此   博主威威喵  |  博客主页https://blog.csdn.net/smile_running

    自定义ViewGroup的时候,你一定会遇到这种情况——滑动冲突。发生这种情况的前提是你的自定义ViewGroup支持滚动,并且可能也内嵌了一个可以支持滚动的控件。例如:ViewPager嵌套了一个ScrollView、ListView等。当然,用系统的控件是已经处理好这些滚动冲突的。也许你不曾有个这样的经历,接下来我们来看看一个例子,了解一下什么叫滑动冲突吧。

    先来看看滑动冲突的效果:

  • 滑动冲突产生原因

    滑动冲突原因分析:外面自定义的ViewGroup自身支持横向滑动,然后内部嵌套了ScrollView就不行了。当我们在ScrollView上左右滑动的时候根本没反应,而在上方的ImageView是不支持滑动的,所以不会造成冲突。

    本例子沿用之前的一篇文章的部分代码,文章链接:结合scrollTo、Scroller、GestureDetector的使用方法,自定义ViewGroup打造ViewPager滑动效果

    先看一张草图

Activity(分发) ---> ViewGroup(不拦截,分发) ---> ScrollView(消费事件)

    Activity分发事件给外层的ViewGroup,ViewGroup不拦截继续分发给ScrollView。最后由ScrollView消费了onTouchEvent(),整个流程结束。那产生左右不滑动的原因就是:ScrollView消费了onTouchEvent(),导致ViewGroup接收不到onTouchEvent(),那么在ViewGroup的onTouchEvent()方法将得不到执行,所以左右无法响应滑动事件。

    如果对事件分发不理解的可以看这篇文章:理解View的事件分发、拦截和消费,处理事件冲突的必备技能

  • 滑动冲突解决

    既然有问题,我们就得解决问题。我们知道ViewGroup是有一个onInterceptTouchEvent()方法,这个方法的最重要的作用就是拦截当前事件。

思路:

    在onInterceptTouchEvent()拦截点击事件,判断手指左右滑动距离 > 上下滑动距离,我们规定这种情况为左右滑动,将此事件拦截,那么ViewGroup的onTouchEvent()将得到执行。我们看一下实现此思路的代码:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean isIntercept = false;
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                firstX = x;
                firstY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                distanceX = Math.abs(x - firstX);
                distanceY = Math.abs(y - firstY);
                if (distanceX >= distanceY) {
                    isIntercept = true;
                }else {
                    scrollIndex(currentIndex);
                }
                break;
            case MotionEvent.ACTION_UP:
                firstX = 0;
                firstY = 0;
                break;
        }
        return isIntercept;
    }

    首先,我们取得最初按下的点的x,y坐标,然后取得滑动后最末一点的x,y坐标做差,判断x,y滑动距离来确定是否是左右滑动还是上下滑动。

    我们的完整的ViewGroup代码:

/**
 * @Created by xww.
 * @Creation time 2018/8/13.
 */

public class MyViewPager extends ViewGroup {

    private int currentIndex;
    private int startX;
    private int endX;
    private Scroller mScroller;
    private int lastX;
    private int distanceX, distanceY;
    private int firstX, firstY;
    int count = 0;

    public MyViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
        mScroller = new Scroller(context);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            //对于每个子View进行布局
            View childView = getChildAt(i);
            childView.layout(i * getWidth(), t, (i + 1) * getWidth(), b);
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            view.measure(widthMeasureSpec, heightMeasureSpec);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                lastX = x;
                break;
            case MotionEvent.ACTION_MOVE:
                startX = firstX;
                endX = x;

                int dx = lastX - x;
                scrollBy(dx, getScrollY());
                lastX = x;
                break;
            case MotionEvent.ACTION_UP:
                int tempIndex = currentIndex;
                if (startX - endX > getWidth() / 2) {    //从右往左滑动
                    tempIndex++;
                } else if (endX - startX > getWidth() / 2) {    //从左往右滑动
                    tempIndex--;
                }
                scrollIndex(tempIndex);
                startX = 0;
                endX = 0;
                lastX = 0;
                count = 0;
                break;
        }
        return true;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean isIntercept = false;
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                firstX = x;
                firstY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                distanceX = Math.abs(x - firstX);
                distanceY = Math.abs(y - firstY);
                if (distanceX >= distanceY) {
                    isIntercept = true;
                } else {
                    scrollIndex(currentIndex);
                }
                break;
            case MotionEvent.ACTION_UP:
                firstX = 0;
                firstY = 0;
                break;
        }
        return isIntercept;
    }

    /**
     * 移动到指定页面
     */
    private void scrollIndex(int tempIndex) {
        //第一页,无法继续向左滑动
        if (tempIndex < 0) {
            tempIndex = 0;
        }
        //同理,最后一页无法向右滑动
        if (tempIndex > getChildCount() - 1) {
            tempIndex = getChildCount() - 1;
        }
        currentIndex = tempIndex;
        mScroller.startScroll(getScrollX(), 0, currentIndex * getWidth() - getScrollX(), 0);
        postInvalidate();
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), 0);
            postInvalidate();
        }
    }
}

    然后在MainActivity中添加的三个页面代码:

    private int[] imgs = {R.drawable.bg_01, R.drawable.bg_02};

    private void initViewPager() {
        for (int img : imgs) {
            AppCompatImageView imageView = new AppCompatImageView(getContext());
            imageView.setBackgroundResource(img);
            myViewpager.addView(imageView);
        }
        View view = LayoutInflater.from(getContext()).inflate(R.layout.fg_custom_viewpager_childview, null);
        myViewpager.addView(view);
    }

    那么我们看看滑动冲突解决了没有,运行下项目,拭目以待吧。

  • Bug产生原因 

    确实是可以了,但是我们在ScrollView里左右滑动的时候有一个bug,我们看手指点下去一瞬间,上一个页面瞬间跳到了我们手指上,这确实是一个重大bug,这也是一个令我头疼的一个问题,不过我想到了一个解决的办法。那么首先,出问题了我们肯定先得找到问题出在哪里,既然是滑动页面,当然和scrollBy()脱不了关系。我就打印了scrollBy()的移动坐标,然后发现这样一种情况。

一、在ScrollView区域里左右滑动,我简单截取了部分Log

08-17 07:02:51.043 15756-15756/com.example.x.mycustomviews I/-------: onInterceptTouchEvent: ACTION_DOWN      596
08-17 07:02:51.304 15756-15756/com.example.x.mycustomviews I/-------: onInterceptTouchEvent: ACTION_MOVE
08-17 07:02:51.317 15756-15756/com.example.x.mycustomviews I/-------: onTouchEvent: ACTION_MOVE     -603
08-17 07:02:51.334 15756-15756/com.example.x.mycustomviews I/-------: onTouchEvent: ACTION_MOVE     -2
08-17 07:02:51.352 15756-15756/com.example.x.mycustomviews I/-------: onTouchEvent: ACTION_MOVE     -1
08-17 07:02:51.800 15756-15756/com.example.x.mycustomviews I/-------: onTouchEvent: ACTION_UP

二、在ImageView区域里左右滑动,我也简单截取了部分Log

07:02:54.746 15756-15756/com.example.x.mycustomviews I/-------: onInterceptTouchEvent: ACTION_DOWN    473
08-17 07:02:54.746 15756-15756/com.example.x.mycustomviews I/-------: onTouchEvent: ACTION_DOWN
08-17 07:02:54.800 15756-15756/com.example.x.mycustomviews I/-------: onTouchEvent: ACTION_MOVE     -2
08-17 07:02:54.867 15756-15756/com.example.x.mycustomviews I/-------: onTouchEvent: ACTION_MOVE     -5
08-17 07:02:55.367 15756-15756/com.example.x.mycustomviews I/-------: onTouchEvent: ACTION_MOVE     -5
08-17 07:02:55.544 15756-15756/com.example.x.mycustomviews I/-------: onTouchEvent: ACTION_UP

    为了更加直观,我用颜色标记了两者不同之处,现在我们来分析一下出现这种情况的原因。

分析:

    在ScrollView区域里左右滑动时,发现onTouchEvnet()事件居然没ACTION_DOWN,也就意味了我们标记不到onTouchEvnet()起始的x坐标,那么它将沿用onInterceptTouchEvent()保留下来的x坐标。所以,我们在滑动一瞬间才会看到瞬间移动的效果,这是因为scrollBy(dy,0);中的dy已经被onInterceptTouchEvent()的值替代,由于这个值可能非常大(手指位置决定),这将造成突然的瞬间移动到手指点击的那里。

解决方法:

    我们修改onTouchEvnet()的ACTION_MOVE事件的部分代码,代码修改结果如下:

            case MotionEvent.ACTION_MOVE:
                startX = firstX;
                endX = x;

                if (count == 0) {
                    count++;
                } else {
                    int dx = lastX - x;
                    scrollBy(dx, getScrollY());
                }
                lastX = x;
                break;

    首先定义一个count数值,这个做法是屏蔽掉第一次增大的效果,因为后面的移动都是正常的。那么,我们来看看效果如何?

  • 另一种方式实现

    经过测试,我们的代码已经实现了,不会出现瞬间移动的情况。当然,这是我个人的做法,其实我们还可以用GestureDetector(手势识别器),那么我们的代码将改为这样:

/**
 * @Created by xww.
 * @Creation time 2018/8/13.
 */

public class MyViewPager extends ViewGroup {

    private int currentIndex;
    private int startX;
    private int endX;
    private Scroller mScroller;
    private int lastX;
    private int distanceX, distanceY;
    private int firstX, firstY;

    private GestureDetector gestureDetector;

    public MyViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
        mScroller = new Scroller(context);

        gestureDetector = new GestureDetector(getContext(), new GestureDetector.OnGestureListener() {
            @Override
            public boolean onDown(MotionEvent e) {
                return false;
            }

            @Override
            public void onShowPress(MotionEvent e) {

            }

            @Override
            public boolean onSingleTapUp(MotionEvent e) {
                return false;
            }

            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
                scrollBy((int) distanceX, getScrollY());
                return true;
            }

            @Override
            public void onLongPress(MotionEvent e) {

            }

            @Override
            public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
                return false;
            }
        });
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            //对于每个子View进行布局
            View childView = getChildAt(i);
            childView.layout(i * getWidth(), t, (i + 1) * getWidth(), b);
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        for (int i = 0; i < getChildCount(); i++) {
            View view = getChildAt(i);
            view.measure(widthMeasureSpec, heightMeasureSpec);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        gestureDetector.onTouchEvent(event);
        int x = (int) event.getX();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                startX = x;
                break;
            case MotionEvent.ACTION_MOVE:
                endX = x;
                break;
            case MotionEvent.ACTION_UP:
                int tempIndex = currentIndex;
                if (startX - endX > getWidth() / 2) {    //从右往左滑动
                    tempIndex++;
                } else if (endX - startX > getWidth() / 2) {    //从左往右滑动
                    tempIndex--;
                }
                scrollIndex(tempIndex);
                break;
        }
        return true;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        gestureDetector.onTouchEvent(ev);
        boolean isIntercept = false;
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                firstX = x;
                firstY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                distanceX = Math.abs(x - firstX);
                distanceY = Math.abs(y - firstY);
                if (distanceX >= distanceY) {
                    isIntercept = true;
                } else {
                    scrollIndex(currentIndex);
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return isIntercept;
    }

    /**
     * 移动到指定页面
     */
    private void scrollIndex(int tempIndex) {
        //第一页,无法继续向左滑动
        if (tempIndex < 0) {
            tempIndex = 0;
        }
        //同理,最后一页无法向右滑动
        if (tempIndex > getChildCount() - 1) {
            tempIndex = getChildCount() - 1;
        }
        currentIndex = tempIndex;
        mScroller.startScroll(getScrollX(), 0, currentIndex * getWidth() - getScrollX(), 0);
        postInvalidate();
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), 0);
            postInvalidate();
        }
    }
}

    那么,实现效果也是如出一辙,我们也看看它的效果吧

相关推荐
©️2020 CSDN 皮肤主题: 成长之路 设计师:Amelia_0503 返回首页