Android常見滑動(dòng)沖突場(chǎng)景及解決方案
前言
Android滑動(dòng)沖突是Android開發(fā)中常見的問題,在同一個(gè)界面,可能存在多個(gè)View可以響應(yīng)滑動(dòng)事件。如果這些View滑動(dòng)方向一致,則會(huì)導(dǎo)致滑動(dòng)沖突。本篇文章將圍繞常見的滑動(dòng)沖突場(chǎng)景展開,并介紹對(duì)應(yīng)的解決方案。
1. 滑動(dòng)沖突的場(chǎng)景
常見的滑動(dòng)沖突總共有3種:
- 同向滑動(dòng)沖突:外部ViewGroup和內(nèi)部子View的滑動(dòng)方向一致,比如ViewGroup可以上下滑動(dòng),子View也可以上下滑動(dòng)。
- 異向滑動(dòng)沖突:外部滑動(dòng)方向和內(nèi)部滑動(dòng)方向不一致。
- 混合滑動(dòng)沖突:以上兩種情況的嵌套。

滑動(dòng)沖突產(chǎn)生的根本原因在于發(fā)生滑動(dòng)時(shí),不知道是讓ViewGroup滑動(dòng),還是讓ViewGroup內(nèi)的子View滑動(dòng)。因此解決滑動(dòng)沖突也很簡(jiǎn)單,就是根據(jù)當(dāng)前布局內(nèi)容的狀態(tài),以及當(dāng)前的滑動(dòng)方向來判斷到底是讓哪個(gè)View滑動(dòng)。
比如,一個(gè)經(jīng)典的滑動(dòng)沖突場(chǎng)景:
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</ScrollView>
ScrollView和RecyclerView在豎直方向上都可以滑動(dòng),所以當(dāng)滑動(dòng)產(chǎn)生時(shí),系統(tǒng)無法知道用戶到底是想讓哪一層滑動(dòng),所以當(dāng)手指滑動(dòng)的時(shí)候就會(huì)出現(xiàn)問題,要么只有一層能滑動(dòng),要么就是內(nèi)外兩層都滑動(dòng)地很卡頓。
要處理該滑動(dòng)沖突場(chǎng)景,首先應(yīng)該明確手指的滑動(dòng)方向是水平還是豎直方向,如果是水平方向,那么事件應(yīng)該交由ScrollView處理,因?yàn)镽ecyclerView是豎向布局,處理不了水平滑動(dòng)事件。如果是豎直方向的滑動(dòng),則應(yīng)先判斷RecyclerView此時(shí)豎直方向是否還可以滑動(dòng),若能就交給RecyclerView來處理,否則依然交給ScrollView處理。
以上思路在解決剩下兩種情況時(shí)依然適用,核心思想就是要根據(jù)滑動(dòng)的方向以及業(yè)務(wù)的場(chǎng)景來判斷此時(shí)應(yīng)該由哪個(gè)View處理滑動(dòng)事件。
2. 滑動(dòng)沖突的處理規(guī)則
對(duì)于異向滑動(dòng)沖突(比如ViewPager嵌套R(shí)ecyclerView),當(dāng)用戶左右滑動(dòng)時(shí),需要讓外部的View攔截點(diǎn)擊事件,當(dāng)用戶上下滑動(dòng)時(shí),需要讓內(nèi)部的View攔截點(diǎn)擊事件。具體就是:根據(jù)滑動(dòng)是水平滑動(dòng)還是豎直滑動(dòng)來判斷到底由誰來攔截事件。

如圖所示,根據(jù)滑動(dòng)過程中兩個(gè)點(diǎn)之間的坐標(biāo)就可以得出到底是水平滑動(dòng)還是豎直滑動(dòng)。根據(jù)坐標(biāo)判斷滑動(dòng)方向有很多方法,比如可以根據(jù)滑動(dòng)路徑和水平方向所形成的夾角,也可以依據(jù)水平方向和豎直方向的速度差來判斷。不過一般都是采用水平方向和豎直方向之間的距離差來判斷。比如豎直方向之間的滑動(dòng)距離大就判斷為豎直滑動(dòng),否則判斷為水平滑動(dòng)。
對(duì)于同向和混合滑動(dòng)沖突,一般要根據(jù)具體的業(yè)務(wù)邏輯來處理規(guī)則,什么時(shí)候讓外部ViewGroup攔截,什么時(shí)候要內(nèi)部View攔截。
3. 滑動(dòng)沖突的解決方式
3.1 外部攔截法
? 外部攔截法是指事件都先經(jīng)過父視圖的攔截處理,如果父視圖需要此事件就攔截,如果不需要此事件就分發(fā)給子視圖。在上面的例子中,指的是事件都先經(jīng)過ScrollView的攔截處理,如果ScrollView不需要攔截此事件,那么就正常分發(fā)給RecyclerView。外部攔截法需要重寫父容器的onInterceptTouchEvent方法,在內(nèi)部做相應(yīng)的攔截即可。代碼如下:
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if (父容器需要當(dāng)前點(diǎn)擊事件) {
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
以上是外部攔截法的典型邏輯,修改ViewGroup的onInterceptTouchEvent()方法,因此稱為外部攔截法。需要注意的是,在ACTION_DOWN事件到來時(shí),父容器必須返回false,即不攔截down事件,因?yàn)閂iewGroup一旦攔截了down事件,那么事件傳遞就會(huì)在ViewGroup終止,無論ViewGroup是否攔截事件,接下來的事件都不會(huì)傳遞給子視圖RecyclerView。在move事件到來時(shí),就根據(jù)此ViewGroup是否需要事件來判斷攔不攔截,如果攔截了,那么RecyclerView不會(huì)接收到后續(xù)的事件,由ScrollView自己處理事件。對(duì)于up事件,不需要做什么,默認(rèn)不攔截,因?yàn)閡p事件本身沒有太多意義。
3.2 內(nèi)部攔截法
內(nèi)部攔截法指的是父視圖不攔截任何事件,所有的事件都傳遞給子視圖,如果子視圖需要此事件就直接消耗,否則交由父視圖處理。這種方法先將事件交給子視圖,然后再傳給父視圖,與Android中的事件默認(rèn)分發(fā)順序不一致,需要配合requestDisallowInterceptTouchEvent()方法才能工作。與外部攔截法相比,內(nèi)部攔截法會(huì)更復(fù)雜一點(diǎn)。需要重寫子元素的dispatchTouchEvent代碼如下:
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 要求父容器不要攔截,確保子View能收到完整事件序列
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
// 當(dāng)父容器需要此類點(diǎn)擊事件時(shí)
if (父容器需要此類點(diǎn)擊事件) { // 這個(gè)條件需要具體實(shí)現(xiàn)
// 允許父容器攔截
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
除了子元素需要做處理之外,父元素也要默認(rèn)攔截除了ACTION_DOWN以外的其他事件,這樣當(dāng)子元素調(diào)用parent.requestDisallowInterceptTouchEvent(false)方法時(shí),父元素才能繼續(xù)攔截所需的事件。父元素所做的修改如下:
public boolean onInterceptTouchEvent(MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}
為什么父容器不能攔截ACTION_DOWN事件呢?
在上一篇文章中已經(jīng)分析過,ACTION_DOWN事件不受FLAG_DISALLOW__INTERCEPT這個(gè)標(biāo)記位的控制,所以一旦父容器攔截ACTION_DOWN事件,那么所有的事件都無法傳遞到子元素中去,這樣內(nèi)部攔截法就無法起作用了。
下面將通過一個(gè)小demo來具體體驗(yàn)一下整個(gè)過程。
4. 解決滑動(dòng)沖突的實(shí)例
這里采用ScrollView嵌套R(shí)ecyclerView的方案,并分別使用外部攔截法和內(nèi)部攔截法的解決滑動(dòng)沖突。
首先,看看主要布局結(jié)構(gòu):
<!-- 水平滑動(dòng)的分類容器 -->
<HorizontalScrollView
android:id="@+id/horizontalScrollView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="#FFFFFF"
android:scrollbars="none">
<LinearLayout
android:id="@+id/categoryContainer"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal">
<!-- 分類1:手機(jī) -->
<LinearLayout
android:layout_width="360dp"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#FAFAFA">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#FF5722"
android:gravity="center"
android:padding="15dp"
android:text="?? 手機(jī)分類"
android:textColor="#FFFFFF"
android:textSize="18sp"
android:textStyle="bold" />
<!-- 垂直的RecyclerView -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView1"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
<!-- 分類2:電腦 -->
<LinearLayout
android:layout_width="360dp"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#F5F5F5">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#3F51B5"
android:gravity="center"
android:padding="15dp"
android:text="?? 電腦分類"
android:textColor="#FFFFFF"
android:textSize="18sp"
android:textStyle="bold" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView2"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
<!-- 分類3:配件 -->
<LinearLayout
android:layout_width="360dp"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#FAFAFA">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#009688"
android:gravity="center"
android:padding="15dp"
android:text="?? 配件分類"
android:textColor="#FFFFFF"
android:textSize="18sp"
android:textStyle="bold" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView3"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
</LinearLayout>
</HorizontalScrollView>
這個(gè)布局中主要就是一個(gè)水平滑動(dòng)的ScrollView和三個(gè)RecyclerView。這個(gè)RecyclerView是豎直滑動(dòng)的,而外部的ScrollView是水平滑動(dòng)的,這就產(chǎn)生了異向滑動(dòng)沖突,這種滑動(dòng)沖突也是最常見的滑動(dòng)沖突。這里沒有采用ViewPager作為外部布局是因?yàn)閂iewPager已經(jīng)幫我們處理了滑動(dòng)沖突,我們不需要自己手動(dòng)去處理。
先看看沒有解決滑動(dòng)沖突時(shí)的滑動(dòng)效果:

可以看到,滑動(dòng)非??D,有時(shí)候豎直方向劃不動(dòng),慢慢滑才能勉強(qiáng)豎直滑動(dòng)。下面分別介紹外部攔截法和內(nèi)部攔截法處理滑動(dòng)沖突。
- 外部攔截法
? 新建CustomHorizontalScrollView繼承HorizontalScrollView,重寫onInterceptTouchEvent方法,在down事件時(shí)記錄手指點(diǎn)擊的坐標(biāo),move事件時(shí)計(jì)算滑動(dòng)的距離,并分別計(jì)算水平方向和豎直方向的距離差并進(jìn)行條件判斷,如果是橫向滑動(dòng),則自己處理事件,否則就交給子RecyclerView處理。
public class CustomHorizontalScrollView extends HorizontalScrollView {
private float lastX, lastY;
public CustomHorizontalScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
float x = ev.getX();
float y = ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
// 必須讓父類處理,否則不能接收后續(xù) MOVE
super.onInterceptTouchEvent(ev);
return false;
case MotionEvent.ACTION_MOVE:
float dx = Math.abs(x - lastX);
float dy = Math.abs(y - lastY);
// 橫向滑動(dòng),外部攔截
if (dx > dy && dx > 6) {
return true;
}
// 縱向滑動(dòng),交給內(nèi)部 RecyclerView
return false;
}
return super.onInterceptTouchEvent(ev);
}
}
需要注意的是,down事件的處理必須要交給父容器處理,因?yàn)檫@個(gè)HorizontalScrollView不是頂級(jí)View,事件是從父視圖分發(fā)下來的,所以down事件要交給父視圖來處理,調(diào)用super.onInterceptTouchEvent(ev);方法即可。
在布局中將HorizontalScrollView替換成我們自定義的CustomHorizontalScrollView,然后看看滑動(dòng)效果:

能夠水平滑動(dòng),同時(shí)也不影響RecyclerView的豎直滑動(dòng),說明左右滑動(dòng)和上下滑動(dòng)都能被正確的對(duì)象消費(fèi),這樣異向的滑動(dòng)沖突就解決了。
- 內(nèi)部攔截法
? 首先把外部的CustomHorizontalScrollView改回HorizontalScrollView,新建HorizontalConflictRecyclerView繼承自RecyclerView,重寫它的dispatchTouchEvent方法:
public class HorizontalConflictRecyclerView extends RecyclerView {
private float startX, startY;
public HorizontalConflictRecyclerView(@NonNull Context context) {
super(context);
}
public HorizontalConflictRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public HorizontalConflictRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
float x = ev.getX();
float y = ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = x;
startY = y;
// 告訴父View(HorizontalScrollView)不要攔截,我來處理
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
float dx = Math.abs(x - startX);
float dy = Math.abs(y - startY);
if (dx > dy && dx > 10) {
// 水平滑動(dòng)距離大于垂直滑動(dòng),讓父View(HorizontalScrollView)處理
getParent().requestDisallowInterceptTouchEvent(false);
} else if (dy > dx && dy > 10) {
// 垂直滑動(dòng)距離大于水平滑動(dòng),自己處理(滾動(dòng)RecyclerView)
getParent().requestDisallowInterceptTouchEvent(true);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
getParent().requestDisallowInterceptTouchEvent(false);
break;
}
return super.dispatchTouchEvent(ev);
}
}
還是一樣,先獲取手指點(diǎn)擊屏幕的坐標(biāo),在down事件記錄這個(gè)初始坐標(biāo),同時(shí)調(diào)用requestDisallowInterceptTouchEvent(true);方法告訴父視圖不要攔截事件,由當(dāng)前View來處理。接著當(dāng)move事件發(fā)生時(shí),獲取水平方向和豎直方向的距離差并進(jìn)行條件判斷,水平方向的滑動(dòng)就交給父視圖(HorizontalScrollView)處理,豎直方向的滑動(dòng)就自己處理,同樣也是調(diào)用父視圖的requestDisallowInterceptTouchEvent();方法。最后放回super.dispatchTouchEvent(ev)讓RecyclerView繼續(xù)執(zhí)行自己的事件分發(fā)邏輯。
然后在布局文件里把普通的RecyclerView改成我們自定義的HorizontalConflictRecyclerView,運(yùn)行看看效果:

和外部攔截的效果一致,各個(gè)方向滑動(dòng)都正常。
以上就是Android常見滑動(dòng)沖突場(chǎng)景及解決方案的詳細(xì)內(nèi)容,更多關(guān)于Android常見滑動(dòng)沖突的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android編程實(shí)現(xiàn)等比例顯示圖片的方法
這篇文章主要介紹了Android編程實(shí)現(xiàn)等比例顯示圖片的方法,實(shí)例分析了Android等比例縮放圖片的具體步驟與相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-11-11
Android仿硅谷商城實(shí)現(xiàn)購物車實(shí)例代碼
這篇文章主要介紹了Android購物車編輯實(shí)現(xiàn),小編覺得挺不錯(cuò)的,一起跟隨小編過來看看吧2018-05-05
Android App開發(fā)中使用RecyclerView實(shí)現(xiàn)Gallery畫廊的實(shí)例
這篇文章主要介紹了Android App開發(fā)中使用RecyclerView實(shí)現(xiàn)Gallery畫廊的實(shí)例,比普通的ListView實(shí)現(xiàn)的效果更為強(qiáng)大,需要的朋友可以參考下2016-04-04
Android彈出DatePickerDialog并獲取值的方法
這篇文章主要為大家詳細(xì)介紹了Android彈出DatePickerDialog并獲取值的方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-05-05
SimpleCommand實(shí)現(xiàn)圖片下載(二)
這篇文章主要為大家詳細(xì)介紹了SimpleCommand實(shí)現(xiàn)圖片下載,并顯示到ImageView控件上,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-10-10
Android項(xiàng)目實(shí)戰(zhàn)之百度地圖地點(diǎn)簽到功能
這篇文章主要介紹了Android項(xiàng)目實(shí)戰(zhàn)之百度地圖地點(diǎn)簽到功能,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-04-04
android FragmentTabhost實(shí)現(xiàn)導(dǎo)航分頁
這篇文章主要為大家詳細(xì)介紹了android FragmentTabhost實(shí)現(xiàn)導(dǎo)航分頁,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-08-08
Android 日常開發(fā)總結(jié)的60條技術(shù)經(jīng)驗(yàn)
這篇文章主要介紹了Android日常開發(fā)總結(jié)的技術(shù)經(jīng)驗(yàn)60條,需要的朋友可以參考下2016-03-03

