Android 滑动冲突的解决方法

一、常见的滑动冲突场景

场景1——外部滑动方向和内部滑动方向不一致,如:ViewPager中有多个fragment,而fragment中有ListView,这时ViewPager可以左右滑动,而ListView可以上下滑动,这就造成了滑动冲突。注意:这只是举个例子说明一下场景1,事实上ViewPager内部已经处理了这种滑动冲突,在采用ViewPager时,我们无需关注这个问题。

场景2——外部滑动方向和内部滑动方向一致。

场景3——上述两种场景的嵌套,即共有三层,外层与中层的滑动方向一致,而中层与内层的滑动方向不一致。

二、滑动冲突的处理规则

场景1的处理规则:
1、当用户左右滑动时,让外部的View拦截点击事件,当用户上下滑动时,让内部的View拦截点击事件;
2、判断用户的滑动方向(左右、上下):如果用户手指滑动的水平距离大于垂直距离,则左右滑动,反之,上下滑动;还可以根据角度、速度差来做判断;

场景2的处理规则:
无法根据滑动的角度、距离差、速度差来判断,因为场景2内部、外部的滑动方向一致;这时候一般都能在业务上找到突破点,如业务上规定:当处于某种状态需要外部View响应用户的滑动,而处于另一种状态时则需要内部View响应用户的滑动,所以我们可以根据业务的需求得出相应的处理规则。

场景3的处理规则:场景1的处理规则和场景2的处理规则一起用。

三、滑动冲突的解决方法

以下两种解决方法都是通用的方法:
1、外部拦截法
我们都知道点击事件是先经过父容器的拦截处理的,所以我们就可以在父容器的拦截方法中写下自己的逻辑代码即可,相当于我们要重写一个布局Layout,使其继承ViewGroup或其它的布局(LinearLayout等),然后重写其onInterceptTouchEvent方法。外部拦截法的伪代码如下:

 @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = false;
        int x = (int)ev.getX();
        int y = (int)ev.getY();

        switch (ev.getAction()){

            //父容器不需要拦截
            case MotionEvent.ACTION_DOWN:
                intercepted = false;
                break;

            //在此判断父容器是否需要拦截
            case MotionEvent.ACTION_MOVE:
                if (父容器需要当前点击事件){
                    intercepted = true;
                }
                //父容器不需要当前的点击事件
                else {
                    intercepted = false;
                }
                break;

            //父容器不需要拦截
            case MotionEvent.ACTION_UP:
                intercepted = false;
                break;
            default:
                break;
        }

        //重置手指的起始位置
        mLastXIntercept = x;
        mLastYIntercept = y;

        return intercepted;
    }
  •  

解释上述代码:
1、父容器不拦截ACTION_DOWN事件:那是因为一旦父容器拦截了ACTION_DOWN事件,后续的ACTION_MOVE、ACTION_UP事件都会直接交给父容器处理,这个时候事件无法传递给子元素了。

2、父容器不拦截ACTION_UP事件:首先我们要知道onClick 事件是在ACTION_UP事件之后执行的,那当子元素有一个onClick事件,而这时候父容器拦截了ACTION_UP事件,那子元素的onClick事件就无法执行了。

3、ACTION_MOVE事件:在这里可以根据我们的需求,来判断父容器是否需要拦截事件,需要则返回true,否则返回false。

总结,外部拦截法比较简单,实现起来较容易。

2、内部拦截法
首先我们了解下ViewGroup.requestDisallowInterceptTouchEvent(boolean)方法,此方法就是在子View中通知父容器拦截或不拦截点击事件,false ——拦截,true ——不拦截;

内部拦截法的思想是父容器先不拦截任何事件,即所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交由父容器进行处理。这时我们就用到requestDisallowInterceptTouchEvent 方法了。

具体做法:我们重写子View的dispatchTouchEvent方法,即我们新建一个类继承子View,然后重写它的dispatchTouchEvent方法,伪代码如下:

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        //获得当前的位置坐标
        int x = (int) ev.getX();
        int y = (int) ev.getY();

        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:

                //通知父容器不要拦截事件
                horizontalScrollLayout.requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:

                if (父容器需要此事件){
                    //通知父容器拦截此事件
                    horizontalScrollLayout.requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
            default:
                break;
        }

        //重置手指的初始位置
        mLastY = y;
        mLastX = x;

        return super.dispatchTouchEvent(ev);
    }
  • 代码:
    1、horizontalScrollLayout是此子View的父容器的对象,我们可以在子 View中定义一个方法:
public void setHorizontalScrollLayout(HorizontalScrollLayout horizontalScrollLayout) {
        this.horizontalScrollLayout = horizontalScrollLayout;
    }
  •  

此时,父容器拦截了除ACTION_DOWN以外的其它事件,那为什么这样做?
原因:只有这样,才能使的当子元素调用了requestDisallowInterceptTouchEvent(false)方法后,父容器才能继续拦截所需的事件。

3、那父容器为什么不能拦截ACTION_DOWN事件呢?
原因:因为ACTION_DOWN事件不受FLAG_DISALLOW_INTERCEPT这个标记位的控制,所以一旦父容器拦截了ACTION_DOWN事件,那么所有的事件都无法传递给子元素了,这样内部拦截就起不到作用了。

总结,内部拦截法比较复杂,在实际应用中,建议还是用外部拦截法。

四、举例说明

在举例的时候,我自定义了几个view,大家主要看其中的dispatchTouchEvent方法和onInterceptTouchEvent方法,至于自定View的方法会在之后的博客中详细描述。

场景1的解决方案:外部拦截和内部拦截的方法都有 。代码下载请点我

实例代码介绍:

 我们的场景是一个水平滑动的HorizontalScrollLayout里面包含一个垂直滑动的listview,有两种做法:一种是父控件拦截,一种是子控件拦截。

1.父控件拦截:

package com.example.baiyuanwei.slideclashtest;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Scroller;

/**
 * Created by baiyuanwei on 16/3/2.
 * 外部拦截法
 */
public class HorizontalScrollLayout extends ViewGroup {


    private static final String TAG = "HorizontalScrollLayout";

    private int mChildrenSize;
    private int mChildWidth;
    private int mChildIndex;

    // 分别记录上次滑动的坐标
    private int mLastX = 0;
    private int mLastY = 0;
    // 分别记录上次滑动的坐标(onInterceptTouchEvent)
    private int mLastXIntercept = 0;
    private int mLastYIntercept = 0;

    private Scroller mScroller;
    private VelocityTracker mVelocityTracker;

    public HorizontalScrollLayout(Context context) {
        super(context);
        init();
    }

    public HorizontalScrollLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public HorizontalScrollLayout(Context context, AttributeSet attrs,
                                  int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init() {
        mScroller = new Scroller(getContext());
        mVelocityTracker = VelocityTracker.obtain();
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean intercepted = false;
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                intercepted = false;
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                    intercepted = true;
                }
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                int deltaX = x - mLastXIntercept;
                int deltaY = y - mLastYIntercept;
                if (Math.abs(deltaX) > Math.abs(deltaY)) {
                    intercepted = true;
                } else {
                    intercepted = false;
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                intercepted = false;
                break;
            }
            default:
                break;
        }

        Log.d(TAG, "intercepted=" + intercepted);
        mLastX = x;
        mLastY = y;
        mLastXIntercept = x;
        mLastYIntercept = y;

        return intercepted;
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mVelocityTracker.addMovement(event);
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                scrollBy(-deltaX, 0);
                break;
            }
            case MotionEvent.ACTION_UP: {
                int scrollX = getScrollX();
                int scrollToChildIndex = scrollX / mChildWidth;
                mVelocityTracker.computeCurrentVelocity(1000);
                float xVelocity = mVelocityTracker.getXVelocity();
                if (Math.abs(xVelocity) >= 50) {
                    mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
                } else {
                    mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
                }
                mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));
                int dx = mChildIndex * mChildWidth - scrollX;
                smoothScrollBy(dx, 0);
                mVelocityTracker.clear();
                break;
            }
            default:
                break;
        }

        mLastX = x;
        mLastY = y;
        return true;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int measuredWidth = 0;
        int measuredHeight = 0;
        final int childCount = getChildCount();
        measureChildren(widthMeasureSpec, heightMeasureSpec);

        int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        if (childCount == 0) {
            setMeasuredDimension(0, 0);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            final View childView = getChildAt(0);
            measuredHeight = childView.getMeasuredHeight();
            setMeasuredDimension(widthSpaceSize, childView.getMeasuredHeight());
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            final View childView = getChildAt(0);
            measuredWidth = childView.getMeasuredWidth() * childCount;
            setMeasuredDimension(measuredWidth, heightSpaceSize);
        } else {
            final View childView = getChildAt(0);
            measuredWidth = childView.getMeasuredWidth() * childCount;
            measuredHeight = childView.getMeasuredHeight();
            setMeasuredDimension(measuredWidth, measuredHeight);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childLeft = 0;
        final int childCount = getChildCount();
        mChildrenSize = childCount;

        for (int i = 0; i < childCount; i++) {
            final View childView = getChildAt(i);
            if (childView.getVisibility() != View.GONE) {
                final int childWidth = childView.getMeasuredWidth();
                mChildWidth = childWidth;
                childView.layout(childLeft, 0, childLeft + childWidth,
                        childView.getMeasuredHeight());
                childLeft += childWidth;
            }
        }
    }

    private void smoothScrollBy(int dx, int dy) {
        mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
        invalidate();
    }

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

    @Override
    protected void onDetachedFromWindow() {
        mVelocityTracker.recycle();
        super.onDetachedFromWindow();
    }
}

这种方式,子控件不用做什么特殊处理。

2.字控件拦截

package com.example.baiyuanwei.slideclashtest;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.ListView;

/**
 * Created by baiyuanwei on 16/3/3.
 * 采用内部拦截法解决滑动冲突
 */
public class MyListView extends ListView {

    private final static String TAG = "MyListView";

    //listView的容器
    private HorizontalScrollLayout2 horizontalScrollLayout2;

    //记录上次滑动的位置
    private int mLastX = 0;
    private int mLastY = 0;

    public MyListView(Context context) {
        super(context);
    }

    public MyListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyListView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public void setHorizontalScrollLayout2(HorizontalScrollLayout2 horizontalScrollLayout2) {
        this.horizontalScrollLayout2 = horizontalScrollLayout2;
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        //获得当前的位置坐标
        int x = (int) ev.getX();
        int y = (int) ev.getY();

        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:

                //通知父容器不要拦截事件
                horizontalScrollLayout2.requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                int delatX = x-mLastX;
                int delatY = y - mLastY;


                if (Math.abs(delatX)>Math.abs(delatY)){
                    //通知父容器拦截此事件
                    horizontalScrollLayout2.requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
            default:
                break;
        }

        //重置手指的初始位置
        mLastY = y;
        mLastX = x;

        return super.dispatchTouchEvent(ev);
    }
}
package com.example.baiyuanwei.slideclashtest;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Scroller;

/**
 * 内部拦截法
 */
public class HorizontalScrollLayout2 extends ViewGroup {
    private static final String TAG = "HorizontalScrollLayout2";

    private int mChildrenSize;
    private int mChildWidth;
    private int mChildIndex;
    // 分别记录上次滑动的坐标
    private int mLastX = 0;
    private int mLastY = 0;

    // 分别记录上次滑动的坐标(onInterceptTouchEvent)
    private int mLastXIntercept = 0;
    private int mLastYIntercept = 0;

    private Scroller mScroller;
    private VelocityTracker mVelocityTracker;

    public HorizontalScrollLayout2(Context context) {
        super(context);
        init();
    }

    public HorizontalScrollLayout2(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public HorizontalScrollLayout2(Context context, AttributeSet attrs,
                                   int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init() {
        mScroller = new Scroller(getContext());
        mVelocityTracker = VelocityTracker.obtain();
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        int action = event.getAction();


        if (action == MotionEvent.ACTION_DOWN) {
            mLastX = x;
            mLastY = y;
            if (!mScroller.isFinished()) {
                mScroller.abortAnimation();
                return true;
            }
            return false;
        } else {
            return true;
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d(TAG, "onTouchEvent action:" + event.getAction());
        mVelocityTracker.addMovement(event);
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
            if (!mScroller.isFinished()) {
                mScroller.abortAnimation();
            }
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            int deltaX = x - mLastX;
            int deltaY = y - mLastY;
            Log.d(TAG, "move, deltaX:" + deltaX + " deltaY:" + deltaY);
            scrollBy(-deltaX, 0);
            break;
        }
        case MotionEvent.ACTION_UP: {
            int scrollX = getScrollX();
            int scrollToChildIndex = scrollX / mChildWidth;
            Log.d(TAG, "current index:" + scrollToChildIndex);
            mVelocityTracker.computeCurrentVelocity(1000);
            float xVelocity = mVelocityTracker.getXVelocity();
            if (Math.abs(xVelocity) >= 50) {
                mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
            } else {
                mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
            }
            mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));
            int dx = mChildIndex * mChildWidth - scrollX;
            smoothScrollBy(dx, 0);
            mVelocityTracker.clear();
            Log.d(TAG, "index:" + scrollToChildIndex + " dx:" + dx);
            break;
        }
        default:
            break;
        }

        mLastX = x;
        mLastY = y;
        return true;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int measuredWidth = 0;
        int measuredHeight = 0;
        final int childCount = getChildCount();
        measureChildren(widthMeasureSpec, heightMeasureSpec);

        int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        if (childCount == 0) {
            setMeasuredDimension(0, 0);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            final View childView = getChildAt(0);
            measuredHeight = childView.getMeasuredHeight();
            setMeasuredDimension(widthSpaceSize, childView.getMeasuredHeight());
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            final View childView = getChildAt(0);
            measuredWidth = childView.getMeasuredWidth() * childCount;
            setMeasuredDimension(measuredWidth, heightSpaceSize);
        } else {
            final View childView = getChildAt(0);
            measuredWidth = childView.getMeasuredWidth() * childCount;
            measuredHeight = childView.getMeasuredHeight();
            setMeasuredDimension(measuredWidth, measuredHeight);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        Log.d(TAG, "width:" + getWidth());
        int childLeft = 0;
        final int childCount = getChildCount();
        mChildrenSize = childCount;

        for (int i = 0; i < childCount; i++) {
            final View childView = getChildAt(i);
            if (childView.getVisibility() != View.GONE) {
                final int childWidth = childView.getMeasuredWidth();
                mChildWidth = childWidth;
                childView.layout(childLeft, 0, childLeft + childWidth,
                        childView.getMeasuredHeight());
                childLeft += childWidth;
            }
        }
    }

    private void smoothScrollBy(int dx, int dy) {
        mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
        invalidate();
    }

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

    @Override
    protected void onDetachedFromWindow() {
        mVelocityTracker.recycle();
        super.onDetachedFromWindow();
    }
}

--------------------- 作者:struggle323 来源:CSDN 原文:https://blog.csdn.net/struggle323/article/details/50788136 

==============================================

拓展内容:

修复垂直滑动RecyclerView嵌套水平滑动RecyclerView水平滑动不灵敏问题

在 Android 应用中,大部分情况下都会使用一个垂直滚动的 View 来显示内容(比如 ListView、RecyclerView 等)。但是有时候你还希望垂直滚动的View 里面的内容可以水平滚动。如果直接在垂直滚动的 View 里面使用水平滚动的 View,则滚动操作并不是很流畅。

比如下图中的示例:

为什么会出现这个问题呢?

上图中的布局为一个 RecyclerView 使用的是垂直滚动的 LinearLayoutManager 布局管理器,而里面每个 Item 为另外一个 RecyclerView 使用的是水平滚动的 LinearLayoutManager。而在 Android系统的事件分发 中,即使最上层的 View 只能垂直滚动,当用户水平拖动的时候,最上层的 View 依然会拦截点击事件。下面是 RecyclerView.java 中 onInterceptTouchEvent 的相关代码:

@Override
public boolean onInterceptTouchEvent(MotionEvent e) {  
  ...
 
  switch (action) {
    case MotionEvent.ACTION_DOWN:
        ...
 
    case MotionEvent.ACTION_MOVE: {
        ...
 
        if (mScrollState != SCROLL_STATE_DRAGGING) {
          boolean startScroll = false;
          if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
            ...
            startScroll = true;
          }
          if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
            ...
            startScroll = true;
          }
          if (startScroll) {
            setScrollState(SCROLL_STATE_DRAGGING);
          }
      }
    } break;
      ...
 
  }
  return mScrollState == SCROLL_STATE_DRAGGING;
}
 

注意上面的 if 判断:

if(canScrollVertically && Math.abs(dy) > mTouchSlop) {...}  
 

RecyclerView 并没有判断用户拖动的角度, 只是用来判断拖动的距离是否大于滚动的最小尺寸。 如果是一个只能垂直滚动的 View,这样实现是没有问题的。如果我们在里面再放一个 水平滚动的 RecyclerView ,则就出现问题了。

可以通过如下的方式来修复该问题:

if(canScrollVertically && Math.abs(dy) > mTouchSlop && (canScrollHorizontally || Math.abs(dy) > Math.abs(dx))) {...}  
 

下面是一个完整的实现 BetterRecyclerView.java :

public class BetterRecyclerView extends RecyclerView{
  private static final int INVALID_POINTER = -1;
  private int mScrollPointerId = INVALID_POINTER;
  private int mInitialTouchX, mInitialTouchY;
  private int mTouchSlop;
  public BetterRecyclerView(Contextcontext) {
    this(context, null);
  }
 
  public BetterRecyclerView(Contextcontext, @Nullable AttributeSetattrs) {
    this(context, attrs, 0);
  }
 
  public BetterRecyclerView(Contextcontext, @Nullable AttributeSetattrs, int defStyle) {
    super(context, attrs, defStyle);
    final ViewConfigurationvc = ViewConfiguration.get(getContext());
    mTouchSlop = vc.getScaledTouchSlop();
  }
 
  @Override
  public void setScrollingTouchSlop(int slopConstant) {
    super.setScrollingTouchSlop(slopConstant);
    final ViewConfigurationvc = ViewConfiguration.get(getContext());
    switch (slopConstant) {
      case TOUCH_SLOP_DEFAULT:
        mTouchSlop = vc.getScaledTouchSlop();
        break;
      case TOUCH_SLOP_PAGING:
        mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(vc);
        break;
      default:
        break;
    }
  }
 
  @Override
  public boolean onInterceptTouchEvent(MotionEvent e) {
    final int action = MotionEventCompat.getActionMasked(e);
    final int actionIndex = MotionEventCompat.getActionIndex(e);
 
    switch (action) {
      case MotionEvent.ACTION_DOWN:
        mScrollPointerId = MotionEventCompat.getPointerId(e, 0);
        mInitialTouchX = (int) (e.getX() + 0.5f);
        mInitialTouchY = (int) (e.getY() + 0.5f);
        return super.onInterceptTouchEvent(e);
 
      case MotionEventCompat.ACTION_POINTER_DOWN:
        mScrollPointerId = MotionEventCompat.getPointerId(e, actionIndex);
        mInitialTouchX = (int) (MotionEventCompat.getX(e, actionIndex) + 0.5f);
        mInitialTouchY = (int) (MotionEventCompat.getY(e, actionIndex) + 0.5f);
        return super.onInterceptTouchEvent(e);
 
      case MotionEvent.ACTION_MOVE: {
        final int index = MotionEventCompat.findPointerIndex(e, mScrollPointerId);
        if (index < 0) {
          return false;
        }
 
        final int x = (int) (MotionEventCompat.getX(e, index) + 0.5f);
        final int y = (int) (MotionEventCompat.getY(e, index) + 0.5f);
        if (getScrollState() != SCROLL_STATE_DRAGGING) {
          final int dx = x - mInitialTouchX;
          final int dy = y - mInitialTouchY;
          final boolean canScrollHorizontally = getLayoutManager().canScrollHorizontally();
          final boolean canScrollVertically = getLayoutManager().canScrollVertically();
          boolean startScroll = false;
          if (canScrollHorizontally && Math.abs(dx) > mTouchSlop && (Math.abs(dx) >= Math.abs(dy) || canScrollVertically)) {
            startScroll = true;
          }
          if (canScrollVertically && Math.abs(dy) > mTouchSlop && (Math.abs(dy) >= Math.abs(dx) || canScrollHorizontally)) {
            startScroll = true;
          }
          return startScroll && super.onInterceptTouchEvent(e);
        }
        return super.onInterceptTouchEvent(e);
      }
 
      default:
        return super.onInterceptTouchEvent(e);
    }
  }
}
 

其他问题

当用户快速滑动(fling)RecyclerView 的时候, RecyclerView 需要一段时间来确定其最终位置。 如果用户在快速滑动一个子的水平 RecyclerView,在子 RecyclerView 还在滑动的过程中,如果用户垂直滑动,则是无法垂直滑动的。原因是子 RecyclerView 依然处理了这个垂直滑动事件。

所以,在快速滑动后的滚动到静止的状态中,子 View 不应该响应滑动事件了,再次看看 RecyclerView 的 onInterceptTouchEvent() 代码:

@Override
public boolean onInterceptTouchEvent(MotionEvent e) {  
    ...
 
    switch (action) {
        case MotionEvent.ACTION_DOWN:
            ...
 
            if (mScrollState == SCROLL_STATE_SETTLING) {
                getParent().requestDisallowInterceptTouchEvent(true);
                setScrollState(SCROLL_STATE_DRAGGING);
            }
 
            ...
    }
    return mScrollState == SCROLL_STATE_DRAGGING;
}
 

可以看到,当 RecyclerView 的状态为 SCROLL_STATE_SETTLING (快速滑动后到滑动静止之间的状态)时, RecyclerView 告诉父控件不要拦截事件。

同样的,如果只有一个方向固定,这样处理是没问题的。

针对我们这个嵌套的情况,父 RecyclerView 应该只拦截垂直滚动事件,所以可以这么修改父 RecyclerView:

public class FeedRootRecyclerView extends BetterRecyclerView{  
  public FeedRootRecyclerView(Contextcontext) {
    this(context, null);
  }
 
  public FeedRootRecyclerView(Contextcontext, @Nullable AttributeSetattrs) {
    this(context, attrs, 0);
  }
 
  public FeedRootRecyclerView(Contextcontext, @Nullable AttributeSetattrs, int defStyle) {
    super(context, attrs, defStyle);
  }
 
  @Override
  public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    /* do nothing */
  }
}
 

下图为最终的结果:

如果感兴趣可以下载 示例项目 ,注意示例项目中使用 kotlin,所以需要配置 kotlin 插件。

原文:http://nerds.headout.com/fix-horizontal-scrolling-in-your-android-app/

猜你喜欢

转载自blog.csdn.net/nnmmbb/article/details/82997461
今日推荐