上一篇文章我們分析了View的事件分發(fā)機制,今天我們分析下ViewGroup的時間分發(fā)機制魄揉。ViewGroup是View的子類剪侮,所以它肯定有繼承部分View的特性,當然它也有自己的特性洛退,二者具體有何不同呢票彪?
例子
我們來舉一個栗子吧~
首先我們自定義Button和自定義LinearLayout,將Button放入在LinearLayout中不狮,下面主要重寫部分方法,添加Log。
TestButton.java
public class TestButton extends Button {
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i("w", "TestButton dispatchTouchEvent-- action=" + event.getAction());
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("w", "TestButton onTouchEvent-- action=" + event.getAction());
return super.onTouchEvent(event);
}
}
TestLinearLayout.java
public class TestLinearLayout extends LinearLayout {
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.i("w", "TestLinearLayout onInterceptTouchEvent-- action=" + ev.getAction());
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i("w", "TestLinearLayout dispatchTouchEvent-- action=" + event.getAction());
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("w", "TestLinearLayout onTouchEvent-- action=" + event.getAction());
return super.onTouchEvent(event);
}
}
main_activity.xml
<io.weimu.caoyang.TestLinearLayout
android:orientation="vertical"
android:gravity="center"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="@+id/mylayout">
<io.weimu.caoyang.TestButton
android:id="@+id/my_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="click test"/>
</com.zzci.light.TestLinearLayout>
MainActivity.java
public class MainActivity.java extends Activity implements View.OnTouchListener, View.OnClickListener {
private TestLinearLayout mLayout;
private TestButton mButton;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mLayout = (TestLinearLayout) this.findViewById(R.id.mylayout);
mButton = (TestButton) this.findViewById(R.id.my_btn);
mLayout.setOnTouchListener(this);
mButton.setOnTouchListener(this);
mLayout.setOnClickListener(this);
mButton.setOnClickListener(this);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.i("w", v+" onTouch-- action="+event.getAction());
return false;
}
@Override
public void onClick(View v) {
Log.i("w", v+" OnClick");
}
}
讓我們看一下打印出來的Log(PS:0為DOWN,1為UP)
例子1:當觸摸事件是在TestLinearLayout內在旱,TestButton內
消息 |
---|
TestLinearLayout dispatchTouchEvent-- action=0 |
TestLinearLayout onInterceptTouchEvent-- action=0 |
TestButton dispatchTouchEvent-- action=0 |
TestButton onTouch-- action=0 |
TestButton onTouchEvent-- action=0 |
TestLinearLayout dispatchTouchEvent-- action=1 |
TestLinearLayout onInterceptTouchEvent-- action=1 |
TestButton dispatchTouchEvent-- action=1 |
TestButton onTouch-- action=1 |
TestButton onTouchEvent-- action=1 |
TestButton onClick |
我們可以看出子View所得到的事件都是由父View派發(fā)所得的摇零。整一個大概流程為:
- 執(zhí)行TestLinearLayout的DispatchTouchEvent
- 執(zhí)行TestLinearLayout的onIntercepTouchEvent
- 執(zhí)行TestButton的disPatchToucheEvent
- 接下去的分發(fā)流程與上一文章一致
例子2:當觸摸事件在TestLinearLayout內,TestButton外
消息 |
---|
TestLinearLayout dispatchTouchEvent-- action=0 |
TestLinearLayout onInterceptTouchEvent-- action=0 |
TestLinearLayout onTouch-- action=0 |
TestLinearLayout onTouchEvent-- action=0 |
TestLinearLayout dispatchTouchEvent-- action=1 |
TestLinearLayout onTouch-- action=1 |
TestLinearLayout onTouchEvent-- action=1 |
TestLinearLayout onClick |
以上可以看到桶蝎,沒有TestButton什么事情了~分發(fā)的流程基本與上面一致驻仅。有一處地方有點奇怪谅畅,就是當action=1(ACTION_UP)時,竟然沒有onInterceptTouchEvent噪服。
ViewGroup和View既有些相似又很大的差異毡泻。View是所有視圖的最小單位,而ViewGroup一般內部都包含著多個View粘优。具體他們有啥區(qū)別呢仇味?接下去我們來分析分析。
源碼解讀:
Step1 ViewGroup
從上面的Log雹顺,我們可以看出ViewGroup事件分發(fā)先觸發(fā)的是dispatchTouchEvent方法(此方法重寫了View的方法)丹墨。此方法代碼較多,代碼會盡量注釋,請耐心閱讀嬉愧。
閱讀前的小準備:
- mFirstTouchTarget:判斷ViewGroup是否已經找到可以傳遞事件的目標組件
- newTouchTarget:新的目標組件
- intercepted:標記ViewGroup是否攔截Touch事件的傳遞
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();//獲取當前的分發(fā)事件
final int actionMasked = action & MotionEvent.ACTION_MASK;
//【Part01】處理初始化按下操作
if (actionMasked == MotionEvent.ACTION_DOWN) {
//重置一切的點擊狀態(tài)
cancelAndClearTouchTargets(ev);
resetTouchState();
}
//【Part02】檢查是否要攔截
final boolean intercepted;
//只要事件為Down,或無傳遞的目標組件
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
//是否不允許攔截(此變量可設置贩挣,默認為false)
if (!disallowIntercept) {
//☆重要的☆
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}
...
//【Part03】檢查取消操作,默認為false
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
//如果需要没酣,更新目標視圖列表
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;//
boolean alreadyDispatchedToNewTouchTarget = false;
//【Part04】子View的掃描
if (!canceled && !intercepted) {
...
//獲取子View的數量
final int childrenCount = mChildrenCount;
//當無傳遞的目標組件王财,且子View數量不為0
if (newTouchTarget == null && childrenCount != 0) {
//獲取當前子View的X,Y
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
//從頭到尾掃描,找到一個可以接受事件分發(fā)的子View裕便,
final ArrayList<View> preorderedList = buildOrderedChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
//for循環(huán)掃描
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder
? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
...
//判斷子View是否為VISIBLE,且判斷觸摸事件是否落在當前子View
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
//獲取新的傳遞控件
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
//☆重要的☆ 【文章下面會分析】
//觸發(fā)子視圖的普通分發(fā)事件或者本身的普通分發(fā)事件
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
...
//☆重要的☆ 增加新的傳遞控件 【文章下面會分析】
//給mFirstTouchTarget賦值
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
//如果沒找到子View可以傳遞事件绒净,則將指針分配給最近添加的對象
if (newTouchTarget == null && mFirstTouchTarget != null) {
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
//part05 分發(fā)事件給傳遞對象
if (mFirstTouchTarget == null) {
//因無傳遞對象,所以直接調用自身的普通傳遞事件
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
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;
//分發(fā)事件到指定的傳遞對象
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;
}
}
//part06 刷新傳遞對象狀態(tài)
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
...
return handled;
}
Part01 處理初始化按下操作
當我們每次按下(ACTION_DOWN)的時候會有初始化操作:cancelAndClearTouchTargets(ev)闪金,resetTouchState()里面有比較重要的操作:將mFirstTouchTarget初始化為null
Part02 檢查是否要攔截
在 if(actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null)的判斷下疯溺,只要事件為ACTION_DOWN或者傳遞事件不為空,就可以進入下一個執(zhí)行體哎垦,否則intercepted = true;在下一個執(zhí)行體中我們調用onInterceptTouchEvent并返回值給intercepted囱嫩。
讓我們看一下onInterceptTouchEvent里寫了什么:
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
默認返回返回false,也就是intercepted = false;
Part03 檢查取消操作,默認為false
通過標記和action檢查cancel漏设,然后將結果賦值給局部boolean變量canceled墨闲。
Part04 子View的掃描
1.當前觸摸事件的x,y,通過for循環(huán)掃描每個子View,判斷子View是否為VISIBLE和x,y是否落在當前子View上,不是的話跳過此循環(huán)郑口,換下一個子View鸳碧。
2.通過getTouchTarget(child);嘗試獲取newTouchTarget
//為指定的子View獲取觸摸對象,如果沒發(fā)現返回空
private TouchTarget getTouchTarget(@NonNull View child) {
for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
//判斷當前target所對應的view是否與傳入的View相同
if (target.child == child) {
return target;
}
}
return null;
}
3.接著調用方法dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)將觸摸事件傳遞給特定的子View!H浴瞻离!此方法重點在于第三個參數,如果傳子View乒裆,內部會調用子View的dispatchTouchEvent進行子View的普通觸摸事件分發(fā)并返回參數套利,而傳入null時,會調用父類的super.dispatchTouchEvent進行父View的普通觸摸事件分發(fā)并返回參數。
在這種情況下肉迫,如果dispatchTransformedTouchEvent為false,即子View沒有消費验辞。為ture,表明子View消費了。
4.當子View消費事件時喊衫,通過addTouchTarget給newTouchTarget賦值跌造,給alreadyDispatchedToNewTouchTarget賦值為true。
//給mFirstTouchTarget賦值W骞骸?翘啊!
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
到這里我們可以稍微總結一下:dispatchTransformedTouchEvent的返回值
return | description | mFirstTouchTarget |
---|---|---|
ture | 事件被消費(子View或自身) | 賦值 |
false | 事件未消費(子View或自身) | null |
5.接下來if (newTouchTarget == null && mFirstTouchTarget != null) 該if表示經過前面的for循環(huán)沒有找到子View接收Touch事件并且之前的mFirstTouchTarget不為空則為真联四,然后newTouchTarget指向了最初的TouchTarget撑碴。
Part05 分發(fā)事件給傳遞對象
經過上面的流程后,接著if (mFirstTouchTarget == null)判斷
mFirstTouchTarget為空時朝墩,表明觸摸事件未被消費醉拓,即觸摸事件被攔截或找不到目標子View。此時調用dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS)內部在調用super.dispatchTouchEvent進行父View的普通觸摸事件分發(fā)
mFirstTouchTarget不為null,依然是遞歸調用dispatchTransformedTouchEvent()方法來實現的處理收苏。
part06 刷新傳遞對象狀態(tài)
當觸摸事件為ACTION_UP或ACTION_HOVER_MOVE時刷新傳遞對象的狀態(tài),mFirstTouchTarget = null;將mFirstTouchTarget置空等操作
Step2 ViewGroup
以上dispatchTouchEvent分析完畢亿卤,此外我們這里在分析下上面經常用到的一個方法dispatchTransformedTouchEvent()
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
//由于重復代碼較多,直接提取精簡代碼
final boolean handled;
if (child == null) {
handled = super.dispatchTouchEvent(event);//父類的普通事件分發(fā)
} else {
handled = child.dispatchTouchEvent(event);//子類的普通事件分發(fā)
}
return handled;
}
這個方法是ViewGroup獨特于View的一個方法鹿霸。我們上面的分析也大概講了一下這個方法:第三個參數有無對象排吴,直接影到了內部的調用【child -> view.dispatchTouchEvent】【null -> super.dispatchTouchEvent】
總結 Summary
- Android事件派發(fā)是先傳遞到最頂級的ViewGroup,再由ViewGroup遞歸傳遞到View的懦鼠。
- 在ViewGroup中可以通過onInterceptTouchEvent方法對事件傳遞進行攔截钻哩,onInterceptTouchEvent方法返回true代表攔截不向子View傳遞,返回false代表不對事件進行攔截肛冶。默認返回false街氢。
- 子View中如果將傳遞的事件消費掉,ViewGroup中將無法接收到任何事件睦袖。
Andorid觸摸事件分發(fā)機制(3)之Activity
PS:本文
整理
自以下文章珊肃,若有發(fā)現問題請致郵 caoyanglee92@gmail.com
工匠若水 Android觸摸屏事件派發(fā)機制詳解與源碼分析二(ViewGroup篇)
Hongyang Android ViewGroup事件分發(fā)機制