在開發(fā)過程中,對(duì)于點(diǎn)擊事件的處理是很頻繁的。對(duì)于一個(gè)控件(View)來說,onClickListenr()和onTouchListener()有什么區(qū)別和聯(lián)系?我們自定義了一個(gè)控件,點(diǎn)擊自定義控件時(shí)如何不觸發(fā)被它覆蓋的View的點(diǎn)擊事件?這些都和事件分發(fā)機(jī)制有關(guān)。下面就來分析一下,當(dāng)一個(gè)點(diǎn)擊事件發(fā)生時(shí),這個(gè)事件到底是怎么溜達(dá)的?
現(xiàn)在讓我們創(chuàng)建一個(gè)簡(jiǎn)單的Activity,創(chuàng)建一個(gè)TestLinearLayout繼承自LinearLayout,創(chuàng)建一個(gè)Test繼承自Button。
在TestLineaLayout類中,重寫了和事件相關(guān)的代碼,整個(gè)代碼如下:
public class TestLinearLayout extends LinearLayout {
public TestLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d("TestLinerLayout", "onInterceptTouchEvent action = " + ev.getAction());
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d("TestLinerLayout", "dispatchTouchEvent action = " + ev.getAction());
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d("TestLinerLayout", "onTouchEvent action = " + event.getAction());
return super.onTouchEvent(event);
}
}
在TestButton中,同樣也重寫的和事件分發(fā)相關(guān)的代碼,整改代碼如下:
public class TestButton extends Button {
public TestButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i("TestButton", "dispatchTouchEvent action = " + event.getAction());
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("TestButton", "onTouchEvent action = " + event.getAction());
return super.onTouchEvent(event);
}
}
然后是MainActivity的XML布局:
<com.wl.com.testview.TestLinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:id="@+id/myLayout"
android:orientation="vertical">
<com.wl.com.testview.TestButton
android:id="@+id/myButton"
android:text="Click Me"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</com.wl.com.testview.TestLinearLayout>
最后是MainActivity的代碼:
public class MainActivity extends AppCompatActivity implements View.OnClickListener, View.OnTouchListener {
TestButton myButton;
TestLinearLayout linearLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
myButton = (TestButton) findViewById(R.id.myButton);
linearLayout = (TestLinearLayout) findViewById(R.id.myLayout);
myButton.setOnClickListener(this);
myButton.setOnTouchListener(this);
linearLayout.setOnClickListener(this);
linearLayout.setOnTouchListener(this);
}
@Override
public void onClick(View v) {
Log.d("MainActivity", "onClick " + v);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("MainActivity", "onTouch action = "+event.getAction() + v);
return false;
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d("MainActivity", "dispatchTouchEvent action = " + ev.getAction());
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d("MainActivity", "onTouchEvent action = " + event.getAction());
return super.onTouchEvent(event);
}
}
然后我們運(yùn)行項(xiàng)目,點(diǎn)擊button按鈕,會(huì)得到如下的信息:

點(diǎn)擊button以外的地方,會(huì)得到如下的信息:

建議使用模擬器來運(yùn)行點(diǎn)擊事件。如果用真機(jī)的話,會(huì)觸發(fā)大量的
ACTION_MOVE事件。
事件分發(fā)的流程就出來了,那么為什么會(huì)是這樣的呢?注意到事件都是從MainActivity的dispatchTouchEvent()方法開始調(diào)用的,那我們就從這個(gè)方法開始著手看代碼吧。
Activity 的事件分發(fā)
事件傳遞到Activity后,第一個(gè)觸發(fā)的方法是dispatchTouchEvent(),我們看一看這個(gè)方法的源碼:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
好簡(jiǎn)單的代碼,最喜歡看到這種“小清新”風(fēng)格的代碼了。
首先判斷MotionEvent是不是ACTION_DOWN,如果是的話,執(zhí)行onUserInteraction()方法,然后判斷getWindow().superDispatchTouchEvent(ev)是否為true,如果為true,就不會(huì)執(zhí)行onTouchEvent()方法,如果為false則執(zhí)行onTouchEvent()方法。
onUserInteraction()
這個(gè)方法只有當(dāng)ACTION_DOWN時(shí)才會(huì)觸發(fā), 那么這個(gè)方法是干嘛的?我們點(diǎn)進(jìn)去看,會(huì)發(fā)現(xiàn)啥也沒有。這個(gè)方法是空方法:
/**
* Called whenever a key, touch, or trackball event is dispatched to the
* activity. Implement this method if you wish to know that the user has
* interacted with the device in some way while your activity is running.
* This callback and {@link #onUserLeaveHint} are intended to help
* activities manage status bar notifications intelligently; specifically,
* for helping activities determine the proper time to cancel a notfication.
*
* <p>All calls to your activity's {@link #onUserLeaveHint} callback will
* be accompanied by calls to {@link #onUserInteraction}. This
* ensures that your activity will be told of relevant user activity such
* as pulling down the notification pane and touching an item there.
*
* <p>Note that this callback will be invoked for the touch down action
* that begins a touch gesture, but may not be invoked for the touch-moved
* and touch-up actions that follow.
*
* @see #onUserLeaveHint()
*/
public void onUserInteraction() {
}
還好有注釋,不然都不知道這個(gè)方法是用來干嘛的了。當(dāng)此activity在棧頂時(shí),觸屏點(diǎn)擊按home,back,menu鍵等都會(huì)觸發(fā)此方法。下拉statubar、旋轉(zhuǎn)屏幕、鎖屏不會(huì)觸發(fā)此方法。所以它會(huì)用在屏保應(yīng)用上,因?yàn)楫?dāng)你觸屏機(jī)器 就會(huì)立馬觸發(fā)一個(gè)事件,而這個(gè)事件又不太明確是什么,正好屏保滿足此需求。
getWindow().superDispatchTouchEvent(ev)
這個(gè)方法是干嘛的呢?我們點(diǎn)進(jìn)superDispatchTouchEvent()看看:
/**
* Used by custom windows, such as Dialog, to pass the touch screen event
* further down the view hierarchy. Application developers should
* not need to implement or call this.
*
*/
public abstract boolean superDispatchTouchEvent(MotionEvent event);
意思大概是說,這個(gè)方法會(huì)在Dialog等界面中使用到,開發(fā)者不需要實(shí)現(xiàn)或者調(diào)用它。
納尼?這樣就把我們打發(fā)了?不帶這樣玩的啊...
我們注意到superDispatchTouchEvent()方法是getWindow()調(diào)用的,getWindow()方法返回的是一個(gè)Window對(duì)象。我們?cè)?code>Window類的說明中可以看到一下內(nèi)容:
/**
* Abstract base class for a top-level window look and behavior policy. An
* instance of this class should be used as the top-level view added to the
* window manager. It provides standard UI policies such as a background, title
* area, default key processing, etc.
*
* <p>The only existing implementation of this abstract class is
* android.view.PhoneWindow, which you should instantiate when needing a
* Window.
*/
相信大家的英語應(yīng)該比我好,我就不翻譯了,注意到這句話:The only existing implementation of this abstract class is android.view.PhoneWindow,這是說PhoneWindow是Window的唯一實(shí)現(xiàn)類。那么我們就可以在PhoneWindow類中看一下superDispatchTouchEvent()方法究竟干了什么。
在PhoneWindow類中,我們可以看到如下代碼:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
很簡(jiǎn)單,mDecor對(duì)象調(diào)用了superDispatchTouchEvent()方法。那么mDecor對(duì)象又是什么?
跳轉(zhuǎn)到mDecor的聲明,代碼如下:
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
發(fā)現(xiàn)原來mDecor是DecorView的實(shí)例。等等,注釋里面說,DecorView是視圖的頂層view?
再跳轉(zhuǎn)到DecorView類的定義處,發(fā)現(xiàn)這么一行代碼:
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
......
}
現(xiàn)在我們清楚了,DecorView繼承自FrameLayout,是我們編寫所有的界面代碼的父類。
然后我們看看mDecor.superDispatchTouchEvent()這個(gè)方法干了什么,也就是在DecorView類中,superDispatchTouchEvent()方法的內(nèi)容:
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
嗯? 看不懂?注意,剛才我們已經(jīng)知道了,DecorView是繼承自FrameLayout,那么它的父類就應(yīng)該是ViewGroup了,而super.dispatchTouchEvent(event)方法,其實(shí)就應(yīng)該是ViewGroup的dispatchTouchEvent()方法,不信?你可以點(diǎn)進(jìn)去看看。
現(xiàn)在回到最開始的地方:在Activity 中:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
其中getWindow().superDispatchTouchEvent(ev)的意義大概就清楚了,就是說,如果視圖頂層的ViewGroup-DecorView類-的dispatchTouchEvnent()方法返回true的話,就不會(huì)執(zhí)行onTouchEvent()方法了。
那么,問題來了,ViewGroup中的dispatchTouchEvent()方法什么時(shí)候返回true,什么時(shí)候返回false呢?
請(qǐng)看下回分解。