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