原帖位址:http://blog.csdn.net/xiaanming/article/details/21696315
viewgroup的事件分發機制
我們用手指去觸摸android手機螢幕,就會産生一個觸摸事件,但是這個觸摸事件在底層是怎麼分發的呢?這個我還真不知道,這裡涉及到操作硬體(手機螢幕)方面的知識,也就是linux核心方面的知識,我也沒有了解過這方面的東西,是以我們可能就往上層來分析分析,我們知道android中負責與使用者互動,與使用者操作緊密相關的四大元件之一是activity, 是以我們有理由相信activity中存在分發事件的方法,這個方法就是dispatchtouchevent(),我們先看其源碼吧
public boolean dispatchtouchevent(motionevent ev) {
//如果是按下狀态就調用onuserinteraction()方法,onuserinteraction()方法
//是個空的方法, 我們直接跳過這裡看下面的實作
if (ev.getaction() == motionevent.action_down) {
onuserinteraction();
}
if (getwindow().superdispatchtouchevent(ev)) {
return true;
//getwindow().superdispatchtouchevent(ev)傳回false,這個事件就交給activity
//來處理, activity的ontouchevent()方法直接傳回了false
return ontouchevent(ev);
}
這個方法中我們還是比較關心getwindow()的superdispatchtouchevent()方法,getwindow()傳回目前activity的頂層視窗window對象,我們直接看window api的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);
這個是個抽象方法,是以我們直接找到其子類來看看superdispatchtouchevent()方法的具體邏輯實作,window的唯一子類是phonewindow,我們就看看phonewindow的superdispatchtouchevent()方法
public boolean superdispatchtouchevent(keyevent event) {
return mdecor.superdispatctouchevent(event);
裡面直接調用decorview類的superdispatchtouchevent()方法,或許很多人不了解decorview這個類,decorview是phonewindow的一個final的内部類并且繼承framelayout的,也是window界面的最頂層的view對象,這是什麼意思呢?别着急,我們接着往下看
我們先建立一個項目,取名androidtouchevent,然後直接用模拟器運作項目, mainactivity的布局檔案為
<relativelayout 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"
tools:context=".mainactivity" >
<textview
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerhorizontal="true"
android:layout_centervertical="true"
android:text="@string/hello_world" />
</relativelayout>
利用hierarchyviewer工具來檢視下mainactivity的view的層次結構,如下圖
我們看到最頂層就是phonewindow$decorview,接着decorview下面有一個linearlayout, linearlayout下面有兩個framelayout
上面那個framelayout是用來顯示标題欄的,這個demo中是一個textview,當然我們還可以定制我們的标題欄,利用getwindow().setfeatureint(window.feature_custom_title,r.layout.xxx); xxx就是我們自定義标題欄的布局xml檔案
下面的framelayout是用來裝載contentview的,也就是我們在activity中利用setcontentview()方法設定的view,現在我們知道了,原來我們利用setcontentview()設定activity的view的外面還嵌套了這麼多的東西
我們來理清下思路,activity的最頂層窗體是phonewindow,而phonewindow的最頂層view是decorview,接下來我們就看decorview類的superdispatchtouchevent()方法
public boolean superdispatchtouchevent(motionevent event) {
return super.dispatchtouchevent(event);
在裡面調用了父類framelayout的dispatchtouchevent()方法,而framelayout中并沒有dispatchtouchevent()方法,是以我們直接看viewgroup的dispatchtouchevent()方法
* {@inheritdoc}
*/
@override
public boolean dispatchtouchevent(motionevent ev) {
final int action = ev.getaction();
final float xf = ev.getx();
final float yf = ev.gety();
final float scrolledxfloat = xf + mscrollx;
final float scrolledyfloat = yf + mscrolly;
final rect frame = mtemprect;
//這個值預設是false, 然後我們可以通過requestdisallowintercepttouchevent(boolean disallowintercept)方法
//來改變disallowintercept的值
boolean disallowintercept = (mgroupflags & flag_disallow_intercept) != 0;
//這裡是action_down的處理邏輯
if (action == motionevent.action_down) {
//清除mmotiontarget, 每次action_down都很設定mmotiontarget為null
if (mmotiontarget != null) {
mmotiontarget = null;
}
//disallowintercept預設是false, 就看viewgroup的onintercepttouchevent()方法
if (disallowintercept || !onintercepttouchevent(ev)) {
ev.setaction(motionevent.action_down);
final int scrolledxint = (int) scrolledxfloat;
final int scrolledyint = (int) scrolledyfloat;
final view[] children = mchildren;
final int count = mchildrencount;
//周遊其子view
for (int i = count - 1; i >= 0; i--) {
final view child = children[i];
//如果該子view是visible或者該子view正在執行動畫, 表示該view才
//可以接受到touch事件
if ((child.mviewflags & visibility_mask) == visible
|| child.getanimation() != null) {
//擷取子view的位置範圍
child.gethitrect(frame);
//如touch到螢幕上的點在該子view上面
if (frame.contains(scrolledxint, scrolledyint)) {
// offset the event to the view's coordinate system
final float xc = scrolledxfloat - child.mleft;
final float yc = scrolledyfloat - child.mtop;
ev.setlocation(xc, yc);
child.mprivateflags &= ~cancel_next_up_event;
//調用該子view的dispatchtouchevent()方法
if (child.dispatchtouchevent(ev)) {
// 如果child.dispatchtouchevent(ev)傳回true表示
//該事件被消費了,設定mmotiontarget為該子view
mmotiontarget = child;
//直接傳回true
return true;
}
// the event didn't get handled, try the next view.
// don't reset the event's location, it's not
// necessary here.
}
}
}
}
//判斷是否為action_up或者action_cancel
boolean isuporcancel = (action == motionevent.action_up) ||
(action == motionevent.action_cancel);
if (isuporcancel) {
//如果是action_up或者action_cancel, 将disallowintercept設定為預設的false
//假如我們調用了requestdisallowintercepttouchevent()方法來設定disallowintercept為true
//當我們擡起手指或者取消touch事件的時候要将disallowintercept重置為false
//是以說上面的disallowintercept預設在我們每次action_down的時候都是false
mgroupflags &= ~flag_disallow_intercept;
// the event wasn't an action_down, dispatch it to our target if
// we have one.
final view target = mmotiontarget;
//mmotiontarget為null意味着沒有找到消費touch事件的view, 是以我們需要調用viewgroup父類的
//dispatchtouchevent()方法,也就是view的dispatchtouchevent()方法
if (target == null) {
// we don't have a target, this means we're handling the
// event as a regular view.
ev.setlocation(xf, yf);
if ((mprivateflags & cancel_next_up_event) != 0) {
ev.setaction(motionevent.action_cancel);
mprivateflags &= ~cancel_next_up_event;
return super.dispatchtouchevent(ev);
//這個if裡面的代碼action_down不會執行,隻有action_move
//action_up才會走到這裡, 假如在action_move或者action_up攔截的
//touch事件, 将action_cancel派發給target,然後直接傳回true
//表示消費了此touch事件
if (!disallowintercept && onintercepttouchevent(ev)) {
final float xc = scrolledxfloat - (float) target.mleft;
final float yc = scrolledyfloat - (float) target.mtop;
mprivateflags &= ~cancel_next_up_event;
ev.setaction(motionevent.action_cancel);
ev.setlocation(xc, yc);
if (!target.dispatchtouchevent(ev)) {
// clear the target
mmotiontarget = null;
// don't dispatch this event to our own view, because we already
// saw it when intercepting; we just want to give the following
// event to the normal ontouchevent().
return true;
// finally offset the event to the target's coordinate system and
// dispatch the event.
final float xc = scrolledxfloat - (float) target.mleft;
final float yc = scrolledyfloat - (float) target.mtop;
ev.setlocation(xc, yc);
if ((target.mprivateflags & cancel_next_up_event) != 0) {
target.mprivateflags &= ~cancel_next_up_event;
//如果沒有攔截action_move, action_down的話,直接将touch事件派發給target
return target.dispatchtouchevent(ev);
}
這個方法相對來說還是蠻長,不過所有的邏輯都寫在一起,看起來比較友善,接下來我們就具體來分析一下
我們點選螢幕上面的textview來看看touch是如何分發的,先看看action_down
在decorview這一層會直接調用viewgroup的dispatchtouchevent(), 先看18行,每次action_down都會将mmotiontarget設定為null, mmotiontarget是什麼?我們先不管,繼續看代碼,走到25行, disallowintercept預設為false,我們再看viewgroup的onintercepttouchevent()方法
public boolean onintercepttouchevent(motionevent ev) {
return false;
}
直接傳回false, 繼續往下看,循環周遊decorview裡面的child,從上面的mainactivity的層次結構圖我們可以看出,decorview裡面隻有一個child那就是linearlayout, 第43行判斷touch的位置在不在linnearlayout上面,這是毫無疑問的,是以直接跳到51行, 調用linearlayout的dispatchtouchevent()方法,linearlayout也沒有dispatchtouchevent()這個方法,是以也是調用viewgroup的dispatchtouchevent()方法,是以這個方法卡在51行沒有繼續下去,而是去先執行linearlayout的dispatchtouchevent()
linearlayout調用dispatchtouchevent()的邏輯跟decorview是一樣的,是以也是周遊linearlayout的兩個framelayout,判斷touch的是哪個framelayout,很明顯是下面那個,調用下面那個framelayout的dispatchtouchevent(), 是以linearlayout的dispatchtouchevent()卡在51也沒繼續下去
繼續調用framelayout的dispatchtouchevent()方法,和上面一樣的邏輯,下面的framelayout也隻有一個child,就是relativelayout,framelayout的dispatchtouchevent()繼續卡在51行,先執行relativelayout的dispatchtouchevent()方法
執行relativelayout的dispatchtouchevent()方法邏輯還是一樣的,循環周遊 relativelayout裡面的孩子,裡面隻有一個textview, 是以這裡就調用textview的dispatchtouchevent(), textview并沒有dispatchtouchevent()這個方法,于是找textview的父類view,在看view的dispatchtouchevent()的方法之前,我們先理清下上面這些viewgroup執行dispatchtouchevent()的思路,我畫了一張圖幫大家理清下(這裡沒有畫出onintercepttouchevent()方法)
上面的viewgroup的touch事件分發就告一段落先,因為這裡要調用textview(也就是view)的dispatchtouchevent()方法,是以我們先分析view的dispatchtouchevent()方法在将上面的繼續下去
view的touch事件分發機制
我們還是先看view的dispatchtouchevent()方法的源碼
public boolean dispatchtouchevent(motionevent event) {
if (montouchlistener != null && (mviewflags & enabled_mask) == enabled &&
montouchlistener.ontouch(this, event)) {
return ontouchevent(event);
在這個方法裡面,先進行了一個判斷
第一個條件montouchlistener就是我們調用view的settouchlistener()方法設定的
第二個條件是判斷view是否為enabled的, view一般都是enabled,除非你手動設定為disabled
第三個條件就是ontouchlistener接口的ontouch()方法的傳回值了,如果調用了settouchlistener()設定ontouchlistener,并且ontouch()方法傳回true,view的dispatchtouchevent()方法就直接傳回true,否則就執行view的ontouchevent() 并傳回view的ontouchevent()的值
現在你了解了view的ontouchevent()方法和ontouch()的關系了吧,為什麼android提供了處理touch事件ontouchevent()方法還要增加一個ontouchlistener接口呢?我覺得ontouchlistener接口是對處理touch事件的屏蔽和擴充作用吧,屏蔽作用我就不舉例介紹了,看上面的源碼就知道了,我就說下擴充吧,比如我們要列印view的touch的點的坐标,我們可以自定義一個view如下
public class customview extends view {
public customview(context context, attributeset attrs) {
super(context, attrs);
public customview(context context, attributeset attrs, int defstyle) {
super(context, attrs, defstyle);
@override
public boolean ontouchevent(motionevent event) {
log.i("tag", "x的坐标 = " + event.getx() + " y的坐标 = " + event.gety());
return super.ontouchevent(event);
}
也可以直接對view設定ontouchlistener接口,在return的時候調用下v.ontouchevent()
view.setontouchlistener(new ontouchlistener() {
@override
public boolean ontouch(view v, motionevent event) {
log.i("tag", "x的坐标 = " + event.getx() + " y的坐标 = " + event.gety());
return v.ontouchevent(event);
}
});
這樣子也實作了我們所需要的功能,是以我認為ontouchlistener是對ontouchevent()方法的一個屏蔽和擴充作用,假如你有不一樣的了解,你也可以告訴我下,這裡就不糾結這個了。
我們再看view的ontouchevent()方法
public boolean ontouchevent(motionevent event) {
final int viewflags = mviewflags;
if ((viewflags & enabled_mask) == disabled) {
return (((viewflags & clickable) == clickable ||
(viewflags & long_clickable) == long_clickable));
}
//如果設定了touch代理,就交給代理來處理,mtouchdelegate預設是null
if (mtouchdelegate != null) {
if (mtouchdelegate.ontouchevent(event)) {
return true;
}
//如果view是clickable或者longclickable的ontouchevent就傳回true, 否則傳回false
if (((viewflags & clickable) == clickable ||
(viewflags & long_clickable) == long_clickable)) {
switch (event.getaction()) {
case motionevent.action_up:
boolean prepressed = (mprivateflags & prepressed) != 0;
if ((mprivateflags & pressed) != 0 || prepressed) {
boolean focustaken = false;
if (isfocusable() && isfocusableintouchmode() && !isfocused()) {
focustaken = requestfocus();
}
if (!mhasperformedlongpress) {
removelongpresscallback();
if (!focustaken) {
if (mperformclick == null) {
mperformclick = new performclick();
}
if (!post(mperformclick)) {
performclick();
}
if (munsetpressedstate == null) {
munsetpressedstate = new unsetpressedstate();
if (prepressed) {
mprivateflags |= pressed;
refreshdrawablestate();
postdelayed(munsetpressedstate,
viewconfiguration.getpressedstateduration());
} else if (!post(munsetpressedstate)) {
munsetpressedstate.run();
removetapcallback();
}
break;
case motionevent.action_down:
if (mpendingcheckfortap == null) {
mpendingcheckfortap = new checkfortap();
mprivateflags |= prepressed;
mhasperformedlongpress = false;
postdelayed(mpendingcheckfortap, viewconfiguration.gettaptimeout());
case motionevent.action_cancel:
mprivateflags &= ~pressed;
refreshdrawablestate();
removetapcallback();
case motionevent.action_move:
final int x = (int) event.getx();
final int y = (int) event.gety();
//當手指在view上面滑動超過view的邊界,
int slop = mtouchslop;
if ((x < 0 - slop) || (x >= getwidth() + slop) ||
(y < 0 - slop) || (y >= getheight() + slop)) {
// outside button
if ((mprivateflags & pressed) != 0) {
mprivateflags &= ~pressed;
return true;
這個方法也是比較長的,我們先看第4行,如果一個view是disabled, 并且該view是clickable或者longclickable, ontouchevent()就不執行下面的代碼邏輯直接傳回true, 表示該view就一直消費touch事件,如果一個enabled的view,并且是clickable或者longclickable的,ontouchevent()會執行下面的代碼邏輯并傳回true,綜上,一個clickable或者longclickable的view是一直消費touch事件的,而一般的view既不是clickable也不是longclickable的(即不會消費touch事件,隻會執行action_down而不會執行action_move和action_up) button是clickable的,可以消費touch事件,但是我們可以通過setclickable()和setlongclickable()來設定view是否為clickable和longclickable。當然還可以通過重寫view的ontouchevent()方法來控制touch事件的消費與否
我們在看57行的action_down, 建立一個checkfortap,我們看看checkfortap是什麼
private final class checkfortap implements runnable {
public void run() {
mprivateflags &= ~prepressed;
mprivateflags |= pressed;
refreshdrawablestate();
if ((mviewflags & long_clickable) == long_clickable) {
postcheckforlongclick(viewconfiguration.gettaptimeout());
原來是個runnable對象,然後使用handler的post方法延時viewconfiguration.gettaptimeout()執行checkfortap的run()方法,在run方法中先判斷view是否longclickable的,一般的view都是false, postcheckforlongclick(viewconfiguration.gettaptimeout())這段代碼就是執行長按的邏輯的代碼,隻有當我們設定為longclickble才會去執行postcheckforlongclick(viewconfiguration.gettaptimeout()),這裡我就不介紹了
由于考慮到文章篇幅的問題,我就不繼續分析view的長按事件和點選事件了,在這裡我直接得出結論吧
長按事件是在action_down中執行,點選事件是在action_up中執行,要想執行長按事件,這個view必須是longclickable的, 也許你會納悶,一般的view不是longclickable為什麼也會執行長按事件呢?我們要執行長按事件必須要調用setonlongclicklistener()設定onlongclicklistener接口,我們看看這個方法的源碼
public void setonlongclicklistener(onlongclicklistener l) {
if (!islongclickable()) {
setlongclickable(true);
}
monlongclicklistener = l;
}
看到沒有,如果這個view不是longclickable的,我們就調用setlongclickable(true)方法設定為longclickable的,是以才會去執行長按方法onlongclick();
要想執行點選事件,這個view就必須要消費action_down和action_move事件,并且沒有設定onlongclicklistener的情況下,如果設定了onlongclicklistener的情況下,需要onlongclick()傳回false才能執行到onclick()方法,也許你又會納悶,一般的view預設是不消費touch事件的,這不是和你上面說的相違背嘛,我們要向執行點選事件必須要調用setonclicklistener()來設定onclicklistener接口,我們看看這個方法的源碼就知道了
public void setonclicklistener(onclicklistener l) {
if (!isclickable()) {
setclickable(true);
monclicklistener = l;
是以說一個enable的并且是clickable的view是一直消費touch事件的,是以才會執行到onclick()方法
對于view的touch事件的分發機制算是告一段落了,從上面我們可以得出textview的dispatchtouchevent()的傳回false的,即不消費touch事件。我們就要往上看relativelayout的dispatchtouchevent()方法的51行,由于textview.dispatchtouchevent()為false, 導緻mmotiontarget沒有被指派,還是null, 繼續往下走執行relativelayout的dispatchtouchevent()方法, 來到第84行,
判斷target是否為null,這個target就是mmotiontarget,滿足條件,執行92行的 super.dispatchtouchevent(ev)代碼并傳回, 這裡調用的是relativelayout父類view的dispatchtouchevent()方法,由于relativelayout沒有設定ontouchlistener, 是以這裡直接調用relativelayout(其實就是view, 因為relativelayout沒有重寫ontouchevent())的ontouchevent()方法
由于relativelayout既不是clickable的也是longclickable的,是以其ontouchevent()方法false, relativelayout的dispatchtouchevent()也是傳回false,這裡就執行完了relativelayout的dispatchtouchevent()方法
繼續執行framelayout的dispatchtouchevent()的第51行,由于relativelayout.dispatchtouchevent()傳回的是false, 跟上面的邏輯是一樣的, 也是執行到92行的super.dispatchtouchevent(ev)代碼并傳回,然後執行framelayout的ontouchevent()方法,而framelayout的ontouchevent()也是傳回false,是以framelayout的dispatchtouchevent()方法傳回false,執行完畢framelayout的dispatchtouchevent()方法
在上面的我就不分析了,大家自行分析一下,跟上面的邏輯是一樣的,我直接畫了個圖來幫大家了解下(這裡沒有畫出onintercepttouchevent()方法)
是以我們點選螢幕上面的textview的事件分發流程是上圖那個樣子的,表示activity的view都不消費action_down事件,是以就不能在觸發action_move, action_up等事件了,具體是為什麼?我還不太清楚,畢竟從activity到textview這一層是分析不出來的,估計是在底層實作的。
但如果将textview換成button,流程是不是還是這個樣子呢?答案不是,我們來分析分析一下,如果是button , button是一個clickable的view,ontouchevent()傳回true, 表示他一直消費touch事件,是以button的dispatchtouchevent()方法傳回true, 回到relativelayout的dispatchtouchevent()方法的51行,滿足條件,進入到if方法體,設定mmotiontarget為button,然後直接傳回true, relativelayout的dispatchtouchevent()方法執行完畢, 不會調用到relativelayout的ontouchevent()方法
然後到framelayout的dispatchtouchevent()方法的51行,由于relativelayout.dispatchtouchevent()傳回true, 滿足條件,進入if方法體,設定mmotiontarget為relativelayout,注意下,這裡的mmotiontarget跟relativelayout的dispatchtouchevent()方法的mmotiontarget不是同一個哦,因為他們是不同的方法中的,然後傳回true
同理framelayout的dispatchtouchevent()也是傳回true, decorview的dispatchtouchevent()方法也傳回true, 還是畫一個流程圖(這裡沒有畫出onintercepttouchevent()方法)給大家理清下
從上面的流程圖得出一個結論,touch事件是從頂層的view一直往下分發到手指按下的最裡面的view,如果這個view的ontouchevent()傳回false,即不消費touch事件,這個touch事件就會向上找父布局調用其父布局的ontouchevent()處理,如果這個view傳回true,表示消費了touch事件,就不調用父布局的ontouchevent()
接下來我們用一個自定義的viewgroup來替換relativelayout,自定義viewgroup代碼如下
package com.example.androidtouchevent;
import android.content.context;
import android.util.attributeset;
import android.view.motionevent;
import android.widget.relativelayout;
public class customlayout extends relativelayout {
public customlayout(context context, attributeset attrs) {
super(context, attrs, 0);
public customlayout(context context, attributeset attrs, int defstyle) {
public boolean onintercepttouchevent(motionevent ev) {
return true;
我們就重寫了onintercepttouchevent(),傳回true, relativelayout預設是傳回false, 然後再customlayout布局中加一個button ,如下圖
我們這次不從decorview的dispatchtouchevent()分析了,直接從customlayout的dispatchtouchevent()分析
我們先看action_down 來到25行,由于我們重寫了onintercepttouchevent()傳回true, 是以不走這個if裡面,直接往下看代碼,來到84行, target為null,是以進入if方法裡面,直接調用super.dispatchtouchevent()方法, 也就是view的dispatchtouchevent()方法,而在view的dispatchtouchevent()方法中是直接調用view的ontouchevent()方法,但是customlayout重寫了ontouchevent(),是以這裡還是調用customlayout的ontouchevent(),
這個方法傳回false, 不消費touch事件,是以不會在觸發action_move,action_up等事件了,這裡我再畫一個流程圖吧(含有onintercepttouchevent()方法的)
好了,就分析到這裡吧,差不多分析完了,還有一種情況沒有分析到,例如我将customlayout的代碼改成下面的情形,touch事件又是怎麼分發的呢?我這裡就不帶大家分析了
if(ev.getaction() == motionevent.action_move){
return super.onintercepttouchevent(ev);
這篇文章的篇幅有點長,如果你想了解touch事件的分發機制,你一定要認真看完,下面來總結一下吧
1.activity的最頂層window是phonewindow,phonewindow的最頂層view是decorview
2.一個clickable或者longclickable的view會永遠消費touch事件,不管他是enabled還是disabled的
3.view的長按事件是在action_down中執行,要想執行長按事件該view必須是longclickable的,并且不能産生action_move
4.view的點選事件是在action_up中執行,想要執行點選事件的前提是消費了action_down和action_move,并且沒有設定onlongclicklistener的情況下,如設定了onlongclicklistener的情況,則必須使onlongclick()傳回false
5.如果view設定了ontouchlistener了,并且ontouch()方法傳回true,則不執行view的ontouchevent()方法,也表示view消費了touch事件,傳回false則繼續執行ontouchevent()
6.touch事件是從最頂層的view一直分發到手指touch的最裡層的view,如果最裡層view消費了action_down事件(設定ontouchlistener,并且ontouch()傳回true 或者ontouchevent()方法傳回true)才會觸發action_move,action_up的發生,如果某個viewgroup攔截了touch事件,則touch事件交給viewgroup處理
7.touch事件的分發過程中,如果消費了action_down,而在分發action_move的時候,某個viewgroup攔截了touch事件,就像上面那個自定義customlayout,則會将action_cancel分發給該viewgroup下面的touch到的view,然後将touch事件交給viewgroup處理,并傳回true