美文网首页精选案例小技巧
修复垂直滑动RecyclerView嵌套水平滑动Recycler

修复垂直滑动RecyclerView嵌套水平滑动Recycler

作者: 289346a467da | 来源:发表于2017-02-21 15:20 被阅读506次

    在 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/

    相关文章

      网友评论

      本文标题:修复垂直滑动RecyclerView嵌套水平滑动Recycler

      本文链接:https://www.haomeiwen.com/subject/dusmsttx.html