前言
在Android View 事件分發機制源碼詳解(ViewGroup篇)一文中,主要對ViewGroup#dispatchTouchEvent的源碼做了相應的解析,其中說到在ViewGroup把事件傳遞給子View的時候,會調用子View的dispatchTouchEvent,這時分兩種情況,如果子View也是一個ViewGroup那么再執行同樣的流程繼續把事件分發下去,即調用ViewGroup#dispatchTouchEvent;如果子View只是單純的一個View,那么調用的是View#dispatchTouchEvent。因此,本文將分析View(非ViewGroup)的事件分發、處理機制。
View#dispatchTouchEvent
事件來到View的時候,會調用該方法,前提是你的自定義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)),這個主要是判斷當前事件到來的時候,窗口有沒有被遮擋,如果被遮擋則會直接返回false,從而中斷事件的處理。如果窗口沒被遮擋,那么會正常處理事件。在IF體內部,首先定義了一個ListenerInfo,那么這個ListenerInfo是什么呢?我們跟進去看看:
static class ListenerInfo {
public OnClickListener mOnClickListener;
protected OnLongClickListener mOnLongClickListener;
private OnKeyListener mOnKeyListener;
private OnTouchListener mOnTouchListener;
...
}
可以看到,這是View里面的一個內部類,定義了一系列的Listener,其中有我們經常用到的onClickListener,這里是獲取當前View所設置的Listener。接著是①號處的一個判斷,判斷當前View是否設置了onTouchListener,如果設置了onTouchListener的話,則會調用onTouchListener.onTouch方法,然后根據onTouch方法的返回值來設置result,表示事件是否被處理。這里可以看出:onTouchListener的優先級最高,如果在onTouchListener#onTouch中返回true即消耗了事件,那么就無必要繼續執行下面的語句了。如果沒有設置onTouchListener或者該監聽器內部沒有消耗事件,那么就會執行②號代碼,來調用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);
}
以上判斷了當前View是否可用,如果不可用則進入IF體,根據注釋我們知道,即使是不可以狀態下的View,如果它自身是可點擊或者可長按的話,一樣會消耗事件,只是不作出任何反應罷了。
2、接著往下看:
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
這里判斷是否設置了mTouchDelegate,這個表示View的代理,即如果設置了代理,那么當前View的點擊事件會交給代理的View來處理,調用代理View的onTouchEvent方法,如果代理View消耗了事件,那么相當于當前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;
}
首先是判斷當前View是否可以點擊或者長按,其中一個為true的話,就會進入IF體。進入IF體后,是對事件進行判斷,可以看到最后會返回true,即事件最后會被消耗。也就是說,如果一個View是clickable或者long_clickable的話,該onTouchEvent方法會返回true,把事件消耗掉。
我們看看對ACTION_UP的事件進行響應的部分,首先會判斷當前View是否是pressed狀態,即按下狀態,如果是按下狀態就會觸發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;
}
可以看出,這里檢測了當前View是否設置了onClickListener,如果設置了那么回調它的onClick方法,所以我們平時對一個Button設置點擊事件之后,都會在其onTouchEvent方法的ACTION_UP邏輯里面得到回調。
這里可以得出結論:onTouchListener、onTouchEvent、onClickListener三者的優先級是:onTouchListener>onTouchEvent>onClickListener。
至此,對于View的事件分發、處理過程分析完畢,接下來總結一下:
1、事件傳遞給View的時候,會調用dispatchTouchEvent()方法,但是View沒有onIntercept方法,所以會接著調用onTouchEvent()方法。
2、如果一個View是可點擊的(clickable或long_clickable),那么它默認會消耗事件。對于一個Button來說,默認是可點擊的,對于一個textView來說,默認是不可點擊的,而對于一個自定義View來說,默認也是不可點擊的,可以在xml布局中設置View的點擊性質。
3、如果對一個View設置了onClickListener監聽,那么確保它的可點擊的,而且接收到了ACTION_DOWN和ACTION_UP事件。
驗證性試驗
以下是驗證性試驗,根據這兩篇文章所述內容來設置不同的場景來驗證以上的源碼分析的正確性。
①首先新建一個ViewGroupA,繼承自LinearLayout,重寫了三個重要方法,但是只是打印了事件,dispatchTouchEvent和onIntercept會調用父類的響應方法,而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內部的一個子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內部只是設置了布局,并無別的代碼,這里不再貼出。
④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>
我們先看看布局圖如下:
上面藍色區域是ViewGroupA,紅色區域是ViewA,運行程序,我們在紅色區域滑動一下,結果如下所示:
可以看出,事件正常分發,從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);
}
運行,結果如下所示:
可以看出,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);
}
運行結果如下:
可以看出,ViewA還是能正常處理ACTION_DOWN事件,但是由于ACTION_MOVE事件被ViewGroup攔截了,所以ViewGroup來處理ACTION_MOVE事件,我們注意到,onIntercept方法來攔截成功后,后續的事件分發流程并不會再次調用,所以一個View攔截了事件后,后續的所有事件都交由這個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);
}
結果如下:
可以看出,super.onTouchEvent(ev)返回了false,表示不消耗事件,為什么會這樣呢?根據本文分析,一個View只有在可點擊的狀態下,自身的onTouchEvent方法才會返回true,這里調用的是super.onTouchEvent表示調用父類的onTouchEvent方法,又由于ViewGroupA繼承自LinearLayout,本身是不可點擊的,所以這里自然會返回false。然后我們看到,最終這些沒被消耗的時候回到了Activity,被Activity消耗掉了。其實這也很好理解,上一篇文章說過,事件的分發是從Activity開始的,不斷往下尋找能消耗事件的子元素,但如果事件沒被子元素消耗,則會逐層返回到Activity。
所以這里得出結論:如果View不消耗除了ACTION_DOWN事件之外的其他事件(因為ACTION_DWON事件會初始化事件序列),這個View依然也會接收后續的事件,同時這些沒被消耗的事件最終會被Activity消耗。
4、ViewGroupA不做任何修改,對ViewA修改,為ViewA設置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 ");
}
});
結果如下:
可以看出,事件分發給子View后,如果設置了onTouchListener,那么直接調用它,如果返回true,那么后續并不會調用onTouchEvent以及onClickListener了。如果返回false,繼而調用onTouchEvent方法,所以onTouchListener的優先級最高,這也符合本文的分析。但是要注意一點,onClickListener在ACTION_UP中起作用,如果子View重寫了onTouchEvent()方法,而最后返回的時候沒有返回super.onTouchEvent(),那么不會調用onClickListener。因為壓根沒有調用到父類的onTouchEvent方法。
至此,對于View的事件分發、處理機制講述完畢,謝謝閱讀。