[聲明] 本文來(lái)自《顧浩鑫-Android高級(jí)進(jìn)階》第一章 的學(xué)習(xí)筆記氢哮,發(fā)文目的在于傳遞分享知識(shí)。如涉及文章內(nèi)容型檀、版權(quán)和其它問(wèn)題冗尤,請(qǐng)與本人聯(lián)系,我將在第一時(shí)間刪除內(nèi)容胀溺!
前言:在Android開(kāi)發(fā)中生闲,經(jīng)常會(huì)遇到觸摸事件沖突,比如ViewPager的輪播圖跟Fragment的劃動(dòng)事件沖突月幌,或者輪播圖跟下拉事件沖突碍讯,自定義view的事件處理等,本文章將會(huì)詳細(xì)介紹Activity扯躺、View捉兴、ViewGroup三者的觸摸事件傳遞機(jī)制,傳遞包括三個(gè)階段:分發(fā)录语、攔截倍啥、消費(fèi)。
一.觸摸事件的類(lèi)型
觸摸事件對(duì)應(yīng)的是 MotionEvent 類(lèi)澎埠,事件類(lèi)型主要有三種:
- ACTION_DOWN:用戶(hù)按下操作虽缕,表示一次觸摸事件的開(kāi)始。
- ACTION_MOVE:在按下的情況下蒲稳,進(jìn)行移動(dòng)氮趋。輕微的移動(dòng)都會(huì)傳遞到該事件伍派。
- ACTION_UP:用戶(hù)手指離開(kāi)屏幕,表示一次觸摸事件的
注 :如果用戶(hù)僅僅的是點(diǎn)擊而已剩胁,則只會(huì)執(zhí)行到 ACTION_DOWN 和 ACTION_UP 兩個(gè)事件诉植,不會(huì)執(zhí)行到 ACTION_MOVE 事件。所以 ACTION_DOWN 和 ACTION_UP 是事件是必須的昵观。
二.觸摸事件的傳遞階段
1.分發(fā)(Dispatch)
在Android系統(tǒng)中所有的觸摸事件都是由 dispatchTouchEvent 方法進(jìn)行分發(fā)的晾腔。該方法中判斷事件是被消費(fèi)(return true),還是繼續(xù)分發(fā)給子視圖處理(return super.dispatchTouchEvent)啊犬,如果當(dāng)前視圖是ViewGroup或者其子類(lèi)灼擂,則會(huì)調(diào)用onInterceptTouchEvent 判斷是否截?cái)r。
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
2.截?cái)r(Intercept)
事件的截?cái)r InterceptTouchEvent 只存在于ViewGroup及其子類(lèi)觉至,activity和View是不存在該方法剔应。該方法判斷事件是被截?cái)r (return true)并交給自身的 OnToucEvent 方法進(jìn)行消費(fèi),還是繼續(xù)傳遞給子視圖(return super.InterceptTouchEvent 或者 return false)康谆。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return super.onInterceptTouchEvent(ev);
}
3.消費(fèi)(Consume)
事件的消費(fèi)通過(guò) OnTouchEvent 方法判斷领斥,是被消費(fèi)(return true),還是不處理(return false)并將事件傳遞給父視圖的 OnTouchEvent 方法進(jìn)行處理嫉到。
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
所有擁有事件傳遞能力的類(lèi):
Activity: 擁有dispatchTouchEvent 沃暗、OnTouchEvent
ViewGroup: 擁有dispatchTouchEvent 、OnInterceptTouchEvent 何恶、OnTouchEvent
View:擁有dispatchTouchEvent 孽锥、OnTouchEvent
三、View的事件傳遞機(jī)制
3.1 dome
雖然說(shuō)ViewGroup是View的子類(lèi)细层,但是這是說(shuō)的View指的是除ViewGroup之外的View控件子類(lèi)惜辑,首先定義一個(gè)MyTextView繼承TextView,打印每次事件的觸發(fā)以變了解事件傳遞的流程疫赎。
MyTextView 類(lèi)
public class MyTextView extends TextView {
private String tag = "MyTextView";
public MyTextView(Context context) {
super(context);
}
public MyTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_UP:
Log.i(tag, "dispatchTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_MOVE:
Log.i(tag, "dispatchTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_DOWN:
Log.i(tag, "dispatchTouchEvent ACTION_DOWN");
break;
}
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_UP:
Log.i(tag, "onTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_MOVE:
Log.i(tag, "onTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_DOWN:
Log.i(tag, "onTouchEvent ACTION_DOWN");
break;
}
return super.onTouchEvent(event);
}
}
定義一個(gè)MainActivity來(lái)展現(xiàn)這個(gè)MyTextView盛撑,同時(shí)設(shè)置點(diǎn)擊(onClick)和觸摸(onTouch)監(jiān)聽(tīng)。
MainActivity 類(lèi)
public class MainActivity extends AppCompatActivity implements View.OnClickListener,View.OnTouchListener{
private MyTextView mMyTextView;
private String tag = "MainActiviy";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mMyTextView = findViewById(R.id.text_view);
// 點(diǎn)擊監(jiān)聽(tīng)
mMyTextView.setOnClickListener(this);
// 觸碰監(jiān)聽(tīng)
mMyTextView.setOnTouchListener(this);
}
// MyTextView 點(diǎn)擊事件
@Override
public void onClick(View view) {
switch (view.getId()){
case R.id.text_view:
Log.i(tag, "MyTextView onClick");
break;
}
}
// MyTextView 觸碰事件
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
switch (motionEvent.getAction()){
case MotionEvent.ACTION_UP:
Log.i(tag, "MyTextView onTouch ACTION_UP");
break;
case MotionEvent.ACTION_MOVE:
Log.i(tag, "MyTextView onTouch ACTION_MOVE");
break;
case MotionEvent.ACTION_DOWN:
Log.i(tag, "MyTextView onTouch ACTION_DOWN");
break;
}
return false;
}
// Activity 的事件分發(fā)
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_UP:
Log.i(tag, "dispatchTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_MOVE:
Log.i(tag, "dispatchTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_DOWN:
Log.i(tag, "dispatchTouchEvent ACTION_DOWN");
break;
}
return super.dispatchTouchEvent(ev);
}
// Activity 的事件消費(fèi)
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_UP:
Log.i(tag, "onTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_MOVE:
Log.i(tag, "onTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_DOWN:
Log.i(tag, "onTouchEvent ACTION_DOWN");
break;
}
return super.onTouchEvent(event);
}
}
3.2 打印日志
運(yùn)行后捧搞,點(diǎn)擊Text View反饋的打印日志
03-28 08:05:14.824 1219-1219/com.mvp.chenzhesheng.androidadvance I/MainActiviy: dispatchTouchEvent ACTION_DOWN
03-28 08:05:14.824 1219-1219/com.mvp.chenzhesheng.androidadvance I/MyTextView: dispatchTouchEvent ACTION_DOWN
03-28 08:05:14.824 1219-1219/com.mvp.chenzhesheng.androidadvance I/MainActiviy: MyTextView onTouch ACTION_DOWN
03-28 08:05:14.824 1219-1219/com.mvp.chenzhesheng.androidadvance I/MyTextView: onTouchEvent ACTION_DOWN
03-28 08:05:15.034 1219-1219/com.mvp.chenzhesheng.androidadvance I/MainActiviy: dispatchTouchEvent ACTION_UP
03-28 08:05:15.034 1219-1219/com.mvp.chenzhesheng.androidadvance I/MyTextView: dispatchTouchEvent ACTION_UP
03-28 08:05:15.034 1219-1219/com.mvp.chenzhesheng.androidadvance I/MainActiviy: MyTextView onTouch ACTION_UP
03-28 08:05:15.034 1219-1219/com.mvp.chenzhesheng.androidadvance I/MyTextView: onTouchEvent ACTION_UP
03-28 08:05:15.044 1219-1219/com.mvp.chenzhesheng.androidadvance I/MainActiviy: MyTextView onClick
dispatchTouchEvent 抵卫、 OnTouchEvent 這兩個(gè)方法的返回值存在三種情況:
- 直接返回true。
- 直接返回false胎撇。
- 返回父類(lèi)同名方法介粘,super.dispatchTouchEvent 或者 super.OnTouchEvent。
由于擁有不同的返回值晚树,所以事件傳遞流程也有不同姻采,經(jīng)過(guò)不斷修改返回值測(cè)試,最終得到了點(diǎn)擊事件的流程圖爵憎,ACTION_DOWN 和 ACTION_UP 事件的傳遞流程是相同的慨亲。
3.3 事件傳遞流程圖
從上面的流程圖可以得出結(jié)論:
- 觸摸事件是從 dispatchTouchEvent 開(kāi)始的婚瓜,默認(rèn)返回父類(lèi)同名方法 super ,事件將會(huì)依照嵌套層次從外向內(nèi)傳遞( MainActivity 到 MyTextView )巡雨,到達(dá)最內(nèi)層的 View 時(shí)闰渔,將由 View 的 OnTouchEvent 方法處理,該方法返回 true 時(shí)進(jìn)行消費(fèi)不再傳遞铐望,返回 false 時(shí)再由內(nèi)向外傳遞冈涧,由外層的 OnTouchEvent 處理。
- 如果外層向內(nèi)層傳遞過(guò)程中正蛙,人為干擾返回 true 消費(fèi)督弓,則不會(huì)繼續(xù)繼續(xù)像內(nèi)部傳遞。
- View 的事件控制順序先執(zhí)行 onTouch 再執(zhí)行 onClick 乒验,如果 onTouch 返回 true 消費(fèi)愚隧,則不會(huì)繼續(xù)傳遞,也不會(huì)執(zhí)行 onClick 方法锻全。
四狂塘、ViewGroup的事件傳遞機(jī)制
4.1 dome
ViewGroup 是 View 的控件容器存在,擁有 dispatchTouchEvent 鳄厌、 onInterceptTouchEvent 和 onTouchEvent 三個(gè)方法荞胡,比 View 多了一個(gè) onInterceptTouchEvent 方法。為了更好的觀察了嚎,我們需要自定義 MyRelativeLayout 繼承 RelativeLayout 泪漂。
MyRelativeLayout類(lèi)
public class MyRelativeLayout extends RelativeLayout {
private final static String tag = "MyRelativeLayout";
public MyRelativeLayout(Context context) {
super(context);
}
public MyRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_UP:
Log.i(tag, "dispatchTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_MOVE:
Log.i(tag, "dispatchTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_DOWN:
Log.i(tag, "dispatchTouchEvent ACTION_DOWN");
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_UP:
Log.i(tag, "onInterceptTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_MOVE:
Log.i(tag, "onInterceptTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_DOWN:
Log.i(tag, "onInterceptTouchEvent ACTION_DOWN");
break;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_UP:
Log.i(tag, "onTouchEvent ACTION_UP");
break;
case MotionEvent.ACTION_MOVE:
Log.i(tag, "onTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_DOWN:
Log.i(tag, "onTouchEvent ACTION_DOWN");
break;
}
return super.onTouchEvent(event);
}
}
main_activity.xml 文件
<?xml version="1.0" encoding="utf-8"?>
<com.mvp.chenzhesheng.androidadvance.MyRelativeLayout
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.mvp.chenzhesheng.androidadvance.MyTextView
android:id="@+id/text_view"
android:clickable="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
</com.mvp.chenzhesheng.androidadvance.MyRelativeLayout>
4.2 打印日志
04-02 08:47:57.980 1030-1030/com.mvp.chenzhesheng.androidadvance I/MainActiviy: dispatchTouchEvent ACTION_DOWN
04-02 08:47:58.000 1030-1030/com.mvp.chenzhesheng.androidadvance I/MyRelativeLayout: dispatchTouchEvent ACTION_DOWN
04-02 08:47:58.000 1030-1030/com.mvp.chenzhesheng.androidadvance I/MyRelativeLayout: onInterceptTouchEvent ACTION_DOWN
04-02 08:47:58.000 1030-1030/com.mvp.chenzhesheng.androidadvance I/MyTextView: dispatchTouchEvent ACTION_DOWN
04-02 08:47:58.010 1030-1030/com.mvp.chenzhesheng.androidadvance I/MainActiviy: MyTextView onTouch ACTION_DOWN
04-02 08:47:58.010 1030-1030/com.mvp.chenzhesheng.androidadvance I/MyTextView: onTouchEvent ACTION_DOWN
04-02 08:47:58.200 1030-1030/com.mvp.chenzhesheng.androidadvance I/MainActiviy: dispatchTouchEvent ACTION_UP
04-02 08:47:58.200 1030-1030/com.mvp.chenzhesheng.androidadvance I/MyRelativeLayout: dispatchTouchEvent ACTION_UP
04-02 08:47:58.200 1030-1030/com.mvp.chenzhesheng.androidadvance I/MyRelativeLayout: onInterceptTouchEvent ACTION_UP
04-02 08:47:58.200 1030-1030/com.mvp.chenzhesheng.androidadvance I/MyTextView: dispatchTouchEvent ACTION_UP
04-02 08:47:58.210 1030-1030/com.mvp.chenzhesheng.androidadvance I/MainActiviy: MyTextView onTouch ACTION_UP
04-02 08:47:58.210 1030-1030/com.mvp.chenzhesheng.androidadvance I/MyTextView: onTouchEvent ACTION_UP
04-02 08:47:58.260 1030-1030/com.mvp.chenzhesheng.androidadvance I/MainActiviy: MyTextView onClick
可以看到 MainActivity 和 MyTextView 的事件傳遞處理中添加了一層 MyRelativeLayout 。通過(guò)不同返回值測(cè)試歪泳,得到一套流程圖萝勤。
4.3 流程圖
從上面的流程圖可以得出結(jié)論:
- 觸摸事件傳遞是從 Activity 傳遞到 ViewGroup ,再傳遞到 View 。如果中間沒(méi)有 ViewGroup 則直接從 Activity 傳遞到 View 呐伞。
- ViewGroup 通過(guò) onInterceptTouchEvent 方法對(duì)事件進(jìn)行截?cái)r敌卓,如果返回 false 或者 super.onInterceptTouchEvent ,則事件會(huì)繼續(xù)傳遞給子 View 伶氢。
- 子 View 中對(duì)事件進(jìn)行消費(fèi)后趟径,ViewGroup 將不會(huì)接收到任何事件。
五.總結(jié)
- 事件分發(fā)是由外到內(nèi)鞍历,從 Activity 到具體的子 View 舵抹;
- 事件處理消費(fèi)是由內(nèi)到外,從子 View 到最外層 Activity 劣砍;
- 事件攔截只存在于 ViewGroup 中惧蛹;
- 掌握事件傳遞機(jī)制可以更好的進(jìn)行事件處理,無(wú)論是自定義 View 還是閱讀 Framework 層源碼都需要對(duì)事件傳遞進(jìn)行學(xué)習(xí),才能更精致的開(kāi)發(fā)應(yīng)用香嗓。