天天看點

android viewgroup事件分發機制 1、案例 2、源碼分析 3、關于攔截 4、如果沒有找到合适的子View 5、總結

今天給大家代碼viewgroup事件分發的源碼解析~~凡是自定義viewgroup實作各種滑動效果的,不可避免的會出現很多事件的沖突,對viewgroup事件分發機制的了解,也有益于大家了解沖突産生的原因,以及對沖突進行處理~

首先我們接着上一篇的代碼,在代碼中添加一個自定義的linearlayout:

package com.example.zhy_event03;  

import android.content.context;  

import android.util.attributeset;  

import android.util.log;  

import android.view.motionevent;  

import android.widget.linearlayout;  

public class mylinearlayout extends linearlayout  

{  

    private static final string tag = mylinearlayout.class.getsimplename();  

    public mylinearlayout(context context, attributeset attrs)  

    {  

        super(context, attrs);  

    }  

    @override  

    public boolean dispatchtouchevent(motionevent ev)  

        int action = ev.getaction();  

        switch (action)  

        {  

        case motionevent.action_down:  

            log.e(tag, "dispatchtouchevent action_down");  

            break;  

        case motionevent.action_move:  

            log.e(tag, "dispatchtouchevent action_move");  

        case motionevent.action_up:  

            log.e(tag, "dispatchtouchevent action_up");  

        default:  

        }  

        return super.dispatchtouchevent(ev);  

    public boolean ontouchevent(motionevent event)  

        int action = event.getaction();  

            log.e(tag, "ontouchevent action_down");  

            log.e(tag, "ontouchevent action_move");  

            log.e(tag, "ontouchevent action_up");  

        return super.ontouchevent(event);  

    public boolean onintercepttouchevent(motionevent ev)  

            log.e(tag, "onintercepttouchevent action_down");  

            log.e(tag, "onintercepttouchevent action_move");  

            log.e(tag, "onintercepttouchevent action_up");  

        return super.onintercepttouchevent(ev);  

    public void requestdisallowintercepttouchevent(boolean disallowintercept)  

        log.e(tag, "requestdisallowintercepttouchevent ");  

        super.requestdisallowintercepttouchevent(disallowintercept);  

}  

繼承linearlayout,然後複寫了與事件分發機制有關的代碼,添加上了日志的列印~

然後看我們的布局檔案:

<com.example.zhy_event03.mylinearlayout 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" />  

</com.example.zhy_event03.mylinearlayout>  

mylinearlayout中包含一個mybutton,mybutton都上篇部落格中已經出現過,這裡就不再貼代碼了,不清楚可以去檢視~

然後mainactivity就是直接加載布局,沒有任何代碼~~~

直接運作我們的代碼,然後點選我們的button,依然是有意的move一下,不然不會觸發move事件,看一下日志的輸出:

09-06 09:57:27.287: e/mylinearlayout(959): dispatchtouchevent action_down  

09-06 09:57:27.287: e/mylinearlayout(959): onintercepttouchevent action_down  

09-06 09:57:27.287: e/mybutton(959): dispatchtouchevent action_down  

09-06 09:57:27.297: e/mybutton(959): ontouchevent action_down  

09-06 09:57:27.297: e/mybutton(959): ontouchevent action_move  

09-06 09:57:27.327: e/mylinearlayout(959): dispatchtouchevent action_move  

09-06 09:57:27.327: e/mylinearlayout(959): onintercepttouchevent action_move  

09-06 09:57:27.337: e/mybutton(959): dispatchtouchevent action_move  

09-06 09:57:27.337: e/mybutton(959): ontouchevent action_move  

09-06 09:57:27.457: e/mylinearlayout(959): dispatchtouchevent action_up  

09-06 09:57:27.457: e/mylinearlayout(959): onintercepttouchevent action_up  

09-06 09:57:27.457: e/mybutton(959): dispatchtouchevent action_up  

09-06 09:57:27.457: e/mybutton(959): ontouchevent action_up  

可以看到大體的事件流程為:

mylinearlayout的dispatchtouchevent -> mylinearlayout的onintercepttouchevent -> mybutton的dispatchtouchevent ->mybutton的ontouchevent 

可以看出,在view上觸發事件,最先捕獲到事件的為view所在的viewgroup,然後才會到view自身~

下面我們按照日志的輸出,進入源碼~

viewgroup - dispatchtouchevent

首先是viewgroup的dispatchtouchevent方法:

@override  

   public boolean dispatchtouchevent(motionevent ev) {  

       if (!onfiltertoucheventforsecurity(ev)) {  

           return false;  

       }  

       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;  

       boolean disallowintercept = (mgroupflags & flag_disallow_intercept) != 0;  

       if (action == motionevent.action_down) {  

           if (mmotiontarget != null) {  

               // this is weird, we got a pen down, but we thought it was  

               // already down!  

               // xxx: we should probably send an action_up to the current  

               // target.  

               mmotiontarget = null;  

           }  

           // if we're disallowing intercept or if we're allowing and we didn't  

           // intercept  

           if (disallowintercept || !onintercepttouchevent(ev)) {  

               // reset this event's action (just to protect ourselves)  

               ev.setaction(motionevent.action_down);  

               // we know we want to dispatch the event down, find a child  

               // who can handle it, start with the front-most child.  

               final int scrolledxint = (int) scrolledxfloat;  

               final int scrolledyint = (int) scrolledyfloat;  

               final view[] children = mchildren;  

               final int count = mchildrencount;  

               for (int i = count - 1; i >= 0; i--) {  

                   final view child = children[i];  

                   if ((child.mviewflags & visibility_mask) == visible  

                           || child.getanimation() != null) {  

                       child.gethitrect(frame);  

                       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;  

                           if (child.dispatchtouchevent(ev))  {  

                               // event handled, we have a target now.  

                               mmotiontarget = child;  

                               return true;  

                           }  

                           // the event didn't get handled, try the next view.  

                           // don't reset the event's location, it's not  

                           // necessary here.  

                       }  

                   }  

               }  

       }                                                                                                                                                  ....//other code omitted  

代碼比較長,決定分段貼出,首先貼出的是action_down事件相關的代碼。

16行:進入action_down的處理

17-23行:将mmotiontarget置為null

26行:進行判斷:if(disallowintercept || !onintercepttouchevent(ev))

兩種可能會進入if代碼段

1、目前不允許攔截,即disallowintercept =true,

2、目前允許攔截但是不攔截,即disallowintercept =false,但是onintercepttouchevent(ev)傳回false ;

注:disallowintercept 可以通過viewgroup.requestdisallowintercepttouchevent(boolean);進行設定,後面會詳細說;而onintercepttouchevent(ev)可以進行複寫。

36-57行:開始周遊所有的子view

true;

viewgroup的action_down分析結束,總結一下:

viewgroup實作捕獲到down事件,如果代碼中不做touch事件攔截,則開始查找目前x,y是否在某個子view的區域内,如果在,則把事件分發下去。

按照日志,接下來到達action_move

首先我們源碼進行删減,隻留下move相關的代碼:

      //...action_down  

      //...actin_up or action_cancel  

       // the event wasn't an action_down, dispatch it to our target if  

       // we have one.  

final view target = mmotiontarget;  

       // if have a target, see if we're allowed to and want to intercept its  

       // events  

       if (!disallowintercept && onintercepttouchevent(ev)) {  

           //....  

       // 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);  

       return target.dispatchtouchevent(ev);  

   }  

18行:把action_down時指派的mmotiontarget,付給target ; 

23行:if (!disallowintercept && onintercepttouchevent(ev)) 目前允許攔截且攔截了,才進入if體,當然了預設是不會攔截的~這裡執行了onintercepttouchevent(ev)

28-30行:把坐标系統轉化為子view的坐标系統

32行:直接return target.dispatchtouchevent(ev); 

可以看到,正常流程下,action_move在檢測完是否攔截以後,直接調用了子view.dispatchtouchevent,事件分發下去;

最後就是action_up了

public boolean dispatchtouchevent(motionevent ev) {  

       if (action == motionevent.action_down) {...}  

boolean isuporcancel = (action == motionevent.action_up) ||  

               (action == motionevent.action_cancel);  

if (isuporcancel) {  

           mgroupflags &= ~flag_disallow_intercept;  

if(target ==null ){...}  

if (!disallowintercept && onintercepttouchevent(ev)) {...}  

       if (isuporcancel) {  

           mmotiontarget = null;  

17行:判斷目前是否是action_up

21,28行:分别重置攔截标志位以及将down指派的mmotiontarget置為null,都up了,當然置為null,下一次down還會再指派的~

最後,修改坐标系統,然後調用target.dispatchtouchevent(ev);

正常情況下,即我們上例整個代碼的流程我們已經走完了:

1、action_down中,viewgroup捕獲到事件,然後判斷是否攔截,如果沒有攔截,則找到包含目前x,y坐标的子view,指派給mmotiontarget,然後調用

mmotiontarget.dispatchtouchevent

2、action_move中,viewgroup捕獲到事件,然後判斷是否攔截,如果沒有攔截,則直接調用mmotiontarget.dispatchtouchevent(ev)

3、action_up中,viewgroup捕獲到事件,然後判斷是否攔截,如果沒有攔截,則直接調用mmotiontarget.dispatchtouchevent(ev)

當然了在分發之前都會修改下坐标系統,把目前的x,y分别減去child.left 和 child.top ,然後傳給child;

上面的總結都是基于:如果沒有攔截;那麼如何攔截呢?

複寫viewgroup的onintercepttouchevent方法:

            //如果你覺得需要攔截  

            return true ;   

        return false;  

預設是不攔截的,即傳回false;如果你需要攔截,隻要return true就行了,這要該事件就不會往子view傳遞了,并且如果你在down retrun true ,則down,move,up子view都不會捕獲事件;如果你在move return true , 則子view在move和up都不會捕獲事件。

原因很簡單,當onintercepttouchevent(ev) return true的時候,會把mmotiontarget 置為null ; 

如果viewgroup的onintercepttouchevent(ev) 當action_move時return true ,即攔截了子view的move以及up事件;

此時子view希望依然能夠響應move和up時該咋辦呢?

android給我們提供了一個方法:requestdisallowintercepttouchevent(boolean) 用于設定是否允許攔截,我們在子view的dispatchtouchevent中直接這麼寫:

    public boolean dispatchtouchevent(motionevent event)  

        getparent().requestdisallowintercepttouchevent(true);    

        return super.dispatchtouchevent(event);  

getparent().requestdisallowintercepttouchevent(true);  這樣即使viewgroup在move的時候return true,子view依然可以捕獲到move以及up事件。

從源碼也可以解釋:

viewgroup move和up攔截的源碼是這樣的:

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)) {  

                // target didn't handle action_cancel. not much we can do  

                // but they should have.  

            }  

            // 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;  

當我們把disallowintercept設定為true時,!disallowintercept直接為false,于是攔截的方法體就被跳過了~

注:如果viewgroup在onintercepttouchevent(ev)  action_down裡面直接return true了,那麼子view是木有辦法的捕獲事件的~~~

我們的執行個體,直接點選viewgroup内的按鈕,當然直接很順利的走完整個流程;

但是有兩種特殊情況

1、action_down的時候,子view.dispatchtouchevent(ev)傳回的為false ; 

如果你仔細看了,你會注意到viewgroup的dispatchtouchevent(ev)的action_down代碼是這樣的

if (child.dispatchtouchevent(ev))  {  

                              // event handled, we have a target now.  

                              mmotiontarget = child;  

                              return true;  

                          }  

隻有在child.dispatchtouchevent(ev)傳回true了,才會認為找到了能夠處理目前事件的view,即mmotiontarget = child;

但是如果傳回false,那麼mmotiontarget 依然是null

mmotiontarget 為null會咋樣呢?

其實viewgroup也是view的子類,如果沒有找到能夠處理該事件的子view,或者幹脆就沒有子view;

那麼,它作為一個view,就相當于view的事件轉發了~~直接super.dispatchtouchevent(ev);

源碼是這樣的:

       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);  

我們沒有一個能夠處理該事件的目标元素,意味着我們需要自己處理~~~就相當于傳統的view~

2、那麼什麼時候子view.dispatchtouchevent(ev)傳回的為true

如果你仔細看了上篇部落格,你會發現隻要子view支援點選或者長按事件一定傳回true~~

if (((viewflags & clickable) == clickable ||  

                (viewflags & long_clickable) == long_clickable)) {     

             return true ;                                                                                                                                                                                                                                                                                                   }  

關于代碼流程上面已經總結過了~

1、如果viewgroup找到了能夠處理該事件的view,則直接交給子view處理,自己的ontouchevent不會被觸發;

2、可以通過複寫onintercepttouchevent(ev)方法,攔截子view的事件(即return true),把事件交給自己處理,則會執行自己對應的ontouchevent方法

3、子view可以通過調用getparent().requestdisallowintercepttouchevent(true);  阻止viewgroup對其move或者up事件進行攔截;

好了,那麼實際應用中能解決哪些問題呢?

比如你需要寫一個類似slidingmenu的左側隐藏menu,主activity上有個button、listview或者任何可以響應點選的view,你在目前view上死命的滑動,菜單欄也出不來;因為move事件被子view處理了~

你需要這麼做:在viewgroup的dispatchtouchevent中判斷使用者是不是想顯示菜單,如果是,則在onintercepttouchevent(ev)攔截子view的事件;自己進行處理,這樣自己的ontouchevent就可以順利展現出菜單欄了~~

繼續閱讀