修复RecyclerView嵌套滚动问题

pcpd8230 8年前
   <p>在 Android 应用中,大部分情况下都会使用一个垂直滚动的 View 来显示内容(比如 ListView、RecyclerView 等)。但是有时候你还希望垂直滚动的View 里面的内容可以水平滚动。如果直接在垂直滚动的 View 里面使用水平滚动的 View,则滚动操作并不是很流畅。</p>    <p>比如下图中的示例:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/cb649f59a1fd6bf1e288431ac0c6f6be.gif"></p>    <h3><strong>为什么会出现这个问题呢?</strong></h3>    <p>上图中的布局为一个 RecyclerView 使用的是垂直滚动的 LinearLayoutManager 布局管理器,而里面每个 Item 为另外一个 RecyclerView 使用的是水平滚动的 LinearLayoutManager。而在 <a href="/misc/goto?guid=4959554884908246509" rel="nofollow,noindex">Android系统的事件分发</a> 中,即使最上层的 View 只能垂直滚动,当用户水平拖动的时候,最上层的 View 依然会拦截点击事件。下面是 RecyclerView.java 中 onInterceptTouchEvent 的相关代码:</p>    <pre>  <code class="language-java">@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;  }     </code></pre>    <p>注意上面的 if 判断:</p>    <pre>  <code class="language-java">if(canScrollVertically && Math.abs(dy) > mTouchSlop) {...}       </code></pre>    <p>RecyclerView 并没有判断用户拖动的角度, 只是用来判断拖动的距离是否大于滚动的最小尺寸。 如果是一个只能垂直滚动的 View,这样实现是没有问题的。如果我们在里面再放一个 水平滚动的 RecyclerView ,则就出现问题了。</p>    <p>可以通过如下的方式来修复该问题:</p>    <pre>  <code class="language-java">if(canScrollVertically && Math.abs(dy) > mTouchSlop && (canScrollHorizontally || Math.abs(dy) > Math.abs(dx))) {...}       </code></pre>    <p>下面是一个完整的实现 <a href="/misc/goto?guid=4959716383262462514" rel="nofollow,noindex">BetterRecyclerView.java</a> :</p>    <pre>  <code class="language-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);      }    }  }     </code></pre>    <h3><strong>其他问题</strong></h3>    <p>当用户快速滑动(fling)RecyclerView 的时候, RecyclerView 需要一段时间来确定其最终位置。 如果用户在快速滑动一个子的水平 RecyclerView,在子 RecyclerView 还在滑动的过程中,如果用户垂直滑动,则是无法垂直滑动的。原因是子 RecyclerView 依然处理了这个垂直滑动事件。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/aea7daa4ad837140cf25e37e61f99471.gif"></p>    <p>所以,在快速滑动后的滚动到静止的状态中,子 View 不应该响应滑动事件了,再次看看 RecyclerView 的 onInterceptTouchEvent() 代码:</p>    <pre>  <code class="language-java">@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;  }     </code></pre>    <p>可以看到,当 RecyclerView 的状态为 SCROLL_STATE_SETTLING (快速滑动后到滑动静止之间的状态)时, RecyclerView 告诉父控件不要拦截事件。</p>    <p>同样的,如果只有一个方向固定,这样处理是没问题的。</p>    <p>针对我们这个嵌套的情况,父 RecyclerView 应该只拦截垂直滚动事件,所以可以这么修改父 RecyclerView:</p>    <pre>  <code class="language-java">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 */    }  }     </code></pre>    <p>下图为最终的结果:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/94595794c10bfc28ddd373176968c52a.gif"></p>    <p>注意示例项目中使用 kotlin,所以需要配置 kotlin 插件。</p>    <p> </p>    <p> </p>    <p> </p>    <p>来自:http://blog.chengyunfeng.com/?p=1017</p>    <p> </p>