其實(shí) Android 事件分發(fā)機(jī)制在早幾年一直都困擾著我双谆,那時(shí)候處理事件分發(fā)的自定義 View 腦子都是一片白绎秒,老感覺(jué)處理不好一姿。后來(lái)自己看了 android 源碼拖云,也閱讀了很多大牛的文章才算徹底明白,總之掌握 Android 事件分發(fā)機(jī)制是必不可少的踏施,而 Android 事件分發(fā)機(jī)制絕對(duì)不是三言兩語(yǔ)就能說(shuō)得清的石蔗。
而今天由于我們自定義 View 進(jìn)階的需要,自己也是籌備了很久读规。目前雖然網(wǎng)上相關(guān)的文章也不少抓督,很多也寫得非常詳細(xì)燃少,但是多數(shù)文章只是講了講理論束亏,然后配合 Log 打印一下結(jié)果而已。而我準(zhǔn)備不僅帶著大家從源碼的角度進(jìn)行分析阵具,還需要理論結(jié)合實(shí)踐寫幾個(gè)關(guān)于這方面的效果碍遍,這樣相信我們會(huì)有更深的理解。閱讀源碼講究由淺入深阳液,循序漸進(jìn)怕敬,我們就不像其他文章一樣搞混合了,先講 View 的 Touch 事件分發(fā)帘皿,然后再講 ViewGroup 的事件分發(fā)东跪,最后再寫個(gè)幾次效果。我們一貫的套路都是理論結(jié)合實(shí)踐,由淺入深
先來(lái)看幾個(gè)效果虽填,如前幾次我們寫自定義評(píng)分控件的 RatingBar 復(fù)寫了 onTouchEvent()丁恭,這里只是舉個(gè)例子:
public class RatingBar extends View {
public RatingBar(Context context) {
super(context);
}
public RatingBar(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public RatingBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d("TAG", "onTouchEvent execute -> " + event.getAction());
return super.onTouchEvent(event);
}
}
如果想要給這個(gè)控件注冊(cè)一個(gè)點(diǎn)擊事件,只需要調(diào)用:
mRatingBar.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d("TAG","onClick execute");
}
});
如果想給這個(gè)按鈕再添加一個(gè)touch事件斋日,只需要調(diào)用:
mRatingBar.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("TAG", "onTouch execute -> " + event.getAction());
return false;
}
});
上面的代碼如果運(yùn)行起來(lái)牲览,哪一個(gè)會(huì)先執(zhí)行呢? 如果是靠猜恶守,那么我們來(lái)試一下就知道了第献,運(yùn)行程序點(diǎn)擊,打印結(jié)果如下:
可以看到兔港,onTouch 是優(yōu)先于優(yōu)先于 onTouchEvent 優(yōu)先于 onClick 執(zhí)行的庸毫,并且 onTouch 和 onTouchEvent 執(zhí)行了兩次,一次是 ACTION_DOWN 衫樊,一次是 ACTION_UP (你還可能會(huì)有多次 ACTION_MOVE 的執(zhí)行岔绸,如果你在上面觸摸)。因此事件傳遞的順序是先經(jīng)過(guò) onTouch 橡伞,然后經(jīng)過(guò) onTouchEvent 盒揉,再傳遞到 onClick 。
如果留心觀察你會(huì)發(fā)現(xiàn) setOnTouchListener 是有返回值的兑徘,如果返回 ture 刚盈,再次運(yùn)行一下會(huì)怎樣?
我們發(fā)現(xiàn)挂脑,onTouchEvent 和 onClick 方法不再執(zhí)行了藕漱!為什么會(huì)這樣呢?你可以先理解成 onTouch 方法返回 true 就認(rèn)為這個(gè)事件被 onTouch 消費(fèi)掉了崭闲,因而不會(huì)再繼續(xù) onTouchEvent 和 onClick 肋联。到目前位置如果你清楚了,那么面試的時(shí)候基本靠背刁俭,那么自己寫效果的時(shí)候基本靠蒙橄仍。我們肯定不能局限于這個(gè)裝態(tài),接下了我們就帶著疑問(wèn)從源碼的角度分析一下牍戚,為什么會(huì)出現(xiàn)上述情況侮繁?
首先我們需要知道,你點(diǎn)擊或者或者觸摸任何一個(gè) View 都會(huì)調(diào)用 View 的 dispatchTouchEvent() 方法如孝,我們就從這里開(kāi)始分析源碼:
public boolean dispatchTouchEvent(MotionEvent event) {
// 省略部分代碼 ...
boolean result = false;
// 省略部分代碼 ...
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
// 返回 result
return result;
}
省略掉部分代碼之后宪哩,這個(gè)方法就變得非常的簡(jiǎn)潔了,只有短短幾行代碼第晰!我們可以看到锁孟,在這個(gè)方法內(nèi)彬祖,首先是進(jìn)行了一個(gè)判斷,如果li != null品抽,mOnTouchListener != null涧至,(mViewFlags & ENABLED_MASK) == ENABLED 和 mOnTouchListener.onTouch(this, event) 這三個(gè)條件都為真,result 就是 true桑包,否則就去執(zhí)行 onTouchEvent(event) 方法并返回南蓬。
那么 ListenerInfo 到底是什么?我們可以看下源碼哑了,這其實(shí)就是有關(guān) View 所有事件的一個(gè)集合類赘方,如 OnFocusChangeListener , OnScrollChangeListener , OnClickListener 、弱左、窄陡、
static class ListenerInfo {
/**
* Listener used to dispatch focus change events.
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
protected OnFocusChangeListener mOnFocusChangeListener;
/**
* Listeners for layout change events.
*/
private ArrayList<OnLayoutChangeListener> mOnLayoutChangeListeners;
protected OnScrollChangeListener mOnScrollChangeListener;
/**
* Listeners for attach events.
*/
private CopyOnWriteArrayList<OnAttachStateChangeListener> mOnAttachStateChangeListeners;
/**
* Listener used to dispatch click events.
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
public OnClickListener mOnClickListener;
/**
* Listener used to dispatch long click events.
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
protected OnLongClickListener mOnLongClickListener;
/**
* Listener used to dispatch context click events. This field should be made private, so it
* is hidden from the SDK.
* {@hide}
*/
protected OnContextClickListener mOnContextClickListener;
/**
* Listener used to build the context menu.
* This field should be made private, so it is hidden from the SDK.
* {@hide}
*/
protected OnCreateContextMenuListener mOnCreateContextMenuListener;
private OnKeyListener mOnKeyListener;
private OnTouchListener mOnTouchListener;
private OnHoverListener mOnHoverListener;
private OnGenericMotionListener mOnGenericMotionListener;
private OnDragListener mOnDragListener;
private OnSystemUiVisibilityChangeListener mOnSystemUiVisibilityChangeListener;
OnApplyWindowInsetsListener mOnApplyWindowInsetsListener;
}
先看一下條件 li.mOnTouchListener 這個(gè)變量是在哪里賦值的呢?我們尋找之后在View里發(fā)現(xiàn)了如下方法:
/**
* Register a callback to be invoked when a touch event is sent to this view.
* @param l the touch listener to attach to this view
*/
public void setOnTouchListener(OnTouchListener l) {
getListenerInfo().mOnTouchListener = l;
}
ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}
第二個(gè)條件 mOnTouchListener 正是在 setOnTouchListener 方法里賦值的拆火,也就是說(shuō)只要我們給控件注冊(cè)了 touch 事件跳夭,mListenerInfo 和 mListenerInfo.mOnTouchListener 就一定被賦值了。
第三個(gè)條件(mViewFlags & ENABLED_MASK) == ENABLED是判斷當(dāng)前點(diǎn)擊的控件是否是enable的们镜,默認(rèn)都是enable的币叹,因此這個(gè)條件恒定為 true 。
第四個(gè)條件就比較關(guān)鍵了模狭,mOnTouchListener.onTouch(this, event)颈抚,其實(shí)也就是去回調(diào)控件注冊(cè) touch 事件時(shí)的 onTouch 方法。也就是說(shuō)如果我們?cè)?onTouch 方法里返回true嚼鹉,就會(huì)讓這三個(gè)條件全部成立贩汉,從而 result 是 true , 那么 onTouchEvent 就不會(huì)被執(zhí)行 锚赤。如果我們?cè)?onTouch 方法里返回 false匹舞,就會(huì)去執(zhí)行 onTouchEvent() 方法。
現(xiàn)在我們可以結(jié)合前面的例子來(lái)分析一下了线脚,首先在 dispatchTouchEvent 中最先執(zhí)行的就是 onTouch 方法赐稽,因此 onTouch 肯定是要優(yōu)先于 onTouchEvent 方法,也是印證了剛剛的打印結(jié)果酒贬。而如果在 onTouch 方法里返回了 true又憨,不會(huì)再執(zhí)行 onTouchEvent 。但是到目前位置我們還沒(méi)有看到 onClick 執(zhí)行锭吨,但是我們可以猜到,onClick的調(diào)用肯定是在onTouchEvent(event)方法中的寒匙!那我們馬上來(lái)看下onTouchEvent的源碼零如,如下所示:
public boolean onTouchEvent(MotionEvent event) {
// 省略部分代碼
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
performClick();
}
mIgnoreNextUpEvent = false;
break;
// 省略部分代碼
}
return true;
}
return false;
}
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}
相較于剛才的 dispatchTouchEvent 方法躏将,onTouchEvent 方法復(fù)雜了很多,不過(guò)沒(méi)關(guān)系考蕾,我們只挑重點(diǎn)看就可以了祸憋。switch 中如果當(dāng)前的事件是抬起手指,則會(huì)進(jìn)入到 MotionEvent.ACTION_UP 這個(gè) case 當(dāng)中肖卧。在經(jīng)過(guò)種種判斷之后蚯窥,會(huì)執(zhí)行到 performClick() 方法,可以看到塞帐,只要 mListenerInfo.mOnClickListener 不是 null拦赠,就會(huì)去調(diào)用它的 onClick 方法,那 mListenerInfo.mOnClickListener 又是在哪里賦值的呢葵姥?我們大概能猜到肯定在 setOnclickLstener 方法中:
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}
View 的 Touch 事件分發(fā)我們就講到這里了荷鼠,下一節(jié)我將帶著大家一起了解 ViewGroup 的事件分發(fā)和事件攔截,在源碼的基礎(chǔ)上寫幾個(gè)效果榔幸,我想應(yīng)該可以說(shuō)就堪稱完美了允乐。
所有分享大綱:Android進(jìn)階之旅 - 自定義View篇