今天給大家代碼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就可以順利展現出菜單欄了~~