在Android項目開發中,為了實現需求和兼并用戶體驗,相信很多人都碰到滑動事件沖突的問題。在Android系統中事件分發機制是一個很重要的組成部分,由于這事件分發機制不是本文重點,故不在此多述,如果有想詳細了解的可以自己搜下,網上有很多相關資料詳細描述了Android事件分發機制。
一、問題場景
由于RecyclerView自身的優點,使得它已經基本取代了GridView、ListView,而且ViewPager2也是基于RecyclerView實現的,所以現在涉及到列表的基本都離不開RecyclerView。
本文就就基于項目中采用RecyclerView + ViewPager + Fragment + RecyclerView這種嵌套方式出現了滑動沖突。
QQ截圖20200516115700.png
二、三種解決方式
首先講下當下的幾種處理方式:
- 在父RecyclerView中的事件攔截事件中處理;
自定義父recyclerView并重寫onInterceptTouchEvent()方法,代碼如下:public class ParentRecyclerView extends RecyclerView { public ParentRecyclerView(@NonNull Context context) { this(context,null); } public ParentRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) { this(context, attrs,0); } public ParentRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } //不攔截,繼續分發下去 @Override public boolean onInterceptTouchEvent(MotionEvent e) { //當然這里可能要根據實際場景去處理下,不僅僅是返回false就結束了。 //todo : 實際場景處理代碼 //--------------------------------------------------------------------------------- return false; } }
- 在子RecyclerView中的事件攔截事件中處理;
通過requestDisallowInterceptTouchEvent方法干預事件分發過程,該方法就是通知父布局要不要攔截事件
自定義子RecyclerView并重寫dispatchTouchEvent,如下:public class ChildRecyclerView extends RecyclerView { public ChildRecyclerView (@NonNull Context context) { this(context,null); } public ChildRecyclerView (@NonNull Context context, @Nullable AttributeSet attrs) { this(context, attrs,0); } public ChildRecyclerView (@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { //父層ViewGroup不要攔截點擊事件,true不要攔截,false攔截 getParent().requestDisallowInterceptTouchEvent(true); return super.dispatchTouchEvent(ev); } }
- 采用優先級最高的OnTouchListener;
從事件分發機制上看,OnTouchListener優先級很高,可以通過這個來告訴父布局,不要攔截我的事件recyclerView.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View view, MotionEvent motionEvent) { switch (motionEvent.getAction()){ case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_MOVE: //這里有時要根據自己的場景去寫自己的邏輯 view.getParent().requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: view.getParent().requestDisallowInterceptTouchEvent(false); break; } return true; } });
以上三種方式至于采用哪種要根據自己的實際場景。
三、針對一種方式進行詳解
下面就針對第二種方式在自定義子RecyclerView的做事件攔截處理,因為這種方式正好適合項目解決沖突。
目標 :觸摸子RecyclerView上下滑動時,子列表滑動,當列表滑動到頂部、底部或觸摸點超出子RecyclerView上下邊距時繼續滑動,則父RecyclerView跟著滑動。
- MotionEvent.ACTION_DOWN
按下時記錄按下的x,y值,并重置標記為;float x = ev.getX(); float y = ev.getY(); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: mDownX = x; mDownY = y; lastY = y; disallowInterceptState = 0; getParent().requestDisallowInterceptTouchEvent(true); break;
- MotionEvent.ACTION_MOVE
手指滑動時,通過計算在x,y軸方向移動的距離,判斷哪個方向先移動超過給的距離來判斷移動的方向,若是x軸方向則不攔截(因為ViewPager橫線滑動)次數將標記位設置為2(disallowInterceptState = 2
),若y方向則告訴父View不要攔截并將標記位設置為1(disallowInterceptState = 1
);
繼續move時,不斷檢查是否到View的上下邊緣和列表是否滑動到頂部或底部,當滿足條件時將標記位設置為2,(disallowInterceptState = 2
)告訴父View可以攔截事件了。if (disallowInterceptState == 0) { float absX = Math.abs(x - mDownX); float absY = Math.abs(y - mDownY); if ((absX > 5f || absY > 5f)) { if (absX < absY) { disallowInterceptState = 1; } else { disallowInterceptState = 2; } } } if (getParent() != null && disallowInterceptState != 0) { //y坐標邊界檢測 boolean bl = y < 0 || y > getMeasuredHeight(); disallowInterceptState = bl ? 2 : disallowInterceptState; //若滑動到頂部 && 繼續下滑動,則釋放攔截事件 if((isScrollTop() && lastY < y) || (isScrollBottom() && lastY > y)){ disallowInterceptState = 2; } //檢查滑動到底部或頂部 getParent().requestDisallowInterceptTouchEvent(disallowInterceptState == 1); } lastY = y;
- MotionEvent.ACTION_UP和MotionEvent.ACTION_CANCEL
這兩個事件不需要做其他處理,恢復父view可以攔截事件//父層ViewGroup不要攔截點擊事件 getParent().requestDisallowInterceptTouchEvent(false);
完整代碼ChildRecyclerView.java
public class ChildRecyclerView extends RecyclerView {
public ChildRecyclerView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
private float mDownX, mDownY,lastY;
private int disallowInterceptState = 0;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
float x = ev.getX();
float y = ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownX = x;
mDownY = y;
lastY = y;
disallowInterceptState = 0;
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if (disallowInterceptState == 0) {
float absX = Math.abs(x - mDownX);
float absY = Math.abs(y - mDownY);
if ((absX > 5f || absY > 5f)) {
if (absX < absY) {
disallowInterceptState = 1;
} else {
disallowInterceptState = 2;
}
}
}
if (getParent() != null && disallowInterceptState != 0) {
//y坐標邊界檢測
boolean bl = y < 0 || y > getMeasuredHeight();
disallowInterceptState = bl ? 2 : disallowInterceptState;
//若滑動到頂部 && 繼續下滑動,則釋放攔截事件
if((isScrollTop() && lastY < y) || (isScrollBottom() && lastY > y)){
disallowInterceptState = 2;
}
//檢查滑動到底部或頂部
getParent().requestDisallowInterceptTouchEvent(disallowInterceptState == 1);
}
lastY = y;
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
//父層ViewGroup不要攔截點擊事件
getParent().requestDisallowInterceptTouchEvent(false);
break;
}
return super.dispatchTouchEvent(ev);
}
/**
* 滑動到底部檢查
* @return true滑動到底部,false沒有到底
*/
private boolean isScrollBottom(){
return !canScrollVertically(1);
}
/**
* 滑動到頂部檢查
* @return true滑動到頂部,false沒有到頂
*/
private boolean isScrollTop(){
return !canScrollVertically(-1);
}
}
最后附上效果圖
效果圖.gif
最后附上處理滑動沖突最根本的解決方法:
-
外部攔截法:
指點擊事件都先經過父容器的攔截處理,如果父容器需要此事 件 就 攔 截 , 否 則 就 不 攔 截 。 具 體 方 法 : 需 要 重 寫 父 容 器 的onInterceptTouchEvent 方法,在內部做出相應的攔截。 -
內部攔截法:
指父容器不攔截任何事件,而將所有的事件都傳遞給子容器,如果子容器需要此事件就直接消耗,否則就交由父容器進行處理。具體方法:需要配合 requestDisallowInterceptTouchEvent 方法。
希望能幫助到大家。
每日一句:要想練就絕世武功 就要忍受常人難忍受的痛。