了解ViewPager源码从内部拦截横竖滑动冲突

从事android开发的时候,经常会自定义控件,这时候会碰到一个问题,就是横向和竖向的滑动冲突。试想如果你在横向滑动一个View,然后不小心突然竖向的控件滑动了一下,这时候横向滑动的控件就无法接收到滑动事件了,造成了特别不好的体验。比如这样:
这里写图片描述

其实解决方法已经在源码中有提现,ViewPager作为一个横向的ViewGroup就已经解决了冲突,给我们写自定义控件时提供解决思路。在看源码之前需要先了解下事件分发的顺序。

事件分发简单流程

事件分发要将清楚需要重新开博客来写才行,不是一两句话就可以说明白。这里就简单的来理一下大概顺序。触摸事件首先会进入ViewGroup的dispatchTouchEvent(),我们来简单看一下。

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
            //...
            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                //这里通过判断disallowIntercept来决定是否调用onInterceptTouchEvent()
                //默认是false,这个变量是内部拦截的重点
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }
            //...
    if (!canceled && !intercepted) {
        //...这里做事件分发到子控件
    }
}

注意看我中文注释的地方,这里我们知道会先调用onInterceptTouchEvent(),通过返回值来判断是否拦截事件或者分发下去。如果分发事件就会到了View的dispatchTouchEvent(),然后会判断调用OnTouchEvent(),中间还有很多判断,但我们暂定这样的流程,因为不是该博文的重点。
所以大致流程这样:
ViewGroup —>dispatchTouchEvent —->onInterceptTouchEvent—->
View —->dispatchTouchEvent —–>onTouchEvent

ViewPager源码分析

我们把情况设想的简单一些,便于分析。假设一个ScrollView中间套了一个ViewPager。那么按照简单的事件分发逻辑,事件经过ScrollView的onInterceptTouchEvent然后再到ViewPager的dispatchTouchEvent再到onInterceptTouchEvent。手指横向滑动ScrollView是不会拦截事件的,这里就不解释了,有兴趣的可以仔细研究研究源码。我们直接看ViewPager的onInterceptTouchEvent做了什么吧。

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
      //...

        // Nothing more to do here if we have decided whether or not we
        // are dragging.
        if (action != MotionEvent.ACTION_DOWN) {
            if (mIsBeingDragged) {
                if (DEBUG) Log.v(TAG, "Intercept returning true!");
                return true;
            }
            if (mIsUnableToDrag) {
                if (DEBUG) Log.v(TAG, "Intercept returning false!");
                return false;
            }
        }

        switch (action) {
            case MotionEvent.ACTION_MOVE: {
                /*
                 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
                 * whether the user has moved far enough from his original down touch.
                 */

                /*
                * Locally do absolute value. mLastMotionY is set to the y value
                * of the down event.
                */
                final int activePointerId = mActivePointerId;
                if (activePointerId == INVALID_POINTER) {
                    // If we don't have a valid id, the touch down wasn't on content.
                    break;
                }

                final int pointerIndex = ev.findPointerIndex(activePointerId);
                final float x = ev.getX(pointerIndex);
                final float dx = x - mLastMotionX;
                final float xDiff = Math.abs(dx);
                final float y = ev.getY(pointerIndex);
                final float yDiff = Math.abs(y - mInitialMotionY);
                if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);

                if (dx != 0 && !isGutterDrag(mLastMotionX, dx)
                        && canScroll(this, false, (int) dx, (int) x, (int) y)) {
                    // Nested view has scrollable area under this point. Let it be handled there.
                    mLastMotionX = x;
                    mLastMotionY = y;
                    mIsUnableToDrag = true;
                    return false;
                }
                if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) {
                    if (DEBUG) Log.v(TAG, "Starting drag!");
                    mIsBeingDragged = true;
                    requestParentDisallowInterceptTouchEvent(true);
                    setScrollState(SCROLL_STATE_DRAGGING);
                    mLastMotionX = dx > 0
                            ? mInitialMotionX + mTouchSlop : mInitialMotionX - mTouchSlop;
                    mLastMotionY = y;
                    setScrollingCacheEnabled(true);
                } else if (yDiff > mTouchSlop) {
                    // The finger has moved enough in the vertical
                    // direction to be counted as a drag...  abort
                    // any attempt to drag horizontally, to work correctly
                    // with children that have scrolling containers.
                    if (DEBUG) Log.v(TAG, "Starting unable to drag!");
                    mIsUnableToDrag = true;
                }
                if (mIsBeingDragged) {
                    // Scroll to follow the motion event
                    if (performDrag(x)) {
                        ViewCompat.postInvalidateOnAnimation(this);
                    }
                }
                break;
            }

            //...
        }

        //...
        return mIsBeingDragged;
    }

代码太长,我们还是只关注重点,在ACTION_MOVE的时候获取这次滑动的xDiff和yDiff,也就是横向和竖向滑动的距离,通过 判断 (xDiff > mTouchSlop && xDiff * 0.5f > yDiff),以及mTouchSlop 来置位mIsBeingDragged =true,从这个变量命名我们就可以知道,用来置为Viewpager已经处于滑动状态了。这个mTouchSlop 可以理解为最小滑动距离,获取方式也贴一下

final ViewConfiguration configuration = ViewConfiguration.get(context);
final float density = context.getResources().getDisplayMetrics().density;
mTouchSlop = configuration.getScaledPagingTouchSlop();

接下来 requestParentDisallowInterceptTouchEvent(true)。


    private void requestParentDisallowInterceptTouchEvent(boolean disallowIntercept) {
        final ViewParent parent = getParent();
        if (parent != null) {
            parent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }

    @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            // We're already in this state, assume our ancestors are too
            return;
        }

        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // Pass it up to our parent
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }

我就可以认为Viewpager的所有上级都已经disallowIntercept 为true了,也就是不会拦截事件了。总结一下流程,onInterceptTouchEvent通过判断滑动距离,x方向的0.5倍大于y方向的偏移距离,以及x方法的距离大于mTouchSlop 我们就可以认定Viewpager处于滑动状态。mIsBeingDragged=true,调用requestParentDisallowInterceptTouchEvent使父控件不拦截事件,并且触摸事件将不会分发到子控件。那么接下来就到了Viewpager的OnTouchEvent,我们接着看一下

 case MotionEvent.ACTION_MOVE:
                if (!mIsBeingDragged) {
                    final int pointerIndex = ev.findPointerIndex(mActivePointerId);
                    if (pointerIndex == -1) {
                        // A child has consumed some touch events and put us into an inconsistent
                        // state.
                        needsInvalidate = resetTouch();
                        break;
                    }
                    final float x = ev.getX(pointerIndex);
                    final float xDiff = Math.abs(x - mLastMotionX);
                    final float y = ev.getY(pointerIndex);
                    final float yDiff = Math.abs(y - mLastMotionY);
                    if (DEBUG) {
                        Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
                    }
                    if (xDiff > mTouchSlop && xDiff > yDiff) {
                        if (DEBUG) Log.v(TAG, "Starting drag!");
                        mIsBeingDragged = true;
                        requestParentDisallowInterceptTouchEvent(true);
                        mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop :
                                mInitialMotionX - mTouchSlop;
                        mLastMotionY = y;
                        setScrollState(SCROLL_STATE_DRAGGING);
                        setScrollingCacheEnabled(true);

                        // Disallow Parent Intercept, just in case
                        ViewParent parent = getParent();
                        if (parent != null) {
                            parent.requestDisallowInterceptTouchEvent(true);
                        }
                    }
                }
                // Not else! Note that mIsBeingDragged can be set above.
                if (mIsBeingDragged) {
                    // Scroll to follow the motion event
                    final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
                    final float x = ev.getX(activePointerIndex);
                    needsInvalidate |= performDrag(x);
                }
                break;

我们也只需要关注MOVE事件就好了,其实如果触摸事件走到这里有两种情况,一种是onInterceptTouchEvent拦截了,还有一种就是没有子控件接收事件,无论那种情况,如果mIsBeingDragged=false,这里会再次进行判断,将xDiff 的范围缩小了,也就是更容易使Viewpager处于滑动状态。也就是说,在Viewpager没有子控件接受事件的情况下,滑动状态的变更更加容易,有子控件的接受事件的情况,判定将范围将扩大,也就是xDiff需要比yDiff大两倍才会判定触发滑动。注意如果已经判定为滑动状态,后面将不会再判定,直接将处理后续的所有事件。

总结

看到这里其实思路已经比较明确了,内部拦截法就是通过横向和竖向滑动的距离的差值来判断是否拦截,然后调用 parent.requestDisallowInterceptTouchEvent(true)来通知父控件不拦截事件,这个会将所有的上层控件一一通知。Viewgroup可以参考Viewpager的写法,如果只是单个View就直接参考OnTouchEvent就可以了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值