Android开发View滑动冲突处理

最近在重构一个老项目,遇到ScrollView嵌套WebView的场景,因为WebView加载的网页并不是自适应,所以导致在滑动网页的时候异常卡顿,很明显是滑动冲突了,解决方式也很常规,针对滑动冲突这里顺便做笔记吧。

冲突场景

滑动冲突场景可简单分为两种:

  • 外部和内部的滑动方向不一致
  • 外部和内部的滑动方向一致

示图如下:
滑动冲突场景图示
其他情况的滑动冲突都是在这两种冲突的基础上延伸出来的,或者以上两种场景的嵌套,面对这种我们处理方式就是剥离成以上基础的冲突场景,逐个处理。

处理规则

面对场景1,它的处理规则是:当用户左右滑动时(父),需要让外部的View拦截点击事件;当用户上下滑动时(子),需要让内部View拦截点击事件。其实说白了,我们的主要目标是确定目标view的滑动方向。

确定滑动方向,列举以下三种方式参考:

  1. 根据垂直滑动和水平滑动的距离对比判断,哪个大就是哪个方向滑动。
  2. 根据滑动路径与水平方向的夹角,大于45度认为是垂直滑动,否则是水平滑动。
  3. 根据垂直滑动和水平滑动的速度对比判断,哪个大就是哪个方向滑动。

面对场景2,因为滑动方向是一致的,所以我们只有根据业务来区分到底应该滑动哪一个view。核心点就是准确判断滑动区域的位置。

解决方式

从上边可以知道冲突的产生主要是不确定具体应该在哪一层滑动(内层、外层),上边也说了相关的处理规则,接下就是我们按照规则主动把事件分发给相应的view。之前我写过一篇《Android开发之onTouch事件的分发拦截消费机制探究学习》,这里用到的知识点就是事件的拦截机制。

针对滑动冲突,这里给出两种解决滑动冲突的方式:外部拦截法和内部拦截法。

外部拦截法

外部拦截法,就是处理父View的滑动事件时,父View主动把事件拦截下来自行消化,其他情况继续不拦截由子View消化处理。
外部拦截法

这里需要注意只在父View的ACTION_MOVE事件中判断是否进行拦截,其他事件不需要,因为一旦拦截事件就无法传递到子View了。

内部拦截法

内部拦截法,默认子View处理滑动事件,当判断某个位置或者某种情况下应该父View滑动时,主动告知父View开启事件拦截,由父View消化处理。这里用到一个parent.requestDisallowInterceptTouchEvent()方法。
内部拦截法

ViewPager处理滑动冲突分析

有同学可能会问,既然内外层都可以滑动,这样很容易出现冲突,但是为什么Viewpager中不存在滑动冲突呢。其实官方已经为我们处理了。

ViewPager也是一个ViewGroup,在ViewPager的initViewPager方法中生成Scroller对象,Scroller是Android内置的专门用于渐进式滑动的类,配合插值器可以产生立体的滑动感,既然ViewPager是一个容器并且可以滑动,那么也就避免不了内嵌view滑动冲突这一遭。

ViewPager只关注水平方向的手指滑动,根据水平方向的手指滑动来切换页面。在垂直方向上,ViewPager并不关心,因此,ViewPager很有必要解决一下滑动冲突,把竖直方向的滑动传递给子View来处理。

我们知道,ViewGroup是在onInterceptTouchEvent函数中决定是否拦截触摸事件, 所以我们直接去查看ViewPager的onInterceptTouchEvent事件拦截,来分析ViewPager的滑动冲突处理方式:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {

    //1. 触摸动作
    final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;

    //2. 时刻要注意触摸是否已经结束
    if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
        //3. Release the drag.
        if (DEBUG) Log.v(TAG, "Intercept done!");
        //4. 重置一些跟判断是否拦截触摸相关变量
        resetTouch();
        //5. 触摸结束,无需拦截
        return false;
    }

    //6. 如果当前不是按下事件,我们就判断一下,是否是在拖拽切换页面
    if (action != MotionEvent.ACTION_DOWN) {
        //7. 如果当前是正在拽切换页面,直接拦截掉事件,后面无需再做拦截判断
        if (mIsBeingDragged) {
            if (DEBUG) Log.v(TAG, "Intercept returning true!");
            return true;
        }
        //8. 如果标记为不允许拖拽切换页面,我们就"放过"一切触摸事件
        if (mIsUnableToDrag) {
            if (DEBUG) Log.v(TAG, "Intercept returning false!");
            return false;
        }
    }
    //9. 根据不同的动作进行处理
    switch (action) {
        //10. 如果是手指移动操作
        case MotionEvent.ACTION_MOVE: {

            //11. 代码能执行到这里,就说明mIsBeingDragged==false,否则的话,在第7个注释处就已经执行结束了

            //12.使用触摸点Id,主要是为了处理多点触摸
            final int activePointerId = mActivePointerId;
            if (activePointerId == INVALID_POINTER) {
                //13.如果当前的触摸点id不是一个有效的Id,无需再做处理
                break;
            }
            //14.根据触摸点的id来区分不同的手指,我们只需关注一个手指就好
            final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
            //15.根据这个手指的序号,来获取这个手指对应的x坐标
            final float x = MotionEventCompat.getX(ev, pointerIndex);
            //16.在x轴方向上移动的距离
            final float dx = x - mLastMotionX;
            //17.x轴方向的移动距离绝对值
            final float xDiff = Math.abs(dx);
            //18.同理,参照16、17条注释
            final float y = MotionEventCompat.getY(ev, pointerIndex);
            final float yDiff = Math.abs(y - mInitialMotionY);
            if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);

            //19.判断当前显示的页面是否可以滑动,如果可以滑动,则将该事件丢给当前显示的页面处理
            //isGutterDrag是判断是否在两个页面之间的缝隙内移动
            //canScroll是判断页面是否可以滑动
            if (dx != 0 && !isGutterDrag(mLastMotionX, dx) &&
                    canScroll(this, false, (int) dx, (int) x, (int) y)) {
                mLastMotionX = x;
                mLastMotionY = y;
                //20.标记ViewPager不去拦截事件
                mIsUnableToDrag = true;
                return false;
            }
            //21.如果x移动距离大于最小距离,并且斜率小于0.5,表示在水平方向上的拖动
            if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) {
                if (DEBUG) Log.v(TAG, "Starting drag!");
                //22.水平方向的移动,需要ViewPager去拦截
                mIsBeingDragged = true;
                //23.如果ViewPager还有父View,则还要向父View申请将触摸事件传递给ViewPager
                requestParentDisallowInterceptTouchEvent(true);
                //24.设置滚动状态
                setScrollState(SCROLL_STATE_DRAGGING);
                //25.保存当前位置
                mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop :
                        mInitialMotionX - mTouchSlop;
                mLastMotionY = y;
                //26.启用缓存
                setScrollingCacheEnabled(true);
            } else if (yDiff > mTouchSlop) {//27.否则的话,表示是竖直方向上的移动
                if (DEBUG) Log.v(TAG, "Starting unable to drag!");
                //28.竖直方向上的移动则不去拦截触摸事件
                mIsUnableToDrag = true;
            }
            if (mIsBeingDragged) {
                // 29.跟随手指一起滑动
                if (performDrag(x)) {
                    ViewCompat.postInvalidateOnAnimation(this);
                }
            }
            break;
        }
        //30.如果手指是按下操作
        case MotionEvent.ACTION_DOWN: {

            //31.记录按下的点位置
            mLastMotionX = mInitialMotionX = ev.getX();
            mLastMotionY = mInitialMotionY = ev.getY();
            //32.第一个ACTION_DOWN事件对应的手指序号为0
            mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
            //33.重置允许拖拽切换页面
            mIsUnableToDrag = false;
            //34.标记开始滚动
            mIsScrollStarted = true;
            //35.手动调用计算滑动的偏移量
            mScroller.computeScrollOffset();
            //36.如果当前滚动状态为正在将页面放置到最终位置,
            //且当前位置距离最终位置足够远
            if (mScrollState == SCROLL_STATE_SETTLING &&
                    Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) {
                //37. 如果此时用户手指按下,则立马暂停滑动
                mScroller.abortAnimation();
                mPopulatePending = false;
                populate();
                mIsBeingDragged = true;
                //38.如果ViewPager还有父View,则还要向父View申请将触摸事件传递给ViewPager
                requestParentDisallowInterceptTouchEvent(true);
                //39.设置当前状态为正在拖拽
                setScrollState(SCROLL_STATE_DRAGGING);
            } else {
                //40.结束滚动
                completeScroll(false);
                mIsBeingDragged = false;
            }

            if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY
                    + " mIsBeingDragged=" + mIsBeingDragged
                    + "mIsUnableToDrag=" + mIsUnableToDrag);
            break;
        }

        case MotionEventCompat.ACTION_POINTER_UP:
            onSecondaryPointerUp(ev);
            break;
    }

    //41.添加速度追踪
    if (mVelocityTracker == null) {
        mVelocityTracker = VelocityTracker.obtain();
    }
    mVelocityTracker.addMovement(ev);


    //42.只有在当前是拖拽切换页面时我们才会去拦截事件
    return mIsBeingDragged;
}

从源码上面看出,斜率小于0.5时,则要拦截,否则不拦截。越靠近y轴的直线,斜率越大,越靠近x轴直线斜率越小。因此,当手指滑动的倾斜度比0.5小时就去拦截事件,由ViewPager来响应切换页面。
斜率图示

参考

  • 《Android开发艺术探索》,微信读书可以免费阅读。
  • https://blog.csdn.net/huachao1001/article/details/51654692
发布了47 篇原创文章 · 获赞 38 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/li0978/article/details/105445096
今日推荐