首先我們先寫個簡單的例子來測試view的事件轉發的流程~
為了更好的研究view的事件轉發,我們自定以一個mybutton繼承button,然後把跟事件傳播有關的方法進行複寫,然後添加上日志~
mybutton
package com.example.zhy_event03;
import android.content.context;
import android.util.attributeset;
import android.util.log;
import android.view.motionevent;
import android.widget.button;
public class mybutton extends button
{
private static final string tag = mybutton.class.getsimplename();
public mybutton(context context, attributeset attrs)
{
super(context, attrs);
}
@override
public boolean ontouchevent(motionevent event)
int action = event.getaction();
switch (action)
{
case motionevent.action_down:
log.e(tag, "ontouchevent action_down");
break;
case motionevent.action_move:
log.e(tag, "ontouchevent action_move");
case motionevent.action_up:
log.e(tag, "ontouchevent action_up");
default:
}
return super.ontouchevent(event);
public boolean dispatchtouchevent(motionevent event)
log.e(tag, "dispatchtouchevent action_down");
log.e(tag, "dispatchtouchevent action_move");
log.e(tag, "dispatchtouchevent action_up");
return super.dispatchtouchevent(event);
}
在ontouchevent和dispatchtouchevent中列印了日志~
然後把我們自定義的按鈕加到主布局檔案中;
布局檔案:
<linearlayout 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" >
<com.example.zhy_event03.mybutton
android:id="@+id/id_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="click me" />
</linearlayout>
最後看一眼mainactivity的代碼
import android.app.activity;
import android.os.bundle;
import android.view.view;
import android.view.view.ontouchlistener;
public class mainactivity extends activity
protected static final string tag = "mybutton";
private button mbutton ;
protected void oncreate(bundle savedinstancestate)
super.oncreate(savedinstancestate);
setcontentview(r.layout.activity_main);
mbutton = (button) findviewbyid(r.id.id_btn);
mbutton.setontouchlistener(new ontouchlistener()
@override
public boolean ontouch(view v, motionevent event)
{
int action = event.getaction();
switch (action)
{
case motionevent.action_down:
log.e(tag, "ontouch action_down");
break;
case motionevent.action_move:
log.e(tag, "ontouch action_move");
case motionevent.action_up:
log.e(tag, "ontouch action_up");
default:
}
return false;
}
});
在mainactivity中,我們還給mybutton設定了ontouchlistener這個監聽~
好了,跟view事件相關一般就這三個地方了,一個ontouchevent,一個dispatchtouchevent,一個setontouchlistener;
下面我們運作,然後點選按鈕,檢視日志輸出:
08-31 06:09:39.030: e/mybutton(879): dispatchtouchevent action_down
08-31 06:09:39.030: e/mybutton(879): ontouch action_down
08-31 06:09:39.049: e/mybutton(879): ontouchevent action_down
08-31 06:09:39.138: e/mybutton(879): dispatchtouchevent action_move
08-31 06:09:39.138: e/mybutton(879): ontouch action_move
08-31 06:09:39.147: e/mybutton(879): ontouchevent action_move
08-31 06:09:39.232: e/mybutton(879): dispatchtouchevent action_up
08-31 06:09:39.248: e/mybutton(879): ontouch action_up
08-31 06:09:39.248: e/mybutton(879): ontouchevent action_up
我有意點選的時候蹭了一下,不然不會觸發move,手抖可能會列印一堆move的日志~~~
好了,可以看到,不管是down,move,up都會按照下面的順序執行:
1、dispatchtouchevent
2、 setontouchlistener的ontouch
3、ontouchevent
下面就跟随日志的腳步開始源碼的探索~
首先進入view的dispatchtouchevent
/**
* 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 (!onfiltertoucheventforsecurity(event)) {
return false;
if (montouchlistener != null && (mviewflags & enabled_mask) == enabled &&
montouchlistener.ontouch(this, event)) {
return true;
return ontouchevent(event);
直接看13行:首先判斷montouchlistener不為null,并且view是enable的狀态,然後 montouchlistener.ontouch(this, event)傳回true,這三個條件如果都滿足,直接return true ; 也就是下面的ontouchevent(event)不會被執行了;
那麼montouchlistener是和方神聖,我們來看看:
* register a callback to be invoked when a touch event is sent to this view.
* @param l the touch listener to attach to this view
*/
public void setontouchlistener(ontouchlistener l) {
montouchlistener = l;
}
其實就是我們在activity中設定的setontouchlistener。
也就是說:如果我們設定了setontouchlistener,并且return true,那麼view自己的ontouchevent就不會被執行了,當然了,本例我們return false,我們還得往下探索 ;
已經解決一個常見的問題:view的ontouchlistener和ontouchevent的調用關系,相信大家應該已經明白了~let's go;繼續往下。
接下來是view的ontouchevent:
* implement this method to handle touch screen motion events.
* @param event the motion event.
* @return true if the event was handled, false otherwise.
public boolean ontouchevent(motionevent event) {
final int viewflags = mviewflags;
if ((viewflags & enabled_mask) == disabled) {
// a disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewflags & clickable) == clickable ||
(viewflags & long_clickable) == long_clickable));
if (mtouchdelegate != null) {
if (mtouchdelegate.ontouchevent(event)) {
return true;
if (((viewflags & clickable) == clickable ||
(viewflags & long_clickable) == long_clickable)) {
switch (event.getaction()) {
boolean prepressed = (mprivateflags & prepressed) != 0;
if ((mprivateflags & pressed) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focustaken = false;
if (isfocusable() && isfocusableintouchmode() && !isfocused()) {
focustaken = requestfocus();
}
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)) {
performclick();
}
if (munsetpressedstate == null) {
munsetpressedstate = new unsetpressedstate();
if (prepressed) {
mprivateflags |= pressed;
refreshdrawablestate();
postdelayed(munsetpressedstate,
viewconfiguration.getpressedstateduration());
} else if (!post(munsetpressedstate)) {
// if the post failed, unpress right now
munsetpressedstate.run();
removetapcallback();
}
if (mpendingcheckfortap == null) {
mpendingcheckfortap = new checkfortap();
mprivateflags |= prepressed;
mhasperformedlongpress = false;
postdelayed(mpendingcheckfortap, viewconfiguration.gettaptimeout());
case motionevent.action_cancel:
mprivateflags &= ~pressed;
refreshdrawablestate();
removetapcallback();
final int x = (int) event.getx();
final int y = (int) event.gety();
// be lenient about moving outside of buttons
int slop = mtouchslop;
if ((x < 0 - slop) || (x >= getwidth() + slop) ||
(y < 0 - slop) || (y >= getheight() + slop)) {
// outside button
if ((mprivateflags & pressed) != 0) {
// remove any future long press/tap checks
// need to switch from pressed to not pressed
mprivateflags &= ~pressed;
return false;
代碼還是比較長的,
10-15行,如果目前view是disabled狀态且是可點選則會消費掉事件(return true);可以忽略,不是我們的重點;
17-21行,如果設定了mtouchdelegate,則會将事件交給代理者處理,直接return true,如果大家希望自己的view增加它的touch範圍,可以嘗試使用touchdelegate,這裡也不是重點,可以忽略;
接下來到我們的重點了:
23行的判斷:如果我們的view可以點選或者可以長按,則,注意if的範圍,最終一定return true ;
if (((viewflags & clickable) == clickable ||
(viewflags & long_clickable) == long_clickable)) {
//...
return true;
}
接下來就是 switch (event.getaction())了,判斷事件類型,down,move,up等;
我們按照例子執行的順序,先看 case motionevent.action_down (71-78行):
75行:給mprivateflags設定一個prepressed的辨別
76行:設定mhasperformedlongpress=false;表示長按事件還未觸發;
77行:發送一個延遲為viewconfiguration.gettaptimeout()的延遲消息,到達延時時間後會執行checkfortap()裡面的run方法:
1、viewconfiguration.gettaptimeout()為115毫秒;
2、checkfortap
private final class checkfortap implements runnable {
public void run() {
mprivateflags &= ~prepressed;
mprivateflags |= pressed;
refreshdrawablestate();
if ((mviewflags & long_clickable) == long_clickable) {
postcheckforlongclick(viewconfiguration.gettaptimeout());
}
}
}
在run方法裡面取消mprivateflags的prepressed,然後設定pressed辨別,重新整理背景,如果view支援長按事件,則再發一個延時消息,檢測長按;
private void postcheckforlongclick(int delayoffset) {
mhasperformedlongpress = false;
if (mpendingcheckforlongpress == null) {
mpendingcheckforlongpress = new checkforlongpress();
}
mpendingcheckforlongpress.rememberwindowattachcount();
postdelayed(mpendingcheckforlongpress,
viewconfiguration.getlongpresstimeout() - delayoffset);
}
class checkforlongpress implements runnable {
private int moriginalwindowattachcount;
public void run() {
if (ispressed() && (mparent != null)
&& moriginalwindowattachcount == mwindowattachcount) {
if (performlongclick()) {
mhasperformedlongpress = true;
可以看到,當使用者按下,首先會設定辨別為prepressed,如果在115毫秒内擡起了,up時會移除checkfortap這個回調(up時會分析);
如果115後,沒有擡起,會将view的辨別設定為pressed且去掉prepressed辨別,然後發出一個檢測長按的延遲任務,延時為:viewconfiguration.getlongpresstimeout() - delayoffset(500ms -115ms),這個115ms剛好時檢測額prepressed時間;也就是使用者從down觸發開始算起,如果500ms内沒有擡起則認為觸發了長按事件:
1、如果此時設定了長按的回調,則執行長按時的回調,且如果長按的回調傳回true;才把mhasperformedlongpress置為ture;
2、否則,如果沒有設定長按回調或者長按回調傳回的是false;則mhasperformedlongpress依然是false;
好了down就分析完成了;大家回個神,下面回到view的ontouchevent中的action_move:
86到105行:
87-88行:拿到目前觸摸的x,y坐标;
91行判斷當然觸摸點有沒有移出我們的view,如果移出了:
1、執行removetapcallback();
2、然後判斷是否包含pressed辨別,如果包含,移除長按的檢查:removelongpresscallback();
3、最後把mprivateflags中pressed辨別去除,重新整理背景;
private void removetapcallback() {
if (mpendingcheckfortap != null) {
mprivateflags &= ~prepressed;
removecallbacks(mpendingcheckfortap);
這個是移除,down觸發時設定的prepressed的檢測;即目前觸發時機在down觸發不到115ms時,你就已經移出控件外了;
如果115ms後,你才移出控件外,則你的目前mprivateflags一定為pressed且發送了長按的檢測;
就會走上面的2和3;首先移除removelongpresscallback()
private void removelongpresscallback() {
if (mpendingcheckforlongpress != null) {
removecallbacks(mpendingcheckforlongpress);
}
然後把mprivateflags中pressed辨別去除,重新整理背景;
好了,move我們也分析完成了,總結一下:隻要使用者移出了我們的控件:則将mprivateflags取出pressed辨別,且移除所有在down中設定的檢測,長按等;
下面再回個神,回到view的ontouchevent的action_up:
26到69行:
27行:判斷mprivateflags是否包含prepressed
28行:如果包含pressed或者prepressed則進入執行體,也就是無論是115ms内或者之後擡起都會進入執行體。
36行:如果mhasperformedlongpress沒有被執行,進入if
38行:removelongpresscallback();移除長按的檢測
45-50行:如果mperformclick如果mperformclick為null,初始化一個執行個體,然後立即通過handler添加到消息隊列尾部,如果添加失敗則直接執行 performclick();添加成功,在mperformclick的run方法中就是執行performclick();
終于執行了我們的click事件了,下面看一下performclick()方法:
public boolean performclick() {
sendaccessibilityevent(accessibilityevent.type_view_clicked);
if (monclicklistener != null) {
playsoundeffect(soundeffectconstants.click);
monclicklistener.onclick(this);
return true;
return false;
if (monclicklistener != null) {
monclicklistener.onclick(this);
久違了~我們的monclicklistener ;
别激動,還沒結束,回到action_up,
58行:如果prepressed為true,進入if體:
為mprivateflags設定表示為pressed,重新整理背景,125毫秒後執行munsetpressedstate
否則:munsetpressedstate.run();立即執行;也就是不管咋樣,最後munsetpressedstate.run()都會執行;
看看這個unsetpressedstate主要幹什麼:
private final class unsetpressedstate implements runnable {
public void run() {
setpressed(false);
public void setpressed(boolean pressed) {
if (pressed) {
mprivateflags |= pressed;
} else {
mprivateflags &= ~pressed;
refreshdrawablestate();
dispatchsetpressed(pressed);
把我們的mprivateflags中的pressed取消,然後重新整理背景,把setpress轉發下去。
action_up的最後一行:removetapcallback(),如果mpendingcheckfortap不為null,移除;
好了,代碼跨度還是相當大的,下面需要總結下:
1、整個view的事件轉發流程是:
view.dispatchevent->view.setontouchlistener->view.ontouchevent
在dispatchtouchevent中會進行ontouchlistener的判斷,如果ontouchlistener不為null且傳回true,則表示事件被消費,ontouchevent不會被執行;否則執行ontouchevent。
2、ontouchevent中的down,move,up
down時:
a、首先設定标志為prepressed,設定mhasperformedlongpress=false ;然後發出一個115ms後的mpendingcheckfortap,如果115ms内擡起手指,觸發了up,則不會觸發click事件,并且最終執行的是unsetpressedstate對象,setpressed(false)将setpress的傳遞下去;這種情況很少發生,可能隻會在壓力測試的時候會發現無法觸發click事件;
b、如果115ms内沒有觸發up,則将标志置為pressed,清除prepressed标志,同時發出一個延時為500-115ms的,檢測長按任務消息;
c、如果500ms内(從down觸發開始算),則會觸發longclicklistener:
此時如果longclicklistener不為null,則會執行回調,同時如果longclicklistener.onclick傳回true,才把mhasperformedlongpress設定為true;否則mhasperformedlongpress依然為false;
move時:
主要就是檢測使用者是否劃出控件,如果劃出了:
115ms内,直接移除mpendingcheckfortap;
115ms後,則将标志中的pressed去除,同時移除長按的檢查:removelongpresscallback();
up時:
a、如果115ms内,觸發up,此時标志為prepressed,則執行unsetpressedstate,setpressed(false);會把setpress轉發下去,可以在view中複寫dispatchsetpressed方法接收;
b、如果是115ms-500ms間,即長按還未發生,則首先移除長按檢測,執行onclick回調;
c、如果是500ms以後,那麼有兩種情況:
i.設定了onlongclicklistener,且onlongclicklistener.onclick傳回true,則點選事件onclick事件無法觸發;
ii.沒有設定onlongclicklistener或者onlongclicklistener.onclick傳回false,則點選事件onclick事件依然可以觸發;
d、最後執行munsetpressedstate.run(),将setpressed傳遞下去,然後将pressed辨別去除;
最後問個問題,然後再運作個例子結束:
1、setonlongclicklistener和setonclicklistener是否隻能執行一個
不是的,隻要setonlongclicklistener中的onclick傳回false,則兩個都會執行;傳回true則會螢幕setonclicklistener
最後我們給mybutton同時設定setonclicklistener和setonlongclicklistener,運作看看:
import android.view.view.onclicklistener;
import android.view.view.onlongclicklistener;
import android.widget.toast;
mbutton.setonclicklistener(new onclicklistener()
public void onclick(view v)
toast.maketext(getapplicationcontext(), "onclick",toast.length_short).show();
mbutton.setonlongclicklistener(new onlongclicklistener()
public boolean onlongclick(view v)
toast.maketext(getapplicationcontext(), "setonlongclicklistener",toast.length_short).show();
效果圖:
可以看到longclicklistener已經clicklistener都觸發了~
最後,本篇博文完成了對view的事件分發機制的整個流程的說明,并且對源碼進行了分析;