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