事件的分發(fā)原理圖:
- 對(duì)于一個(gè)root viewgroup來說,如果接受了一個(gè)點(diǎn)擊事件,那么首先會(huì)調(diào)用他dispatchTouchEvent方法.
- viewgroup的onInterceptTouchEvent 返回true,那就代表要攔截這個(gè)事件.接下來這個(gè)事件就給viewgroup自己處理了,從而viewgroup的onTouchEvent方法就會(huì)被調(diào)用.
- viewgroup的onInterceptTouchEvent返回false就代表我不攔截這個(gè)事件,然后就把這個(gè)事件傳遞給自己的子元素,然后子元素的dispatchTouchEvent就會(huì)被調(diào)用,就是這樣一個(gè)循環(huán)直到 事件被處理.
我們可以看下事件分發(fā)的原理圖.
圖1:
簡(jiǎn)單的說只要各事件不消費(fèi),返回false,分發(fā)就會(huì)一直走下去:
dispatchTouchEvent(false) -> onInterceptTouchEvent(false) -> onTouchEvent(false) - 事件結(jié)束
重要的事情說一遍:
也就是說在任何View或者ViewGrop中只要它想消費(fèi)Touch事件,就直接onInterceptTouchEvent(true),這樣它就不會(huì)把事件傳下去給孩子view了,而是自己消費(fèi).
知其人先知其心,我們繼續(xù)進(jìn)一步了解下其他事件分發(fā)的api.
- dispatchTouchEvent 分發(fā)事件
return false; //不是目標(biāo)對(duì)象,則分發(fā),默認(rèn)false;
return true; // 是目標(biāo)view,則不分發(fā);
dispatchTouchEvent作用是將touch事件向下傳遞直到遇到被觸發(fā)的目標(biāo)view.
我們可以通過返回的boolean對(duì)touch事件的分發(fā)進(jìn)行處理,是否要向下分發(fā)尋找目標(biāo)view,當(dāng)然這個(gè)方法也可以被重載,手動(dòng)分配事件.
- onInterceptTouchEvent 攔截事件
return false; //表示不攔截,默認(rèn)false;
return true; // 表示攔截;
攔截是相當(dāng)于它的孩子,也就是說不會(huì)攔截自己.
如果攔截,則TouchEvent會(huì)傳給他自己,而它孩子是接收不了.
如果不攔截會(huì)繼續(xù)往他的孩子遞歸是否onInterceptTouchEvent需要攔截.
- onTouchEvent 觸摸事件
return false; //表示不消費(fèi),默認(rèn)false;
return true; // 表示消費(fèi);
當(dāng)onInterceptTouchEvent 確認(rèn)攔截,會(huì)問自己是否要消費(fèi)
TouchEvent,如果攔截了又不消費(fèi)則,那么Touch結(jié)束.
- invalidate 重新繪制
讓整個(gè)view失效,這樣view會(huì)被重新調(diào)用, 配合onDraw()使用.
下面是調(diào)用流程:
- 當(dāng)invalidate時(shí)會(huì)重新調(diào)用draw方法;
- draw會(huì)調(diào)用onDraw,而在draw內(nèi)還會(huì)調(diào)用computeScroll();
- 此時(shí)如果想讓computeScroll()循環(huán)被調(diào)用可以在computeScroll()內(nèi)自己調(diào)用postInvaildate()重新繪制;
invalidate刷新UI步驟: draw() -> onDraw() -> computeScroll()computeScroll() 源碼是空實(shí)現(xiàn),具體實(shí)現(xiàn)由自己來寫.
開發(fā)中事件分發(fā)的常見問題(重點(diǎn))
view的onTouchEvent,OnClickListerner和OnTouchListener的onTouch方法 三者優(yōu)先級(jí)如何?
答:
onTouchListener優(yōu)先級(jí)最高,也就是說如果onTouch方法返回 true ,那么事件結(jié)束,反之如果返回false,那么onTouchEvent講會(huì)被調(diào)用,至于OnClickListerner優(yōu)先級(jí)是最低的.
優(yōu)先級(jí)如下:
OnTouchListener > onTouchEvent > OnClickListerner點(diǎn)擊事件的傳遞順序如何?
答:Activity > Window > View.從上到下依次傳遞.
如果你最低的那個(gè)view
onTouchEvent返回false 那就說明他不想處理 那就再往下拋,都不處理的話最終就還是讓Activity自己處理了。舉個(gè)例子,pm下發(fā)一個(gè)任務(wù)給leader,leader自己不做 給架構(gòu)師a,小a也不做 給程序員b,b如果做了那就結(jié)束了這個(gè)任務(wù)。b如果發(fā)現(xiàn)自己搞不定,那就找a做,a要是也搞不定 就會(huì)不斷向上發(fā)起請(qǐng)求,最終可能還是pm做。
總結(jié)下流程: view的事件分發(fā)會(huì)從上往下,只要在子不消費(fèi)的情況,又會(huì)接著從下往上,最后結(jié)束.
//activity的dispatchTouchEvent 方法 一開始就是交給window去處理的
//win的superDispatchTouchEvent 返回true 那就直接結(jié)束了 這個(gè)函數(shù)了。返回false就意味
//這事件沒人處理,最終還是給activity的onTouchEvent 自己處理 這里的getwindow 其實(shí)就是phonewindow
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
//來看phonewindow的這個(gè)函數(shù) 直接把事件傳遞給了mDecor
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
//devorview就是 我們的rootview了 就是那個(gè)framelayout 我們的setContentView里面?zhèn)鬟f的那個(gè)layout
//就是這個(gè)decorview的 子view了
@Override
public final View getDecorView() {
if (mDecor == null) {
installDecor();
}
return mDecor;
}
- enable是否影響view的onTouchEvent返回值?
我們知道其實(shí)enable的優(yōu)先級(jí)高于cliable,當(dāng)enable=false時(shí)會(huì)屏蔽view的點(diǎn)擊事件.而事實(shí)上enable=false并不會(huì)影響onTouchEvent返回true.
答:
- 不影響,只要clickable和longClickable有一個(gè)為真,那么onTouchEvent就返回true。
- 設(shè)置了enable為false的話,onClick事件是完全屏蔽的,而clickable屬性就要看設(shè)置屬性和設(shè)置OnClicListener的先后順序了.
我們可以看下面demo
xml代碼:
android:clickable="true"
android:enabled="false"
android代碼:
//我在XML布局中設(shè)置了enabled="false",雖然是屏蔽了點(diǎn)擊事件,但是在自定義Button中,實(shí)現(xiàn)的`onTouchEvent`方法還是會(huì)返回true.
public class MyButton extends Button {
public MyButton(Context context) {
super(context);
}
public MyButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
System.out.println("onTouchEvent:"+super.onTouchEvent(event));
return super.onTouchEvent(event);
}
}
輸出結(jié)果:
onTouchEvent:true
- 滑動(dòng)沖突問題如何解決思路是什么?
答: - 讓誰消費(fèi)滑動(dòng):
要解決滑動(dòng)沖突 其實(shí)最主要的就是有一個(gè)核心思想。你到底想在一個(gè)事件序列中,讓哪個(gè)view 來響應(yīng)你的滑動(dòng)?比如 從上到下滑,是哪個(gè)view來處理這個(gè)事件,從左到右呢? - 攔截內(nèi)外滑動(dòng):
用業(yè)務(wù)需求來想明白以后剩下的其實(shí)就很好做了。
核心的方法就2個(gè):- 外部攔截
也就是父攔截.(重寫父控件的onInterceptTouchEvent即可). - 內(nèi)部攔截
也就是子view攔截方法.
- 外部攔截
學(xué)會(huì)這2種,基本上所有的滑動(dòng)沖突.都是這2種的變種,而且核心代碼思想都一樣,下面是兩種情況是示例代碼:
- 外部攔截法:思路就是重寫父控件的onInterceptTouchEvent即可。子元素一般不需要管。可以很容易理解,因?yàn)檫@和android自身的事件處理機(jī)制 邏輯是一模一樣的.
父控件示例代碼:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
//down事件肯定不能攔截 攔截了后面的就收不到了
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if (你的業(yè)務(wù)需求) {
//如果確定攔截了 就去自己的onTouchEvent里 處理攔截之后的操作和效果 即可了
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
//up事件 我們一般都是返回false的 一般父控件都不會(huì)攔截他。 因?yàn)閡p是事件的最后一步。這里返回true也沒啥意義
//唯一的意義就是因?yàn)?父元素 up被攔截。導(dǎo)致子元素 收不到up事件,那子元素 就肯定沒有onClick事件觸發(fā)了,這里的
//小細(xì)節(jié) 要想明白
intercepted = false;
break;
default:
break;
}
return intercepted;
}
- 內(nèi)部攔截法:內(nèi)部攔截法稍微復(fù)雜一點(diǎn),就是事件到來的時(shí)候,父控件不管,讓子元素自己來決定是否處理。如果消耗了就最好,沒消耗自然就轉(zhuǎn)給父控件處理了。
子控件示例代碼:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);//子元素自己消費(fèi),父控件不進(jìn)行攔截
break;
case MotionEvent.ACTION_MOVE:
if (如果父控件需要這個(gè)點(diǎn)擊事件) {
getParent().requestDisallowInterceptTouchEvent(false);
}//否則的話 就交給自己本身view的onTouchEvent自動(dòng)處理了
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
return super.dispatchTouchEvent(event);
}
PS: 父控件代碼也要修改一下,其實(shí)就是保證父控件別攔截down:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
return false;
}
return true;
}
事件分發(fā)的項(xiàng)目
需求:仿QQ側(cè)滑菜單
- google給我們提供了
DrawerLayout控件來作為側(cè)滑菜單控件,但是側(cè)滑菜單一般都是覆蓋到主頁面頂部的. - 我們需求在向右拖拉滑動(dòng)時(shí)可以側(cè)滑出菜單,并且要求菜單不可覆蓋到主頁面上,需求主頁面跟著側(cè)滑菜單的滑動(dòng)和位移.
圖2:
上面紅色框區(qū)域的結(jié)構(gòu)可以這樣設(shè)計(jì)ScrollView+多TextView.使用scrollView的原因是當(dāng)菜單的item增加時(shí)可以滾動(dòng),當(dāng)然listview,rv也可以做到.
問題分析:
- 當(dāng)點(diǎn)擊它任意一個(gè)孩子(TextView)時(shí),如果ScrollView不進(jìn)行onInterceptTouchEvent ,則它就不可以在菜單上進(jìn)行左右滑動(dòng).
- 但是如果攔截了全部,則它的孩子又會(huì)消費(fèi)不了TouchEvent.
問題解決:
- 只有左右移動(dòng)的時(shí)候進(jìn)行攔截,這樣父控件就擁有了TouchEvent,可在菜單上繼續(xù)左右滑動(dòng).
- 上下移動(dòng)或靜止的時(shí)候就不攔截,這樣孩子又有了TouchEvent,那么孩子就可以點(diǎn)擊了.
實(shí)例代碼:
/**
* 當(dāng)滑動(dòng)的時(shí)候,需要攔截TouchEvent時(shí)間,讓scrollView消化,否則會(huì)分發(fā)到孩子去;
* 當(dāng)不滑動(dòng)的停止的時(shí)候,不攔截,則會(huì)分發(fā)到孩子去,也就是TexView;
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
// 只有水平滑動(dòng)時(shí)才攔截touch
case MotionEvent.ACTION_DOWN:
startX = (int) (ev.getRawX() + 0.5f);
startY = (int) (ev.getRawY() + 0.5f);
break;
case MotionEvent.ACTION_MOVE:
int newX = (int) (ev.getRawX() + 0.5f);
int newY = (int) (ev.getRawY() + 0.5f);
int dx = Math.abs(startX - newX);
int dy = Math.abs(startY - newY);
if (dx > dy) {
// 水平滑動(dòng),只有水平滑動(dòng)才會(huì)攔截事件
return true;
}
startX = (int) ev.getRawX();// 初始化當(dāng)前位置
case MotionEvent.ACTION_UP:
break;
}
return super.onInterceptTouchEvent(ev);
}