前言
在Android View 事件分發(fā)機(jī)制源碼詳解(ViewGroup篇)一文中围俘,主要對ViewGroup#dispatchTouchEvent的源碼做了相應(yīng)的解析泛释,其中說到在ViewGroup把事件傳遞給子View的時候更哄,會調(diào)用子View的dispatchTouchEvent闪萄,這時分兩種情況誊涯,如果子View也是一個ViewGroup那么再執(zhí)行同樣的流程繼續(xù)把事件分發(fā)下去洪鸭,即調(diào)用ViewGroup#dispatchTouchEvent;如果子View只是單純的一個View仑扑,那么調(diào)用的是View#dispatchTouchEvent览爵。因此,本文將分析View(非ViewGroup)的事件分發(fā)夫壁、處理機(jī)制拾枣。
View#dispatchTouchEvent
事件來到View的時候,會調(diào)用該方法盒让,前提是你的自定義View沒有重寫該方法梅肤。我們先看看它的源碼:
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)) { // 1
result = true;
}
if (!result && onTouchEvent(event)) { // 2
result = true;
}
}
...
return result;
}
我們只看重點部分,這里有一個判斷if(onFilterTouchEventForSecurity(event))邑茄,這個主要是判斷當(dāng)前事件到來的時候姨蝴,窗口有沒有被遮擋,如果被遮擋則會直接返回false肺缕,從而中斷事件的處理左医。如果窗口沒被遮擋,那么會正常處理事件同木。在IF體內(nèi)部浮梢,首先定義了一個ListenerInfo,那么這個ListenerInfo是什么呢彤路?我們跟進(jìn)去看看:
static class ListenerInfo {
public OnClickListener mOnClickListener;
protected OnLongClickListener mOnLongClickListener;
private OnKeyListener mOnKeyListener;
private OnTouchListener mOnTouchListener;
...
}
可以看到秕硝,這是View里面的一個內(nèi)部類,定義了一系列的Listener洲尊,其中有我們經(jīng)常用到的onClickListener远豺,這里是獲取當(dāng)前View所設(shè)置的Listener。接著是①號處的一個判斷坞嘀,判斷當(dāng)前View是否設(shè)置了onTouchListener躯护,如果設(shè)置了onTouchListener的話,則會調(diào)用onTouchListener.onTouch方法丽涩,然后根據(jù)onTouch方法的返回值來設(shè)置result棺滞,表示事件是否被處理。這里可以看出:onTouchListener的優(yōu)先級最高矢渊,如果在onTouchListener#onTouch中返回true即消耗了事件检眯,那么就無必要繼續(xù)執(zhí)行下面的語句了。如果沒有設(shè)置onTouchListener或者該監(jiān)聽器內(nèi)部沒有消耗事件昆淡,那么就會執(zhí)行②號代碼,來調(diào)用View#onTouchEvent()刽严。
View#onTouchEvent
由于源碼較長昂灵,這里分段來講述避凝。
1、先看下面這一段:
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
以上判斷了當(dāng)前View是否可用眨补,如果不可用則進(jìn)入IF體管削,根據(jù)注釋我們知道,即使是不可以狀態(tài)下的View撑螺,如果它自身是可點擊或者可長按的話含思,一樣會消耗事件,只是不作出任何反應(yīng)罷了甘晤。
2含潘、接著往下看:
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
這里判斷是否設(shè)置了mTouchDelegate,這個表示View的代理线婚,即如果設(shè)置了代理遏弱,那么當(dāng)前View的點擊事件會交給代理的View來處理,調(diào)用代理View的onTouchEvent方法塞弊,如果代理View消耗了事件漱逸,那么相當(dāng)于當(dāng)前View消耗了事件。
3游沿、接下來便是onTouchEvent對View事件的具體處理了:
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) {
...
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
...
break;
...
}
return true;
}
首先是判斷當(dāng)前View是否可以點擊或者長按饰抒,其中一個為true的話,就會進(jìn)入IF體诀黍。進(jìn)入IF體后袋坑,是對事件進(jìn)行判斷,可以看到最后會返回true蔗草,即事件最后會被消耗咒彤。也就是說,如果一個View是clickable或者long_clickable的話咒精,該onTouchEvent方法會返回true镶柱,把事件消耗掉。
我們看看對ACTION_UP的事件進(jìn)行響應(yīng)的部分模叙,首先會判斷當(dāng)前View是否是pressed狀態(tài)歇拆,即按下狀態(tài),如果是按下狀態(tài)就會觸發(fā)performClick()方法范咨,我們看看這個方法做了什么故觅,View#performClick:
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;
}
可以看出,這里檢測了當(dāng)前View是否設(shè)置了onClickListener渠啊,如果設(shè)置了那么回調(diào)它的onClick方法输吏,所以我們平時對一個Button設(shè)置點擊事件之后,都會在其onTouchEvent方法的ACTION_UP邏輯里面得到回調(diào)替蛉。
這里可以得出結(jié)論:onTouchListener贯溅、onTouchEvent拄氯、onClickListener三者的優(yōu)先級是:onTouchListener>onTouchEvent>onClickListener。
至此它浅,對于View的事件分發(fā)译柏、處理過程分析完畢,接下來總結(jié)一下:
1姐霍、事件傳遞給View的時候鄙麦,會調(diào)用dispatchTouchEvent()方法,但是View沒有onIntercept方法镊折,所以會接著調(diào)用onTouchEvent()方法胯府。
2、如果一個View是可點擊的(clickable或long_clickable)腌乡,那么它默認(rèn)會消耗事件盟劫。對于一個Button來說,默認(rèn)是可點擊的与纽,對于一個textView來說侣签,默認(rèn)是不可點擊的,而對于一個自定義View來說急迂,默認(rèn)也是不可點擊的影所,可以在xml布局中設(shè)置View的點擊性質(zhì)。
3僚碎、如果對一個View設(shè)置了onClickListener監(jiān)聽猴娩,那么確保它的可點擊的,而且接收到了ACTION_DOWN和ACTION_UP事件勺阐。
驗證性試驗
以下是驗證性試驗卷中,根據(jù)這兩篇文章所述內(nèi)容來設(shè)置不同的場景來驗證以上的源碼分析的正確性。
①首先新建一個ViewGroupA渊抽,繼承自LinearLayout蟆豫,重寫了三個重要方法,但是只是打印了事件懒闷,dispatchTouchEvent和onIntercept會調(diào)用父類的響應(yīng)方法十减,而onTouchEvent方法則返回true。代碼如下:
public class ViewGroupA extends LinearLayout {
public ViewGroupA(Context context) {
super(context);
}
public ViewGroupA(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ViewGroupA(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action){
case MotionEvent.ACTION_DOWN:
Log.d("cylog", "ViewGroupA onTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.d("cylog","ViewGroupA onTouchEvent ACTION_MOVE");
break;
}
return true;
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
Log.d("cylog","ViewGroupA dispatchTouchEvent down");
break;
case MotionEvent.ACTION_MOVE:
Log.d("cylog","ViewGroupA dispatchTouchEvent move");
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
Log.d("cylog","ViewGroupA onInterceptTouchEvent down");
break;
case MotionEvent.ACTION_MOVE:
Log.d("cylog","ViewGroupA onInterceptTouchEvent move");
break;
}
return super.onInterceptTouchEvent(ev);
}
}
②接下來是在ViewGroupA內(nèi)部的一個子View愤估,ViewA帮辟,重寫了dispatchToucheEvent和onTouchEvent方法,如下所示:
package com.chenyu.viewstudy;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
/**
* Created by Administrator on 2016/4/17.
*/
public class ViewA extends View {
public ViewA(Context context) {
super(context);
}
public ViewA(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ViewA(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.d("cylog","ViewA onTouchEvent down");
break;
case MotionEvent.ACTION_MOVE:
Log.d("cylog","ViewA onTouchEvent move");
break;
case MotionEvent.ACTION_UP:
Log.d("cylog","ViewA onTouchEvent up");
break;
}
return super.onTouchEvent(event);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.d("cylog","ViewA dispatchTouchEvent down");
break;
case MotionEvent.ACTION_MOVE:
Log.d("cylog","ViewA dispatchTouchEvent move");
break;
}
return super.dispatchTouchEvent(event);
}
}
③MainActivity內(nèi)部只是設(shè)置了布局玩焰,并無別的代碼由驹,這里不再貼出。
④xml布局文件如下:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.chenyu.viewstudy.ViewGroupA
android:id="@+id/viewgroupa"
android:layout_width="400dp"
android:layout_height="400dp"
android:gravity="center"
android:background="#2e8abb">
<com.chenyu.viewstudy.ViewA
android:id="@+id/viewa"
android:layout_width="200dp"
android:layout_height="200dp"
android:clickable="true"
android:background="#ed132e"/>
</com.chenyu.viewstudy.ViewGroupA>
</RelativeLayout>
我們先看看布局圖如下:
上面藍(lán)色區(qū)域是ViewGroupA,紅色區(qū)域是ViewA,運行程序昔园,我們在紅色區(qū)域滑動一下荔棉,結(jié)果如下所示:
可以看出闹炉,事件正常分發(fā),從ViewGroup開始到View,并在View中得到處理润樱。
以下開始改變條件:
1、ViewGroup攔截ACTION_DOWN事件:
在ViewGroupA中做出如下改動:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
...
}
//對ACTION_DWON攔截羡棵,返回true壹若。
if (ev.getAction() == MotionEvent.ACTION_DOWN){
return true;
}
return super.onInterceptTouchEvent(ev);
}
運行,結(jié)果如下所示:
可以看出皂冰,ViewGroupA攔截了ACTION_DOWN事件店展,那么ViewA接收不到事件了,所以后面的全部事件都由ViewGroupA處理秃流。
2赂蕴、ViewGroup攔截ACTION_MOVE事件:
同樣,在ViewGroupA中做出如下改動:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
...
}
if (ev.getAction() == MotionEvent.ACTION_MOVE){
return true;
}
return super.onInterceptTouchEvent(ev);
}
運行結(jié)果如下:
可以看出舶胀,ViewA還是能正常處理ACTION_DOWN事件概说,但是由于ACTION_MOVE事件被ViewGroup攔截了,所以ViewGroup來處理ACTION_MOVE事件嚣伐,我們注意到糖赔,onIntercept方法來攔截成功后,后續(xù)的事件分發(fā)流程并不會再次調(diào)用轩端,所以一個View攔截了事件后放典,后續(xù)的所有事件都交由這個View處理,并不會再次判斷是否需要攔截基茵,所以這也符合上一篇文章的分析奋构。
3、基于第2點攔截了MOVE事件拱层,同時ViewGroup的onTouchEvent返回值修改弥臼,原來是直接返回true的,表示消耗了事件舱呻,那么這里直接返回super.onTouchEvent(ev):
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action){
...
}
return super.onTouchEvent(event);
}
同時在Activity中重寫onTouchEvent()方法:
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_MOVE:
Log.d("cylog","Activity onTouchEvent ACTION_MOVE");
break;
}
return super.onTouchEvent(event);
}
結(jié)果如下:
可以看出醋火,super.onTouchEvent(ev)返回了false,表示不消耗事件箱吕,為什么會這樣呢芥驳?根據(jù)本文分析,一個View只有在可點擊的狀態(tài)下茬高,自身的onTouchEvent方法才會返回true兆旬,這里調(diào)用的是super.onTouchEvent表示調(diào)用父類的onTouchEvent方法,又由于ViewGroupA繼承自LinearLayout怎栽,本身是不可點擊的丽猬,所以這里自然會返回false宿饱。然后我們看到,最終這些沒被消耗的時候回到了Activity脚祟,被Activity消耗掉了谬以。其實這也很好理解,上一篇文章說過由桌,事件的分發(fā)是從Activity開始的为黎,不斷往下尋找能消耗事件的子元素,但如果事件沒被子元素消耗行您,則會逐層返回到Activity铭乾。
所以這里得出結(jié)論:如果View不消耗除了ACTION_DOWN事件之外的其他事件(因為ACTION_DWON事件會初始化事件序列),這個View依然也會接收后續(xù)的事件娃循,同時這些沒被消耗的事件最終會被Activity消耗炕檩。
4、ViewGroupA不做任何修改捌斧,對ViewA修改笛质,為ViewA設(shè)置onTouchListener和onClickListener
View viewA = findViewById(R.id.viewa);
viewA.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.d("cylog","ViewA onTouchListener down");
break;
case MotionEvent.ACTION_MOVE:
Log.d("cylog", "ViewA onTouchListener move");
}
return true;
}
});
viewA.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d("cylog","ViewA onClickListener ");
}
});
結(jié)果如下:
可以看出,事件分發(fā)給子View后骤星,如果設(shè)置了onTouchListener经瓷,那么直接調(diào)用它,如果返回true洞难,那么后續(xù)并不會調(diào)用onTouchEvent以及onClickListener了舆吮。如果返回false,繼而調(diào)用onTouchEvent方法队贱,所以onTouchListener的優(yōu)先級最高色冀,這也符合本文的分析。但是要注意一點柱嫌,onClickListener在ACTION_UP中起作用锋恬,如果子View重寫了onTouchEvent()方法,而最后返回的時候沒有返回super.onTouchEvent()编丘,那么不會調(diào)用onClickListener与学。因為壓根沒有調(diào)用到父類的onTouchEvent方法。
至此嘉抓,對于View的事件分發(fā)索守、處理機(jī)制講述完畢,謝謝閱讀抑片。