• 修复垂直滑动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/

  • 相关阅读:
    相机中白平衡的算法模拟实现
    双边滤波算法的简易实现bilateralFilter
    图像处理卷积算法实现
    最快的3x3中值模糊
    黑米手机抢购软件通杀破解补丁
    分享一款12306购票软件
    快速堆栈模糊算法
    大数据(1)---大数据及HDFS简述
    springboot自动装配(3)---条件注解@Conditional
    随机瓜分百万红包
  • 原文地址:https://www.cnblogs.com/ldq2016/p/5952726.html
Copyright © 2020-2023  润新知