天天看點

android view事件分發機制 1、案例 2、dispatchTouchEvent  3、View的onTouchEvent: 4、總結

首先我們先寫個簡單的例子來測試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();  

效果圖:

android view事件分發機制 1、案例 2、dispatchTouchEvent  3、View的onTouchEvent: 4、總結

可以看到longclicklistener已經clicklistener都觸發了~

最後,本篇博文完成了對view的事件分發機制的整個流程的說明,并且對源碼進行了分析;

繼續閱讀