關(guān)于android中的事件分發(fā) 可以查看這篇文章:
http://www.reibang.com/p/38015afcdb58
在文章中 用一張圖片總結(jié)了android事件分發(fā)的主要過程:
從這張圖上 我們可以說已經(jīng)把android的事件分發(fā)過程看了個大概 但是實際上 上述的過程只是android事件分發(fā)的一個部分 在實際的事件處理中 已經(jīng)遇見更復(fù)雜的情況 那時 事件不一定按這張圖的過程走 也就是我們需要在事件分發(fā)過程中注意的“細(xì)節(jié)”(在上面提到的文章中也說起過這部分的事 也就是所謂的“事件后續(xù)” 這里我們會詳細(xì)的解釋下這部分的知識)
先設(shè)置以下代碼:
1.MyGroupView 我們的 外層-父布局
public class MyGroupView extends ViewGroup {
public MyGroupView(Context context) {
super(context);
}
public MyGroupView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyGroupView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
getChildAt(0).layout(l, t, r, b);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.i("測試用", "MyGroupView" + ";" + "dispatchTouchEvent | action:" + ev.getAction());
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.i("測試用", "MyGroupView" + ";" + "onInterceptTouchEvent | action:" + ev.getAction());
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("測試用", "MyGroupView" + ";" + "onTouchEvent | action:" + event.getAction());
return false;
}
}
2.MyView 我們的子view
public class MyView extends View {
public MyView(Context context) {
super(context);
}
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i("測試用", "MyView" + ";" + "dispatchTouchEvent | action:" + event.getAction());
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("測試用", "MyView" + ";" + "onTouchEvent | action:" + event.getAction());
return false;
}
}
3.在activity的xml文件中
<com.example.godru.demozhurui.MyGroupView
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.godru.demozhurui.MyView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.example.godru.demozhurui.MyGroupView>
現(xiàn)在 我們有了用于測試的代碼 現(xiàn)在來談?wù)勱P(guān)于事件的細(xì)節(jié):
ps:在android中 事件以int類型表示 0-DOWN 1-UP 2-MOVE 3-CANCEL 具體可以點擊進android的MotionEvent類中詳細(xì)查看
1.只要View在onTouch中返回false 就會將事件向上傳遞到ViewGroup嗎扬蕊?
從上述的事件分發(fā)圖中好像是這么一回事 現(xiàn)在我們用上文代碼測試一下
先假設(shè)事情會按圖上走 那么我們想看到的log應(yīng)該是:
MyGroupView dispatchTouchEvent DOWN
MyGroupView onInterceptTouchEvent DOWN
MyView dispatchTouchEvent DOWN
MyView onTouchEvent DOWN
MyGroupView onTouchEvent DOWN
MyGroupView dispatchTouchEvent MOVE
MyGroupView onInterceptTouchEvent MOVE
MyView dispatchTouchEvent MOVE
MyView onTouchEvent MOVE
MyGroupView onTouchEvent MOVE
....
現(xiàn)在看看實際結(jié)果:
我在測試中實際動作并不只有點擊 而是有劃動的 但是圖上的結(jié)果卻很奇怪 不但是沒有move事件 連up事件都沒有出現(xiàn) 這是為什么圆丹?
這就是Android事件分發(fā)中關(guān)于事件的一個細(xì)節(jié):android對手勢事件是如何定性的谤专?
Android對事件的定義
在android中 一個完整的事件是由DOWN事件“開頭” 以UP或CANCEL(這個事件后面會說到)“結(jié)束” 這整個過程被認(rèn)為是一個“完整”的事件
而重點就在于這個“開頭” android的事件不是時時刻刻都在分發(fā)中 為了性能 為了不必要的浪費
---只有那些”表示“需要這個事件的控件才會被分發(fā)后續(xù)的事件---
而這個”需要“就是指在onTouchEvent 或者 dispatchTouchEvent中對DOWN事件返回ture(只有對DOWN返回true才有這個功能 因為DOWN事件是一切事件的開始 其它事件 如:MOVE 還是會按照流程圖的路線行走) 也就是向android表示本控件需要本次事件以及后續(xù)的其它事件 這樣才會接收到后續(xù)MOVE和UP
如果在一個事件走完了整個流程 還是沒有任何控件”消費“DOWN事件 android就認(rèn)為這個事件沒有任何控件想要 事件也就被取消了
2.ViewGroup在dispatchTouchEvent中返回默認(rèn)值 且在onInterceptTouchEvent 返回false時 一定會將事件傳遞到View嗎华望?
我們將上代碼中的ViewGroup改為:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.i("測試用", "MyGroupView" + ";" + "dispatchTouchEvent | action:" + ev.getAction());
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.i("測試用", "MyGroupView" + ";" + "onInterceptTouchEvent | action:" + ev.getAction());
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("測試用", "MyGroupView" + ";" + "onTouchEvent | action:" + event.getAction());
return true;
}
View的代碼改為:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i("測試用", "MyView" + ";" + "dispatchTouchEvent | action:" + event.getAction());
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("測試用", "MyView" + ";" + "onTouchEvent | action:" + event.getAction());
return false;
}
這次有控件在onTouchEvent中返回了true 所以事件應(yīng)該會有后續(xù)傳遞來 我們理想中的情況是:
MyGroupView dispatchTouchEvent DOWN
MyGroupView onInterceptTouchEvent DOWN
MyView dispatchTouchEvent DOWN
MyView onTouchEvent DOWN
MyGroupView onTouchEvent DOWN
MyGroupView dispatchTouchEvent MOVE
MyGroupView onInterceptTouchEvent MOVE
MyView dispatchTouchEvent MOVE
MyView onTouchEvent MOVE
MyGroupView onTouchEvent MOVE
....
從上圖可以看出 事件并非是什么時候到在向下傳遞的 這也和上文中說到的”事件需求者“有關(guān)
Android中事件傳遞的意義
我們發(fā)現(xiàn)事件并不總是在向下傳遞的 這是因為android向下傳遞后又向上返回的這整個過程的本質(zhì)就是在尋求”需求者“ 需要事件的控件返回 要不在dispatchTouchEvent中返回true 要不在onTouchEvent中返回true 以確定是否由自己來處理這個事件
而在android中 它們的設(shè)計是一個事件面向一個對象 不會出現(xiàn)一個事件由多個控件來處理的情況(你可以通過代碼來實現(xiàn) 但是事件分發(fā)機制本身只會以一個控件為目標(biāo)來傳遞事件)所以在已經(jīng)明確了處理者后 再繼續(xù)向下傳遞事件是沒有意義的
---誰需求 誰處理 即使還存在子控件---
也就是說 android不但只在確定有人”需求“事件后才傳遞后續(xù)事件 且控件本身在明確了由自己處理后也只接收后續(xù)事件了 不再繼續(xù)傳遞給自己的子控件
3.ViewGroup是否一定可以通過onInterceptTouchEvent返回true的方式來獲取事件?
從上文中我們已經(jīng)知道了事件分發(fā)的流程 并且應(yīng)該也可以發(fā)現(xiàn) 這個流程中 其實子view是非常被動的 因為無論DOWN事件是否是被子view消費 ViewGroup都可以用onInterceptTouchEvent來攔截事件 因為事件一定會經(jīng)過它 那是否意味著 我們可以無限制的使用onInterpecTouchEventt來獲取事件呢厂镇? 答案是 不是的 因為ViewGroup有一個公共方法:
requestDisallowInterceptTouchEvent(true);
這個方法的描述是鎖定ViewGroup的攔截方法 事實上就是確保事件一定不會被攔截 和一定會被攔截
我們修改下代碼來測試一下
MyGroupView:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.i("測試用", "MyGroupView" + ";" + "dispatchTouchEvent | action:" + ev.getAction());
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.i("測試用", "MyGroupView" + ";" + "onInterceptTouchEvent | action:" + ev.getAction());
return ev.getAction() != MotionEvent.ACTION_DOWN;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("測試用", "MyGroupView" + ";" + "onTouchEvent | action:" + event.getAction());
return true;
}
MyView:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i("測試用", "MyView" + ";" + "dispatchTouchEvent | action:" + event.getAction());
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("測試用", "MyView" + ";" + "onTouchEvent | action:" + event.getAction());
if (event.getAction() == MotionEvent.ACTION_DOWN)
getParent().requestDisallowInterceptTouchEvent(true);
return true;
}
這次我們在攔截方法中除了DOWN事件以外都返回true 在View里 我們在接收到DOWN事件后 對父布局使用一次requestDisallowInterceptTouchEvent() 鎖定攔截方法
一般理想過程應(yīng)該是:
MyGroupView dispatchTouchEvent DOWN
MyGroupView onInterceptTouchEvent DOWN
MyView dispatchTouchEvent DOWN
MyView onTouchEvent DOWN
MyGroupView dispatchTouchEvent MOVE
MyGroupView onInterceptTouchEvent MOVE
MyGroupView onTouchEvent MOVE
MyGroupView dispatchTouchEvent MOVE
MyGroupView onTouchEvent MOVE
MyGroupView dispatchTouchEvent MOVE
MyGroupView onTouchEvent MOVE
....
實際運行情況:
如上 攔截方法甚至都沒有走到過 因為requestDisallowInterceptTouchEvent方法會影響ViewGroup的鎖定判斷 這個判斷的益在onInterceptTouchEvent方法調(diào)用前 所以鎖定開啟后 甚至都不會走這個方法
這個方法的作用是避免本View獲取的事件被其它View攔截
本方法的存在也可以解釋為什么有的view的事件無法被攔截的問題
但是這個方法說到底還是子view對父view的調(diào)用 所以如果DOWN事件都被直接攔截 這個方法也就沒有任何作用了
4.CANCEL事件的意義 以及 使用了攔截方法就會直接調(diào)用其onTouchEvent方法嗎
CANCEL事件在有的文章中介紹是手指滑動到超過手機屏幕時出現(xiàn) 其實這個說法是錯誤的 因為實際在這種情況下 出現(xiàn)的仍然是UP事件
要再現(xiàn)CANCEL事件可以直播修改代碼:
MyGroupView:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.i("測試用", "MyGroupView" + ";" + "dispatchTouchEvent | action:" + ev.getAction());
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.i("測試用", "MyGroupView" + ";" + "onInterceptTouchEvent | action:" + ev.getAction());
return ev.getAction() != MotionEvent.ACTION_DOWN;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("測試用", "MyGroupView" + ";" + "onTouchEvent | action:" + event.getAction());
return true;
}
MyView:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i("測試用", "MyView" + ";" + "dispatchTouchEvent | action:" + event.getAction());
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("測試用", "MyView" + ";" + "onTouchEvent | action:" + event.getAction());
return true;
}
運行效果:
從圖中 我們就可以解答一開始的兩個問題
CANCEL事件其實是在事件被view攔截時 由被攔截的事件轉(zhuǎn)化而來 是一個代碼性質(zhì)上的事件
攔截方法在攔截后并不是會直接調(diào)用onTouchEvent方法 而是向下傳遞了一個CANCEL事件
這是因為android中事件本身是有目標(biāo)的(也就是需求者)在事件傳遞的過程中 決定是否向下和向上傳遞的實際是view自己
如果一個事件沒有傳遞到目標(biāo)view 而是被攔截了 android不會讓目標(biāo)view一無所知 而是向他傳遞一個CANCEL事件 表示你已經(jīng)不是需求者了 之后才會繼續(xù)向新的需求者傳遞事件
這本質(zhì)上也符合之前所說的事件-控件一一對應(yīng)的原則
總結(jié)
1.文章最上方的事件傳遞圖只對事件的開始-DOWN事件完全適用 后續(xù)事件的情況與其不一樣
2.android中事件的開始是DOWN事件 只有在有控件確定自己為需求者(消費了DOWN事件)的情況下 后續(xù)事件才會出現(xiàn) 不然就沒有后續(xù)事件
對DOWN事件的消費有兩種:
在dispatchTouchEvent里返回true or 在onTouchEvent里返回true
3.除DOWN事件外 后續(xù)事件傳遞的對象都是消費了DOWN事件的控件 傳遞到這個控件后 無論其是否在onTouchEvent或dispatchTouchEvent里消費與否 都會繼續(xù)向這個控件傳遞后續(xù)事件(直到UP事件或CANCEL事件出現(xiàn) 意味著本次事件的結(jié)束)
4.在已經(jīng)確定了“需求者”的情況下控件不再會因為像文章頂部的圖中一樣 在onTouchEvent或dispatchTouchEvent里返回了false 而將事件向上返回 因為已經(jīng)確定了需求者
同理 在確定了“需求者”的情況下 事件也不會因為控件在dispatchTouchEvent里返回默認(rèn)值 和在攔截方法onInterceptTouchEvent里返回false 而把事件向下傳遞
5.攔截事件onInterceptTouchEvent可以讓ViewGroup強行確定自己“需求者”的地位 這個方法在返回了一次true后 就會讓后續(xù)事件向它傳遞 之后不會再調(diào)用 而在攔截開始時 被攔截的事件并不是直接就返回到ViewGroup的onTouchEvent方法中 而是將這個事件轉(zhuǎn)化為一個CANCEL事件 向下傳遞到了之前的“需求者”控件 之后的事件才會傳遞到ViewGroup
6.可以對一個viewGroup調(diào)用requestDisallowInterceptTouchEvent來使其攔截方法失效或開啟 這個方法可以用于讓子view避免事件被父view攔截 設(shè)置為true會讓ViewGroup不經(jīng)過攔截方法 直接按false的路線傳遞事件