探讨android事件傳遞機制前,明确android的兩大基礎控件類型:view和viewgroup。view即普通的控件,沒有子布局的,如button、textview. viewgroup繼承自view,表示可以有子控件,如linearlayout、listview這些。而事件即motionevent,最重要的有3個:
(1)motionevent.action_down 按下view,是所有事件的開始
(2)motionevent.action_move 滑動事件
(3)motionevent.action_up 與down對應,表示擡起
另外,明确事件傳遞機制的最終目的都是為了觸發執行view的點選監聽和觸摸監聽:
******.setonclicklistener(new view.onclicklistener() {
@override
public void onclick(view v) {
// todo auto-generated method stub
log.i(tag, "testlinelayout---onclick...");
}
});
*******.setontouchlistener(new view.ontouchlistener() {
public boolean ontouch(view v, motionevent event) {
return false;
我們簡稱為onclick監聽和ontouch監聽,一般程式會注冊這兩個監聽。從上面可以看到,ontouch監聽裡預設return false。不要小看了這個return false,後面可以看到它有大用。
對于view來說,事件傳遞機制有兩個函數:dispatchtouchevent負責分發事件,在dispatch***裡又會調用ontouchevent表示執行事件,或者說消費事件,結合源碼分析其流程。事件傳遞的入口是view的dispatchtouchevent()函數:
<span style="font-size:18px;"> /**
* pass the touch screen motion event down to the target view, or this
* view if it is the target.
*
* @param event the motion event to be dispatched.
* @return true if the event was handled by the view, false otherwise.
*/
public boolean dispatchtouchevent(motionevent event) {
if (minputeventconsistencyverifier != null) {
minputeventconsistencyverifier.ontouchevent(event, 0);
}
if (onfiltertoucheventforsecurity(event)) {
//noinspection simplifiableifstatement
listenerinfo li = mlistenerinfo;
if (li != null && li.montouchlistener != null && (mviewflags & enabled_mask) == enabled
&& li.montouchlistener.ontouch(this, event)) {
return true;
}
if (ontouchevent(event)) {
minputeventconsistencyverifier.onunhandledevent(event, 0);
return false;
}</span>
找到這個判斷:
if (li != null && li.montouchlistener != null && (mviewflags & enabled_mask) == enabled
&& li.montouchlistener.ontouch(this, event)) {
return true;
}
他會執行view的ontouchlistener.ontouch這個函數,也就是上面說的ontouch監聽。裡面有三個判斷,如果三個都為1,就會執行return true,不往下走了。而預設的ontouch監聽傳回false,隻要一個是false,就不會傳回true。接着往下看,程式執行ontouchevent:
<span style="font-size:18px;"> if (ontouchevent(event)) {
}</span>
ontouchevent的源碼比較多,貼最重要的:
<span style="font-size:18px;"> if (!mhasperformedlongpress) {
// this is a tap, so remove the longpress check
removelongpresscallback();
// only perform take click actions if we were in the pressed state
if (!focustaken) {
// use a runnable and post this rather than calling
// performclick directly. this lets other visual state
// of the view update before click actions start.
if (mperformclick == null) {
mperformclick = new performclick();
}
if (!post(mperformclick)) {
<span style="color:#ff0000;"> performclick();</span>
}
}</span>
可以看到有個performclick(),它的源碼裡有這麼一句 li.monclicklistener.onclick(this);
<span style="font-size:18px;"> public boolean performclick() {
sendaccessibilityevent(accessibilityevent.type_view_clicked);
listenerinfo li = mlistenerinfo;
if (li != null && li.monclicklistener != null) {
playsoundeffect(soundeffectconstants.click);
<span style="color:#ff0000;"><strong>li.monclicklistener.onclick(this);</strong></span>
return true;
終于對上了,它執行了我們注冊的onclick監聽。當然執行前會經過一系列判斷,是否注冊了監聽等。
總結:
1、事件入口是dispatchtouchevent(),它會先執行注冊的ontouch監聽,如果一切順利的話,接着執行ontouchevent,在ontouchevent裡會執行onclick監聽。
2、無論是dispatchtouchevent還是ontouchevent,如果傳回true表示這個事件已經被消費、處理了,不再往下傳了。在dispathtouchevent的源碼裡可以看到,如果ontouchevent傳回了true,那麼它也傳回true。如果dispatch***在執行ontouch監聽的時候,ontouch傳回了true,那麼它也傳回true,這個事件提前被ontouch消費掉了。就不再執行ontouchevent了,更别說onclick監聽了。
3、我們通常在ontouch監聽了設定圖檔一旦被觸摸就改變它的背景、透明度之類的,這個ontouch表示事件的時機。而在onclick監聽了去具體幹某些事。
下面通過代碼來說明,自定義一個testbutton繼承自button,重寫它的dispath***和ontouchevent方法,為了簡單隻關注down和up事件。
<span style="font-size:18px;">package org.yanzi.ui;
import android.content.context;
import android.util.attributeset;
import android.util.log;
import android.view.motionevent;
import android.widget.button;
public class testbutton extends button {
private final static string tag = "yan";
public testbutton(context context, attributeset attrs) {
super(context, attrs);
// todo auto-generated constructor stub
}
@override
public boolean ontouchevent(motionevent event) {
// todo auto-generated method stub
switch(event.getaction()){
case motionevent.action_down:
log.i(tag, "testbutton-ontouchevent-action_down...");
break;
case motionevent.action_up:
log.i(tag, "testbutton-ontouchevent-action_up...");
default:break;
return super.ontouchevent(event);
log.i(tag, "testbutton-dispatchtouchevent-action_down...");
log.i(tag, "testbutton-dispatchtouchevent-action_up...");
return super.dispatchtouchevent(event);
}
</span>
在activity裡注冊兩個監聽:
<span style="font-size:18px;"> testbtn.setonclicklistener(new view.onclicklistener() {
@override
public void onclick(view v) {
// todo auto-generated method stub
log.i(tag, "testbtn---onclick...");
});
testbtn.setontouchlistener(new view.ontouchlistener() {
public boolean ontouch(view v, motionevent event) {
switch(event.getaction()){
case motionevent.action_down:
log.i(tag, "testbtn-ontouch-action_down...");
break;
case motionevent.action_up:
log.i(tag, "testbtn-ontouch-action_up...");
default:break;
}
return false;
});</span>
同時複寫activity的dispatch方法和ontouchevent方法:
<span style="font-size:18px;">@override
public boolean dispatchtouchevent(motionevent ev) {
switch(ev.getaction()){
log.i(tag, "mainactivity-dispatchtouchevent-action_down...");
log.i(tag, "mainactivity-dispatchtouchevent-action_up...");
return super.dispatchtouchevent(ev);
log.i(tag, "mainactivity-ontouchevent-action_down...");
log.i(tag, "mainactivity-ontouchevent-action_up...");
最終一次點選,列印資訊如下:
line 33: 01-08 14:59:45.847 i/yan ( 4613): mainactivity-dispatchtouchevent-action_down...
line 35: 01-08 14:59:45.849 i/yan ( 4613): testbutton-dispatchtouchevent-action_down...
line 37: 01-08 14:59:45.849 i/yan ( 4613): testbtn-ontouch-action_down...
line 39: 01-08 14:59:45.849 i/yan ( 4613): testbutton-ontouchevent-action_down...
line 41: 01-08 14:59:45.939 i/yan ( 4613): mainactivity-dispatchtouchevent-action_up...
line 43: 01-08 14:59:45.941 i/yan ( 4613): testbutton-dispatchtouchevent-action_up...
line 45: 01-08 14:59:45.944 i/yan ( 4613): testbtn-ontouch-action_up...
line 47: 01-08 14:59:45.946 i/yan ( 4613): testbutton-ontouchevent-action_up...
line 49: 01-08 14:59:45.974 i/yan ( 4613): testbtn---onclick...
事件先由activity的dispatchtouchevent進行分發,然後testbutton的dispatchtouchevent進行分發,接着執行ontouch監聽,然後執行ontouchevent。第二次up動作的時候,在ontouchevent裡又執行了onclick監聽。
如果我們想這個testbutton隻能執行ontouch監聽不能執行onclick監聽,方法有很多。在ontouch監聽裡預設傳回false改為true,如下:
<span style="font-size:18px;">testbtn.setontouchlistener(new view.ontouchlistener() {
事件流程為:
line 75: 01-08 15:05:51.627 i/yan ( 5262): mainactivity-dispatchtouchevent-action_down...
line 77: 01-08 15:05:51.628 i/yan ( 5262): testbutton-dispatchtouchevent-action_down...
line 79: 01-08 15:05:51.629 i/yan ( 5262): testbtn-ontouch-action_down...
line 81: 01-08 15:05:51.689 i/yan ( 5262): mainactivity-dispatchtouchevent-action_up...
line 83: 01-08 15:05:51.691 i/yan ( 5262): testbutton-dispatchtouchevent-action_up...
line 85: 01-08 15:05:51.695 i/yan ( 5262): testbtn-ontouch-action_up...
可以看到壓根就沒執行ontouchevent。因為ontouch傳回了true,已提前将這個事件消費了,就不往下傳了,dispatch流程提前終止。
<span style="font-size:18px;"> public boolean dispatchtouchevent(motionevent ev) {
minputeventconsistencyverifier.ontouchevent(ev, 1);
boolean handled = false;
if (onfiltertoucheventforsecurity(ev)) {
final int action = ev.getaction();
final int actionmasked = action & motionevent.action_mask;
// handle an initial down.
if (actionmasked == motionevent.action_down) {
// throw away all previous state when starting a new touch gesture.
// the framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, anr, or some other state change.
cancelandcleartouchtargets(ev);
resettouchstate();
// check for interception.
final boolean intercepted;
if (actionmasked == motionevent.action_down
|| mfirsttouchtarget != null) {
final boolean disallowintercept = (mgroupflags & flag_disallow_intercept) != 0;
if (!disallowintercept) {
<strong><span style="color:#ff0000;">intercepted = onintercepttouchevent(ev);</span></strong>
ev.setaction(action); // restore action in case it was changed
} else {
intercepted = false;
} else {
// there are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
// check for cancelation.
final boolean canceled = resetcancelnextupflag(this)
|| actionmasked == motionevent.action_cancel;
// update list of touch targets for pointer down, if needed.
final boolean split = (mgroupflags & flag_split_motion_events) != 0;
touchtarget newtouchtarget = null;
boolean alreadydispatchedtonewtouchtarget = false;
<strong><span style="color:#ff0000;">if (!canceled && !intercepted)</span></strong> {
if (actionmasked == motionevent.action_down
|| (split && actionmasked == motionevent.action_pointer_down)
|| actionmasked == motionevent.action_hover_move) {
final int actionindex = ev.getactionindex(); // always 0 for down
final int idbitstoassign = split ? 1 << ev.getpointerid(actionindex)
: touchtarget.all_pointer_ids;
// clean up earlier touch targets for this pointer id in case they
// have become out of sync.
removepointersfromtouchtargets(idbitstoassign);
final int childrencount = mchildrencount;
if (childrencount != 0) {
// find a child that can receive the event.
// scan children from front to back.
final view[] children = mchildren;
final float x = ev.getx(actionindex);
final float y = ev.gety(actionindex);
final boolean customorder = ischildrendrawingorderenabled();
for (int i = childrencount - 1; i >= 0; i--) {
final int childindex = customorder ?
getchilddrawingorder(childrencount, i) : i;
final view child = children[childindex];
if (!canviewreceivepointerevents(child)
|| !istransformedtouchpointinview(x, y, child, null)) {
continue;
newtouchtarget = gettouchtarget(child);
if (newtouchtarget != null) {
// child is already receiving touch within its bounds.
// give it the new pointer in addition to the ones it is handling.
newtouchtarget.pointeridbits |= idbitstoassign;
break;
resetcancelnextupflag(child);
if (dispatchtransformedtouchevent(ev, false, child, idbitstoassign)) {
// child wants to receive touch within its bounds.
mlasttouchdowntime = ev.getdowntime();
mlasttouchdownindex = childindex;
mlasttouchdownx = ev.getx();
mlasttouchdowny = ev.gety();
newtouchtarget = addtouchtarget(child, idbitstoassign);
alreadydispatchedtonewtouchtarget = true;
}
}
if (newtouchtarget == null && mfirsttouchtarget != null) {
// did not find a child to receive the event.
// assign the pointer to the least recently added target.
newtouchtarget = mfirsttouchtarget;
while (newtouchtarget.next != null) {
newtouchtarget = newtouchtarget.next;
newtouchtarget.pointeridbits |= idbitstoassign;
// dispatch to touch targets.
if (mfirsttouchtarget == null) {
// no touch targets so treat this as an ordinary view.
handled = dispatchtransformedtouchevent(ev, canceled, null,
touchtarget.all_pointer_ids);
// dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. cancel touch targets if necessary.
touchtarget predecessor = null;
touchtarget target = mfirsttouchtarget;
while (target != null) {
final touchtarget next = target.next;
if (alreadydispatchedtonewtouchtarget && target == newtouchtarget) {
handled = true;
} else {
final boolean cancelchild = resetcancelnextupflag(target.child)
|| intercepted;
if (dispatchtransformedtouchevent(ev, cancelchild,
target.child, target.pointeridbits)) {
handled = true;
if (cancelchild) {
if (predecessor == null) {
mfirsttouchtarget = next;
} else {
predecessor.next = next;
target.recycle();
target = next;
continue;
predecessor = target;
target = next;
// update list of touch targets for pointer up or cancel, if needed.
if (canceled
|| actionmasked == motionevent.action_up
|| actionmasked == motionevent.action_hover_move) {
} else if (split && actionmasked == motionevent.action_pointer_up) {
final int actionindex = ev.getactionindex();
final int idbitstoremove = 1 << ev.getpointerid(actionindex);
removepointersfromtouchtargets(idbitstoremove);
if (!handled && minputeventconsistencyverifier != null) {
minputeventconsistencyverifier.onunhandledevent(ev, 1);
return handled;
可以看到标紅的有兩句(intercepted = onintercepttouchevent(ev); if (!canceled && !intercepted) ),它會先調用 intercepted = onintercepttouchevent(ev);然後通過if判斷。
<span style="font-size:18px;"> public boolean onintercepttouchevent(motionevent ev) {
它就一句話,預設false。也就是說這個謀士預設的意見是,永遠不攔截!!!!隻要有孩子,就交給孩子們處理吧。下面給出執行個體說明,建立testlinearlayout繼承自linearlayout。
import android.widget.linearlayout;
public class testlinearlayout extends linearlayout{
public testlinearlayout(context context, attributeset attrs) {
log.i(tag, "testlinearlayout-dispatchtouchevent-action_down...");
log.i(tag, "testlinearlayout-dispatchtouchevent-action_up...");
public boolean onintercepttouchevent(motionevent ev) {
log.i(tag, "testlinearlayout-onintercepttouchevent-action_down...");
log.i(tag, "testlinearlayout-onintercepttouchevent-action_up...");
return super.onintercepttouchevent(ev);
log.i(tag, "testlinearlayout-ontouchevent-action_down...");
log.i(tag, "testlinearlayout-ontouchevent-action_up...");
布局檔案改成:
<span style="font-size:18px;"><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"
android:paddingbottom="@dimen/activity_vertical_margin"
android:paddingleft="@dimen/activity_horizontal_margin"
android:paddingright="@dimen/activity_horizontal_margin"
android:paddingtop="@dimen/activity_vertical_margin"
tools:context=".mainactivity" >
<textview
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hello_world" />
<org.yanzi.ui.testlinearlayout
android:id="@+id/linearlayout_test"
android:layout_width="200dip"
android:layout_height="200dip" >
<org.yanzi.ui.testbutton
android:id="@+id/btn_test"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="測試按鈕" />
</org.yanzi.ui.testlinearlayout>
</relativelayout></span>
在activity裡給這個自定義linearlayout也注冊上onclick監聽、ontouch監聽。
<span style="font-size:18px;">testlinelayout = (testlinearlayout)findviewbyid(r.id.linearlayout_test);
testlinelayout.setontouchlistener(new view.ontouchlistener() {
log.i(tag, "testlinelayout-ontouch-action_down...");
log.i(tag, "testlinelayout-ontouch-action_up...");
testlinelayout.setonclicklistener(new view.onclicklistener() {
log.i(tag, "testlinelayout---onclick...");
不複寫事件傳遞裡的 任何方法,流程如下:
line 57: 01-08 15:29:42.167 i/yan ( 5826): mainactivity-dispatchtouchevent-action_down...
line 59: 01-08 15:29:42.169 i/yan ( 5826): testlinearlayout-dispatchtouchevent-action_down...
line 61: 01-08 15:29:42.169 i/yan ( 5826): testlinearlayout-onintercepttouchevent-action_down...
line 63: 01-08 15:29:42.169 i/yan ( 5826): testbutton-dispatchtouchevent-action_down...
line 65: 01-08 15:29:42.170 i/yan ( 5826): testbtn-ontouch-action_down...
line 67: 01-08 15:29:42.170 i/yan ( 5826): testbutton-ontouchevent-action_down...
---------------------------------------------------------------------------------------------------------------------------
line 69: 01-08 15:29:42.279 i/yan ( 5826): mainactivity-dispatchtouchevent-action_up...
line 71: 01-08 15:29:42.280 i/yan ( 5826): testlinearlayout-dispatchtouchevent-action_up...
line 73: 01-08 15:29:42.283 i/yan ( 5826): testlinearlayout-onintercepttouchevent-action_up...
line 75: 01-08 15:29:42.287 i/yan ( 5826): testbutton-dispatchtouchevent-action_up...
line 81: 01-08 15:29:42.298 i/yan ( 5826): testbtn-ontouch-action_up...
line 83: 01-08 15:29:42.301 i/yan ( 5826): testbutton-ontouchevent-action_up...
line 85: 01-08 15:29:42.313 i/yan ( 5826): testbtn---onclick...
由activity的dispatchtouchevent----linearlayout的dispatchtouchevent------------問問它的謀士要不要讓孩子知道onintercepttouchevent---------孩子的dispatchtouchevent-----孩子的ontouch監聽------孩子的ontouchevent----孩子的onclick監聽。為了更清晰這個流程,下面作如下改動:
1、如果事件傳給了孩子們,但孩子沒有ontouch和onclick監聽怎麼辦?即将button的onclick和ontouch都注釋掉:
流程如下:
line 131: 01-08 15:36:16.574 i/yan ( 6124): testlinearlayout-dispatchtouchevent-action_down...
line 133: 01-08 15:36:16.574 i/yan ( 6124): testlinearlayout-onintercepttouchevent-action_down...
line 135: 01-08 15:36:16.574 i/yan ( 6124): testbutton-dispatchtouchevent-action_down...
line 137: 01-08 15:36:16.575 i/yan ( 6124): testbutton-ontouchevent-action_down...
line 143: 01-08 15:36:16.746 i/yan ( 6124): mainactivity-dispatchtouchevent-action_up...
line 145: 01-08 15:36:16.747 i/yan ( 6124): testlinearlayout-dispatchtouchevent-action_up...
line 147: 01-08 15:36:16.747 i/yan ( 6124): testlinearlayout-onintercepttouchevent-action_up...
line 149: 01-08 15:36:16.748 i/yan ( 6124): testbutton-dispatchtouchevent-action_up...
line 151: 01-08 15:36:16.748 i/yan ( 6124): testbutton-ontouchevent-action_up...
因為事件給了孩子們,它沒監聽也關系不到父親了,父親的onclick和ontouch都沒執行。
2,如果将testlinearlayout的onintercepttouchevent 改成return true,即不讓孩子們知道。
line 57: 01-08 15:40:06.832 i/yan ( 6640): mainactivity-dispatchtouchevent-action_down...
line 59: 01-08 15:40:06.835 i/yan ( 6640): testlinearlayout-dispatchtouchevent-action_down...
line 61: 01-08 15:40:06.836 i/yan ( 6640): testlinearlayout-onintercepttouchevent-action_down...
line 63: 01-08 15:40:06.836 i/yan ( 6640): testlinelayout-ontouch-action_down...
line 65: 01-08 15:40:06.836 i/yan ( 6640): testlinearlayout-ontouchevent-action_down...
line 67: 01-08 15:40:07.016 i/yan ( 6640): mainactivity-dispatchtouchevent-action_up...
line 69: 01-08 15:40:07.017 i/yan ( 6640): testlinearlayout-dispatchtouchevent-action_up...
line 73: 01-08 15:40:07.025 i/yan ( 6640): testlinelayout-ontouch-action_up...
line 75: 01-08 15:40:07.026 i/yan ( 6640): testlinearlayout-ontouchevent-action_up...
line 77: 01-08 15:40:07.052 i/yan ( 6640): testlinelayout---onclick...
果然事件就此打住,孩子們壓根不知道,父親執行了onclick和ontouch監聽。可見父親還是偉大的啊,隻要謀士不攔截事件,那麼事件就給孩子。
最後的結論:
1、如果是自定義複合控件,如圖檔+文字,我再activity裡給你注冊了onclick監聽,期望點選它執行。那麼最簡單的方法就是将圖檔+文字的父布局,也即讓其容器viewgroup的秘書将事件攔下,這樣父親就可以執行onclick了。這時候的父親就像一個獨立的孩子一樣了(view),無官一身輕,再也不用管它的孩子了,可以正常onclick ontouch.
2、如果希望一個view隻ontouch而不onclick,在ontouch裡return true就ok了。
3、dispatch是為了ontouch監聽,ontouchevent是為了onclick監聽。
4、自定義布局時,一般情況下:
public boolean ontouchevent(motionevent event) {return super.ontouchevent(event);}
public boolean dispatchtouchevent(motionevent event) {return super.dispatchtouchevent(event);
我們可以複寫,但是最後的super.***是萬萬不能少滴。如果少了,表示連dispatch*** ontouchevent壓根就不調用了,事件就此打住。
貌似真相水落石出了,但究竟清楚了沒有請看下篇根據自定義複合控件的監聽問題再探讨下。