源码分析SwipeRefreshLayout子View侧滑冲突(侧滑菜单ListView,ViewPager)

先上干货,侧滑菜单listview和去除冲突的SwipeRefreshLayout github项目地址:                                            https://github.com/little-tongue/SidesplidListView

前段时间项目需要侧滑菜单的ListView,所以自己重写ListView仿qq的部分效果自定义了一个SideslipListView,在Dome里面运行正常,但是在使用的时候,发现经常侧滑有时候滑一半就失灵了,并且同时触发了SwipeRefreshLayout的下拉刷新CircleImageView的显示。反复试验了几次并配合log,得出问题:当SideslipListView到了顶部且侧滑的时候出现垂直方向滑动,会导致子View的滑动事件失效,SwipeRefreshLayout处理了滑动事件,显示顶部CircleImageView。因为我的SideslipListView是通过对触摸事件做处理实现侧滑的,所以我第一反应就是可能滑动冲突了。

源码分析SwipeRefreshLayout子View侧滑冲突(侧滑菜单ListView,ViewPager)_第1张图片

上面这张图相信所有人都烂熟于心了,简单分析可以知道SwipeRefreshLayout可能搞事情的地方是dispatchTouchEvent ,onInterceptTouchEvent。那就去看看源码中是如何实现。

SwipeRefreshLayout继承自ViewGroup,在SwipeRefreshLayout中没有重写dispatchTouchEvent,只重写了 onInterceptTouchEvent,所以只用看在onInterceptTouchEvent中怎么处理的。

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        ensureTarget();

        final int action = ev.getActionMasked();
        int pointerIndex;

        if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
            mReturningToStart = false;
        }

        if (!isEnabled() || mReturningToStart || canChildScrollUp()
                || mRefreshing || mNestedScrollInProgress) {
            // Fail fast if we're not in a state where a swipe is possible
            return false;
        }

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop());
                mActivePointerId = ev.getPointerId(0);
                mIsBeingDragged = false;

                pointerIndex = ev.findPointerIndex(mActivePointerId);
                if (pointerIndex < 0) {
                    return false;
                }
                mInitialDownY = ev.getY(pointerIndex);
                break;

            case MotionEvent.ACTION_MOVE:
                if (mActivePointerId == INVALID_POINTER) {
                    Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id.");
                    return false;
                }

                pointerIndex = ev.findPointerIndex(mActivePointerId);
                if (pointerIndex < 0) {
                    return false;
                }
                final float y = ev.getY(pointerIndex);
                startDragging(y);
                break;

            case MotionEvent.ACTION_POINTER_UP:
                onSecondaryPointerUp(ev);
                break;

            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mIsBeingDragged = false;
                mActivePointerId = INVALID_POINTER;
                break;
        }

        return mIsBeingDragged;
    }

可以看到最终返回的是mIsBeingDragged的值,mIsBeingDragged表示SwipeRefreshLayout是否开始下拉刷新的操作,即SwipeRefreshLayout顶部的CircleImageView是否开始显示。mIsBeingDragged的值是true时,就会导致SwipeRefreshLayout的子View不能接受到相应的事件。

在分Action处理事件之前有一段代码

if (!isEnabled() || mReturningToStart || canChildScrollUp()
                || mRefreshing || mNestedScrollInProgress) {
            // Fail fast if we're not in a state where a swipe is possible
            return false;
        }

这段代码判断了SwipeRefreshLayout是否可用,SwipeRefreshLayout的子View是不是滑动到了顶部,其中ListView的判断是canChildScrollUp(),这也就是为什么最开始产生的问题中,必须是SideSlipListView滑动到顶部的时候才会产生。

public boolean canChildScrollUp() {
    if (mChildScrollUpCallback != null) {
        return mChildScrollUpCallback.canChildScrollUp(this, mTarget);
    }
    if (mTarget instanceof ListView) {
        return ListViewCompat.canScrollList((ListView) mTarget, -1);
    }
    return mTarget.canScrollVertically(-1);
}

ACTION_DOWN :mIsBeingDragged的第一次赋值在ACTION_DOWN中赋值为false,ACTION_DOWN中其他的代码都是初始化一些参数,可以略过。

ACTION_MOVE:在ACTION_MOVE中获取了触摸的Y坐标,然后调用了startDrag个ing(y),跟踪过去。

 private void startDragging(float y) {
        final float yDiff = y - mInitialDownY;
        if (yDiff > mTouchSlop && !mIsBeingDragged) {
            mInitialMotionY = mInitialDownY + mTouchSlop;
            mIsBeingDragged = true;
            mProgress.setAlpha(STARTING_PROGRESS_ALPHA);
        }
    }

这里一目了然,把当前的y坐标和ACTION_DOWN中的起始y坐标求差,当Y轴的移动距离大于系统最小滑动距离的时候,会将mIsBeingDragged从false变成true。所以只要我们重写SwipeRefreshLayout的onInterceptTouchEvent方法,当滑动事件可判断为水平滑动的时候直接返回false,就可以解决SwipeRefreshLayout下子View的水平滑动冲突了。

     
    /**
     * 是否让子view处理touch事件
     */
    private boolean letChildDealTouchEvent;
    private float startX;
    private float startY;
    private int mTouchSlop;

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 记录手指按下的位置
                startY = ev.getY();
                startX = ev.getX();
                // 初始化标记
                letChildDealTouchEvent = false;
                break;
            case MotionEvent.ACTION_MOVE:
                // 如果子view正在拖拽中,那么不拦截它的事件,直接return false;
                if (letChildDealTouchEvent) {
                    return false;
                }

                // 获取当前手指位置
                float endY = ev.getY();
                float endX = ev.getX();
                float distanceX = Math.abs(endX - startX);
                float distanceY = Math.abs(endY - startY);
                // 如果X轴位移大于Y轴位移,那么将事件交给子View处理
                if (distanceX > mTouchSlop && distanceX > distanceY) {
                    letChildDealTouchEvent = true;
                    return false;
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                // 初始化标记
                letChildDealTouchEvent = false;
                break;
            default:
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

总结:SwipeRefreshLayout在子view滑动到顶部的时候,会对垂直方向的滑动时间做判断,当垂直方向向下的滑动距离大于系统最小滑动距离的时候,会拦截子View的Touch事件,开始做下拉刷新处理。重写SwipeRefreshLayout的onInterceptTouchEvent事件,对水平滑动做相应处理,可以避免该问题产生。


你可能感兴趣的:(tips)