宣告:本文章已獨家授權郭霖公眾號
目錄
一、發現問題
二、原因分析
1、事件分發的機制
2、原因猜測
三、RecyclerView事件攔截機制
switch (action)部分
回傳值
ACTION_DOWN
ACTION_MOVE
ACTION_UP
小結
switch (action) 之前
OnItemTouchListener介面
攔程序序
小結
四、解決問題
方案一
方案二
五、總結
一、發現問題
最近在利用RecyclerView做開發的時候,遇到了一點問題:
給RecyclerView的子項添加事件監聽的時候,發現ACITON_DOWN能得到處理,ACITON_UP和ACTION_MOVE卻得不到處理,
二、原因分析
在剛開始開發需求的時候我還不太了解事件分發的機制,所以我先去學習了一下事件分發,這里對事件分發做一個簡單的總結,
1、事件分發的機制
事件分發是由三個方法配合完成的:
-
dispatchTouchEvent() 分發事件
-
onInterceptTouchEvent() 攔截事件
-
onTouchEvent() 處理事件
而且事件分發的順序是:
Activity -> ViewGroup -> View
借助一張圖來配合理解:

(圖源:Android事件分發機制詳解:史上最全面、最易懂 - 天涯海角路 - 博客園 (cnblogs.com))
通過圖片我們可以看到,ViewGroup是比較特殊的,onInterceptTouchEven()是他獨有方法,他可以將事件攔截下來選擇不分發給下一層的View而是自己處理,
2、原因猜測
在了解了事件分發的機制過后,我就猜測會不會是因為RecyclerView將事件攔截了下來,因為RecyclerView肯定有他自己的事件監聽,當ACTION_MOVE的時候應該會觸發滾動,加載資料然后顯示到螢屏上,
那如果真的是被RecyclerView給攔截了,那我又產生了新的疑問:
-
根據事件分發的機制,再
ACITON_DOWN的時候應該就決定了targetView是itemView,為什么在ACITON_MOVE的時候會目標View又變成了RecyclerView? -
RecyclerView是怎么做到只攔截
ACTION_MOVE和ACTION_UP而不攔截ACTION_DOWN的呢? -
那如果想要實作子項自己處理
ACTION_MOVE和ACTION_UP要怎么處理呢?
為了驗證我的猜想和解決這些疑問,我決定去RecyclerView的原始碼里一探究竟,
三、RecyclerView事件攔截機制
既然我們要分析的是攔截機制,那么當然應該去onTouchEvent()這個方法里去看,
這里先說明一下,以下貼出來的原始碼并不是全部,我一直覺得分析原始碼不能一行一行的扣,不然思路會很混亂,在這篇文章里,我只把對解決問題有用的部分貼了出來,也能讓大家更好理解,如果有小伙伴有看不懂的地方,可以再配合所有原始碼來理解,
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
...
?
mInterceptingOnItemTouchListener = null;
if (findInterceptingOnItemTouchListener(e)) {
cancelScroll();
return true;
}
...
final int action = e.getActionMasked();
...
?
switch (action) {
case MotionEvent.ACTION_DOWN: {
...
} break
...
case MotionEvent.ACTION_MOVE: {
...
} break;
?
...
?
case MotionEvent.ACTION_UP: {
..
} break;
?
...
}
return mScrollState == SCROLL_STATE_DRGING;
}
總的來說這個函式我把他分為兩個部分,switch(aciton)之前和switch(aciton)部分,我們按倒序分析一下,
switch (action)部分
回傳值
這部分呢我們需要先看一下最后的回傳值,因為回傳值決定了是否攔截,
return mScrollState == SCROLL_STATE_DRGING;
解釋一下mScrollState這個變數,這個變數是用來記錄滑動狀態的,有下面三個值:
//停止滾動
public static final int SCROLL_STATE_IDLE = 0;
?
//正在被外部拖拽,一般為用戶正在用手指滾動
public static final int SCROLL_STATE_DRAGGING = 1;
?
//自動滾動開始
public static final int SCROLL_STATE_SETTLING = 2;
第一個和第二個都好理解,這里解釋一下第三個狀態,
整個RecyclerView里只有在fing()方法里會把mScrollState的值設定為SCROLL_STATE_SETTLING,而fling()這個函式呢,其實就是指當你手指在螢屏上快速滑動時,會觸發自動滑動,就像下面這樣:

這個功能其實大家日常使用中也經常會用到,大家知道這個狀態的含義即可,
那這個回傳值的意思就是判斷最后RecyclerView是否是手指正在拖著滾動的狀態,如果是正在滾動,那么就會攔截本次事件;反之則不攔截,
ACTION_DOWN
進入到ACTION_DOWN操作,前面部分和后面部分都是設定一些狀態(觸點的位置,布局滾動方向是豎直的還是垂直的),最重要的是中間的判斷,
case MotionEvent.ACTION_DOWN:
...
?
if (mScrollState == SCROLL_STATE_SETTLING) {
getParent().requestDisallowInterceptTouchEvent(true);
setScrollState(SCROLL_STATE_DRAGGING);
stopNestedScroll(TYPE_NON_TOUCH);
}
?
...
break;
如果目前的狀態是在自動滾動的狀態下,里面就會將mScrollState設定為SCROLL_STATE_DRAGGING,
這里其實很好想明白,當你的串列在自動快速滾動的程序中,手指再按上去,是需要他立即停下來的,那么理所應當這里需要把事件攔截下來RecyclerView自己處理,就像下面這樣就會攔截:

不攔截的話,就只能等他自己停下來,那這個自動滾動就是不可控的了,
那如果不是這種情況,便不會攔截,那么子項就可以接受到ACTION_DOWN事件啦,
ACTION_MOVE
case MotionEvent.ACTION_MOVE: {
...
final int x = (int) (e.getX(index) + 0.5f);
final int y = (int) (e.getY(index) + 0.5f);
if (mScrollState != SCROLL_STATE_DRAGGING) {
final int dx = x - mInitialTouchX;
final int dy = y - mInitialTouchY;
boolean startScroll = false;
if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
mLastTouchX = x;
startScroll = true;
}
if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
mLastTouchY = y;
startScroll = true;
}
if (startScroll) {
setScrollState(SCROLL_STATE_DRAGGING);
}
}
} break;
ACTION_MOVE里面就很簡單啦,
-
如果當前mScrollState的狀態是正在滾動,那么就不做任何處理了,這個時候表示手指正在拖著串列滾動,自然是要攔截下來的,
-
如果當前mScrollState的狀態不是滾動,那就會進行一個判斷了,判斷你手指的移動的距離是否在相應方向上超過了一個閾值,如果超過了這個閾值,說明你想要開始滑動了,那么這個時候又會呼叫
setScrollState(SCROLL_STATE_DRAGGING)來將mScrollState的值設定為滾動,將事件攔截下來,
ACTION_UP
ACTION_UP里并沒有對mScrollState進行修改和賦值,所以這個時候也就會根據是否正在滑動來判斷是否攔截事件了,
小結
RecyclerView確實會攔截事件,會對最基本的三個事件根據情況攔截:
-
ACTION_DOWN:當串列在自動滾動的狀態下會攔截,用于處理停止滾動, -
ACTION_MOVE:當手指移動的距離在對應方向上超過了閾值,就會攔截掉事件,用于串列滾動, -
ACTION_UP:根據當前串列是否處于滾動狀態選擇是否攔截,
這部分的內容其實就已經能證實我們的猜想了,
switch (action) 之前
看到這里你可能會好奇,前面不是已經能證實猜想了嗎?別急,在分析原始碼的時候我還發現一個東西,短短的幾行代碼,展現出RecyclerView的靈活性,這也就是為什么我要把這部分放到后面來說,
mInterceptingOnItemTouchListener = null;
if (findInterceptingOnItemTouchListener(e)) {
cancelScroll();
return true;
}
我們先從mInterceptingOnItemTouchListener的型別OnItemTouchListener介面開始說起吧,
OnItemTouchListener介面
熟悉ListView的同學都知道,ListView可以通過setOnItemClickListener()來給一個ItemView添加事件的監聽器,而RecyclerView并沒有這樣的方法,
那么你可能就有疑問了,為什么RecyclerView在各方面的設計都要優于ListView,偏偏在點擊事件上卻沒有處理的非常好呢?其實不是這樣的,ListView在點擊事件上處理得并不人性化,setOnItemClickListener()方法注冊的是子項的點擊事件,但如果我想點擊的是子項里具體的某一個按鈕呢?雖然ListView也能做到,但是實作起來就相對比較麻煩了,為此,RecyclerView干脆直接摒棄了子項點擊事件的監聽器,讓所有的點擊事件都由具體的View去注冊,就再沒有這個困擾了,
(摘自郭霖《第一行代碼》)
郭神在書中給出的方法,也是在Adapter的onCreateViewHolder()方法里去給每一個子項系結事件監聽,這樣做確實更靈活,但同時因為每個子項都系結了事件監聽,在記憶體上也會有一定的消耗,其實RecyclerView內部也有一個介面,能夠實作對整個RecyclerView的監聽,
public interface OnItemTouchListener {
boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e);
?
?
void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e);
?
?
void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept);
}
前兩個通過方法名我們可以輕易的猜出他們的目的,不就是ViewGroup里事件分發的兩個函式嗎,
我們重點說一下第三個函式,第三個函式在ViewGroup里也有實作,他的作用是設定ViewGroup是否開啟事件攔截,也解釋說,通過這個函式我們可以在子View里設定父ViewGroup關閉攔截,這樣就能讓子View自行處理事件了,
那對于整個介面的作用,這里我放一下官方的注釋,
An OnItemTouchListener allows the application to intercept touch events in progress at the view hierarchy level of the RecyclerView before those touch events are considered for RecyclerView's own scrolling behavior.
This can be useful for applications that wish to implement various forms of gestural manipulation of item views within the RecyclerView. OnItemTouchListeners may intercept a touch interaction already in progress even if the RecyclerView is already handling that gesture stream itself for the purposes of scrolling.
翻譯一下,大概意思就是這個監聽器允許在RecyclerView考慮自己的滾動事件之前,在ViewGroup層面攔截事件,
說人話就是RecyclerView在處理事件的時候,得先看這個監聽器要不要攔截這個事情,如果監聽器要攔截,那么RecyclerView就沒資格自己處理了,
實作了對RecyclerView整個視圖的監聽,允許我們自定義對一些特定手勢的處理,
用這個介面有什么好處呢?
-
節省記憶體,在運行期間只有一個監聽器,不像之前RecyclerView的每個子項都要設定一個監聽器,
-
對于整個面板來說更加靈活,如果說我們需要對整個面板有一些自定義的手勢操作,那么就只能通過實作這個介面,去子項里實作已經不太可能了,
攔程序序
因為本文篇幅原因,就不展示怎么去實作了,我們這里通過原始碼分析一下他是如何做到讓RecyclerView沒資格處理自己的滾動的,
private final ArrayList<OnItemTouchListener> mOnItemTouchListeners =
new ArrayList<>();
private OnItemTouchListener mInterceptingOnItemTouchListener;
首先是有兩個全域變數,一個用來存放所有的自定義實作的OnItemTouchListener,我們可以通過呼叫addOnItemTouchListener()來添加監聽器,這里也說明了一個RecycerView里可以自定義多個監聽器,另一個是用來記錄攔截事件的監聽器,可能這里有點懵,看到下面就能明白了,
public void addOnItemTouchListener(@NonNull OnItemTouchListener listener) {
mOnItemTouchListeners.add(listener);
}
然后我們回到RecyclerView的onInterceptTouchEvent()那五行代碼
mInterceptingOnItemTouchListener = null;
if (findInterceptingOnItemTouchListener(e)) {
cancelScroll();
return true;
}
先將mInterceptingOnItemTouchListener置為null,是為了避免上一次賦值的mInterceptingOnItemTouchListener沒有被銷毀,導致出錯,
然后我們到findInterceptingOnItemTouchListener()方法里去看看,
private boolean findInterceptingOnItemTouchListener(MotionEvent e) {
int action = e.getAction();
final int listenerCount = mOnItemTouchListeners.size();
for (int i = 0; i < listenerCount; i++) {
final OnItemTouchListener listener = mOnItemTouchListeners.get(i);
if (listener.onInterceptTouchEvent(this, e) && action != MotionEvent.ACTION_CANCEL) {
mInterceptingOnItemTouchListener = listener;
return true;
}
}
return false;
}
這里的邏輯非常簡單,去遍歷監聽器陣列,如果發現其中一個監聽器攔截了此類事件并且事件不是ACTION_CANCEL ,那么就給mInterceptingOnItemTouchListener賦值,這里說明了mInterceptingOnItemTouchListener的用處,記錄了攔截事件的監聽器,
然后如果找到了這么一個監聽器,回傳true,那么RecyclerView就會取消滾動,幫監聽器直接攔截下本次事件,確保不會往下分發,從這里就能看出這個監聽器的優先級了,
而對事件的處理的入口呢,是在RecyclerView的onTouchEvnet()里面,
@Override
public boolean onTouchEvent(MotionEvent e) {
...
if (dispatchToOnItemTouchListeners(e)) {
cancelScroll();
return true;
}
...
}
這里呼叫的是dispatchToOnItemTouchListeners()這個方法
private boolean dispatchToOnItemTouchListeners(MotionEvent e) {
if (mInterceptingOnItemTouchListener == null) {
if (e.getAction() == MotionEvent.ACTION_DOWN) {
return false;
}
return findInterceptingOnItemTouchListener(e);
} else {
mInterceptingOnItemTouchListener.onTouchEvent(this, e);
final int action = e.getAction();
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
mInterceptingOnItemTouchListener = null;
}
return true;
}
}
在這個方法里,如果mInterceptingOnItemTouchListener不為空,那么就在這里呼叫它的onTouchEvent()去處理,回傳了true,ReyclerView自然就不會自己處理了,
小結
這個就是自定義的onItemTouchListener的攔程序序了,
-
我們自定義實作的onItemTouchListener,需要通過
addOnItemTouchListener()添加到RecyclerView里, -
RecyclerView在判斷攔截事件時,會優先判斷有沒有自定義的onItemTouchListener要攔截此次事件,如果有,則會幫他攔截下來,
-
RecyclerView在處理事件時,也會優先判斷判斷有沒有自定義的onItemTouchListener要處理該次事件,如果有,那就交給它處理,自己不再處理,
四、解決問題
在分析完整個攔截機制后,我們就可以有兩套解決方案了,具體方案可以根據需求自行選擇,
方案一
這種方案推薦用于針對子項的某一具體組件的事項,比如RecyclerView的子項是一個RelativeLayout,事件只針對其中的一個Button,
-
如果子View不需要自己處理
ACTION_MOVE,只需要在ACTION_UP里做一些收尾操作,那么可以把收尾操作添加一份到ACTION_CANCEL里, -
如果子View需要自己處理
ACTION_MOVE和ACTION_UP,那么就可以通過requestDisallowInterceptTouchEvent(boolean disallowIntercept)來設定不讓RecyclerView對事件進行攔截,不過這種方法不建議添加到ACTION_DOWN里,會導致串列無法滑動,
方案二
這種方案用于針對子項或者整個RecyclerView的事件,
通過實作onItemTouchListener介面來處理自己需要的事件,通過手指按下的位置獲取到具體的子項,
五、總結
做一個整體的總結,
-
ReyclerView的ItemView的事件,特別是
ACTION_MOVE和ACTION_UP容易被RecyclerView攔截,但是會發送一個ACTION_CANCEL給子View用來處理一些收尾作業, -
如果ItemView不希望被RecyclerView給攔截,可以通過
parent.requestDisallowInterceptTouchEvent(true)來設定,這樣就不會被攔截, -
RecyclerView提供了一個內部介面onItemTouchListener用于對整個RecyclerView進行監聽,可以實作更靈活的功能,優先級高于RecyclerView自己的事件處理,
最后,非常感謝你可以看到這里,我是一個即將大四的實習生,Android的知識體系還沒有成為一個牢固的系統,在這之前我甚至都不知道什么是事件分發,本篇內容全是自己學習相關知識后讀原始碼的理解,難免會有差錯,如果發現了錯誤希望大家諒解并為我指出錯誤,也非常希望這篇文章能給你帶來一點幫助,
感謝你的閱讀
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/296639.html
標籤:其他
上一篇:Android:“金九銀十”戰役打響還沒看到跟BAT大佬的差距就危險了!來看看如何在大廠面試一擊而中
下一篇:移動端性能優化—啟動速度
