修復RecyclerView嵌套滾動問題

pcpd8230 8年前發布 | 68K 次閱讀 Android開發 移動開發 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://blog.chengyunfeng.com/?p=1017

 

 本文由用戶 pcpd8230 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!