上一篇我們介紹了View的事件分發(fā)機(jī)制淤翔,不熟悉的可以先了解一下
上一篇:Android事件分發(fā)機(jī)制(View篇)
引言
本篇我們接著上一篇,來繼續(xù)學(xué)習(xí)一下Android ViewGroup的事件分發(fā)機(jī)制
本來View的事件分發(fā)機(jī)制和ViewGroup的事件分發(fā)機(jī)制是緊密聯(lián)系在一起的胆屿,但是因?yàn)槠渲械脑聿皇侨齼删淠軌蛘f清楚的,也為了方便理解偶宫,就先拆開來講非迹,然后融合起來統(tǒng)一歸納總結(jié),這樣結(jié)構(gòu)更清晰纯趋,好了憎兽,廢話不多,我們進(jìn)入正文吵冒。
正文
本篇ViewGroup的事件分發(fā)機(jī)制的場景和上一篇Android事件分發(fā)機(jī)制(View篇)開始的場景是一樣的纯命,這里不再重復(fù)。
上篇也提到過對于ViewGroup我們關(guān)注三個方法:
ViewGroup 三個方法:
-
dispatchTouchEvent (
MotionEvent event
)//負(fù)責(zé)事件分發(fā) -
onInterCeptTouchEvent(
MotionEvent event
)//處理是否攔截當(dāng)前事件 -
onTouchEvent(
MotionEvent event
)//當(dāng)前View自己處理當(dāng)前事件
我們先新建一個project: ViewGroupDemo
- 新建一個MyLinearLayout
繼承LinearLayout
重寫dispatchTouchEvent痹栖、onInterceptTouchEvent亿汞、onTouchEvent
三個方法并打印log
/**
* @author Charay
* @data 2017/10/31
*/
public class MyLinearLayout extends LinearLayout {
private static final String TAG = MyLinearLayout.class.getSimpleName();
public MyLinearLayout(Context context) {
super(context);
}
public MyLinearLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"--MyLinearLayout--dispatchTouchEvent---ACTION_DOWN---");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG,"--MyLinearLayout--dispatchTouchEvent---ACTION_MOVE---");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG,"--MyLinearLayout--dispatchTouchEvent---ACTION_UP---");
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"--MyLinearLayout--onInterceptTouchEvent---ACTION_DOWN---");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG,"--MyLinearLayout--onInterceptTouchEvent---ACTION_MOVE---");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG,"--MyLinearLayout--onInterceptTouchEvent---ACTION_UP---");
break;
}
return super.onInterceptTouchEvent(ev);
// return true;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"--MyLinearLayout--onTouchEvent---ACTION_DOWN---");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG,"--MyLinearLayout--onTouchEvent---ACTION_MOVE---");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG,"--MyLinearLayout--onTouchEvent---ACTION_UP---");
break;
}
return super.onTouchEvent(event);
}
}
- 新建一個MyButton
繼承Button
重寫dispatchTouchEvent、onTouchEvent
兩個方法并打印log
/**
* @author Charay
* @data 2017/10/31
*/
public class MyButton extends Button {
private static final String TAG = MyButton.class.getSimpleName();
public MyButton(Context context) {
super(context);
}
public MyButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"--MyButton--dispatchTouchEvent---ACTION_DOWN---");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG,"--MyButton--dispatchTouchEvent---ACTION_MOVE---");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG,"--MyButton--dispatchTouchEvent---ACTION_UP---");
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"--MyButton--onTouchEvent---ACTION_DOWN---");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG,"--MyButton--onTouchEvent---ACTION_MOVE---");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG,"--MyButton--onTouchEvent---ACTION_UP---");
break;
}
return super.onTouchEvent(event);
}
}
在布局文件中添加 MyLinearLayout
和 MyButton
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
>
<com.marco.viewgroupdemo.MyLinearLayout
android:id="@+id/my_linearlayout"
android:layout_width="300dp"
android:layout_height="368dp"
android:background="@color/colorPrimaryDark"
android:gravity="center">
<com.marco.viewgroupdemo.MyButton
android:id="@+id/my_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/colorAccent"
android:text="按鈕"
/>
</com.marco.viewgroupdemo.MyLinearLayout>
</LinearLayout>
在MainActivity
中初始化 MyLinearLayout
和 MyButton
并添加setOnTouchListener
,然后打印log
public class MainActivity extends Activity {
private static final String TAG = MainActivity.class.getSimpleName();
private MyLinearLayout mMyLinearLayout;
private MyButton mMyButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mMyLinearLayout = (MyLinearLayout) findViewById(R.id.my_linearlayout);
mMyButton = (MyButton) findViewById(R.id.my_button);
mMyLinearLayout.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
switch (motionEvent.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"--MyLinearLayout--onTouch---ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG,"--MyLinearLayout--onTouch---ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG,"--MyLinearLayout--onTouch---ACTION_UP");
break;
}
return false;
}
});
mMyButton.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
switch (motionEvent.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"--mMyButton--onTouch---ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG,"--mMyButton--onTouch---ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG,"--mMyButton--onTouch---ACTION_UP");
break;
}
return false;
}
});
}
}
為了能完整看到事件分發(fā)攔截的整個流程揪阿,我們在上面代碼中沒有更改任何一個方法的返回值疗我,只是打印了log
現(xiàn)在我們運(yùn)行程序,點(diǎn)擊MyButton
,打印日志log如下:
11-01 11:54:02.993 E/MyLinearLayout: --MyLinearLayout--dispatchTouchEvent---ACTION_DOWN---
11-01 11:54:02.993 E/MyLinearLayout: --MyLinearLayout--onInterceptTouchEvent---ACTION_DOWN---
11-01 11:54:02.993 E/MyButton: --MyButton--dispatchTouchEvent---ACTION_DOWN---
11-01 11:54:02.994 E/MainActivity: --mMyButton--onTouch---ACTION_DOWN
11-01 11:54:02.994 E/MyButton: --MyButton--onTouchEvent---ACTION_DOWN---
11-01 11:54:03.004 E/MyLinearLayout: --MyLinearLayout--dispatchTouchEvent---ACTION_MOVE---
11-01 11:54:03.004 E/MyLinearLayout: --MyLinearLayout--onInterceptTouchEvent---ACTION_MOVE---
11-01 11:54:03.004 E/MyButton: --MyButton--dispatchTouchEvent---ACTION_MOVE---
11-01 11:54:03.005 E/MainActivity: --mMyButton--onTouch---ACTION_MOVE
11-01 11:54:03.005 E/MyButton: --MyButton--onTouchEvent---ACTION_MOVE---
11-01 11:54:03.028 E/MyLinearLayout: --MyLinearLayout--dispatchTouchEvent---ACTION_UP---
11-01 11:54:03.028 E/MyLinearLayout: --MyLinearLayout--onInterceptTouchEvent---ACTION_UP---
11-01 11:54:03.028 E/MyButton: --MyButton--dispatchTouchEvent---ACTION_UP---
11-01 11:54:03.028 E/MainActivity: --mMyButton--onTouch---ACTION_UP
11-01 11:54:03.029 E/MyButton: --MyButton--onTouchEvent---ACTION_UP---
雖然日志比較長南捂,但是不要怕吴裤,上面把ACTION_DOWN、ACTION_MOVE溺健、ACTION_UP
都打印了出來麦牺,
我們只需要看一組ACTION_DOWN
即可,因?yàn)橐粋€View
一旦消費(fèi)了ACTION_DOWN
事件,那么其他兩個事件一定都是這個View
消費(fèi)枕面。
從log
中我們發(fā)現(xiàn)ACTION_DOWN愿卒、ACTION_MOVE缚去、ACTION_UP
三個手勢動作規(guī)律是一樣的潮秘,執(zhí)行順序都是從最外層ViewGroup
向內(nèi)層View
(或ViewGroup
傳遞):先是MyLinearLayout
的dispatchTouchEvent
在這個方法中先執(zhí)行onInterceptTouchEvent
判斷事都攔截這個事件,如果不攔截(默認(rèn)不攔截)就傳遞給MyButton
,然后事件分發(fā)給MyButton
的dispatchTouchEvent
易结,執(zhí)行MyButton
的dispatchTouchEvent
枕荞,由于MyButton
及其父View
沒有onInterceptTouchEvent
方法,所以直接在dispatchTouchEvent
中先判斷onTouch
的返回值搞动,默認(rèn)為false
躏精,再執(zhí)行MyButton
的onTouchEvent
如果我們在MyLinearLayout中攔截了這個事件結(jié)果將是怎樣呢?
下面我們在MyLinearLayout中的重寫的onInterceptTouchEvent中把返回值改為true
onInterceptTouchEvent
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
Log.e(TAG,"--MyLinearLayout--onInterceptTouchEvent---ACTION_DOWN---");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG,"--MyLinearLayout--onInterceptTouchEvent---ACTION_MOVE---");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG,"--MyLinearLayout--onInterceptTouchEvent---ACTION_UP---");
break;
}
// return super.onInterceptTouchEvent(ev);
return true;
}
然后運(yùn)行程序分別點(diǎn)擊MyButton和MyLinearLayout,發(fā)現(xiàn)打印的log是一樣的:
//點(diǎn)擊MyButton
11-02 11:07:14.207 E/MyLinearLayout: --MyLinearLayout--dispatchTouchEvent---ACTION_DOWN---
11-02 11:07:14.207 E/MyLinearLayout: --MyLinearLayout--onInterceptTouchEvent---ACTION_DOWN---
11-02 11:07:14.207 E/MainActivity: --MyLinearLayout--onTouch---ACTION_DOWN
11-02 11:07:14.207 E/MyLinearLayout: --MyLinearLayout--onTouchEvent---ACTION_DOWN---
//點(diǎn)擊MyLinearLayout
11-02 11:07:21.667 E/MyLinearLayout: --MyLinearLayout--dispatchTouchEvent---ACTION_DOWN---
11-02 11:07:21.677 E/MyLinearLayout: --MyLinearLayout--onInterceptTouchEvent---ACTION_DOWN---
11-02 11:07:21.677 E/MainActivity: --MyLinearLayout--onTouch---ACTION_DOWN
11-02 11:07:21.677 E/MyLinearLayout: --MyLinearLayout--onTouchEvent---ACTION_DOWN---
一分鐘思考一下下面兩個問題:
- 為什么
MyButton
沒有任何有關(guān)log
鹦肿,而且這次還執(zhí)行了MyLinearLayout的onTouch
和onTouchEvent
方法矗烛? - 為什么我們把返回值改為true之前,只執(zhí)行了
MyLinearLayout的dispatchTouchEvent
和onInterceptTouchEvent
箩溃,而沒有執(zhí)行onTouch
和onTouchEvent
方法瞭吃?
下面我們來看一下 ViewGroup
中的源碼(android-10,即2.3.3的源碼)
dispatchTouchEvent
/**
* {@inheritDoc}
*/
@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.
}
}
}
}
}
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
if (isUpOrCancel) {
// Note, we've already copied the previous state to our local
// variable, so this takes effect on the next event
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// The event wasn't an ACTION_DOWN, dispatch it to our target if
// we have one.
final View target = mMotionTarget;
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);
}
// if have a target, see if we're allowed to and want to intercept its
// events
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;
}
if (isUpOrCancel) {
mMotionTarget = null;
}
// 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);
if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
mMotionTarget = null;
}
return target.dispatchTouchEvent(ev);
}
代碼比較多,我們拋開干擾涣旨,只看對我們有用的
先看這行boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
disallowIntercept
的含義是禁用攔截事件,下面看 if (disallowIntercept || !onInterceptTouchEvent(ev))
判斷,
進(jìn)入判斷條件的情況有兩種:
-
disallowIntercept
為true
,即禁用攔截事件歪架,這時候即使攔截事件onInterceptTouchEvent(ev)
返回值為true
,也不會攔截 -
disallowIntercept
為false
霹陡,即允許攔截和蚪,但是不攔截onInterceptTouchEvent(ev)
返回值為false
默認(rèn)情況下是允許攔截的,即disallowIntercept
為false
烹棉,只有當(dāng)我們調(diào)用mMyLinearLayout.requestDisallowInterceptTouchEvent(true);
的時候攒霹,即禁止攔截,disallowIntercept
的值才為true
.
進(jìn)入到if判斷中后浆洗,我們看
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
......
if (child.dispatchTouchEvent(ev)) {
// Event handled, we have a target now.
mMotionTarget = child;
return true;
}
......
}
這時遍歷子View,把事件傳給子View
的dispatchTouchEvent
催束,如果子View
是ViewGroup
,那就繼續(xù)遍歷,直到遍歷到子View
是View
類型的辅髓,然后調(diào)用調(diào)用這個子View
的dispatchTouchEvent
泣崩,之后的判斷就干我們上一篇的Android事件分發(fā)機(jī)制(View篇)原理一致了。
而我們剛才的代碼中把onInterceptTouchEvent(ev)
返回值改為true
洛口,則不符合上面的兩種情況矫付,因此進(jìn)入不到if判斷中,事件被攔截了第焰,因此事件到不了MyButton
的dispatchTouchEvent
方法买优,也就不會有MyButton
的任何有log
。
下面我們看這幾行代碼:
// The event wasn't an ACTION_DOWN, dispatch it to our target if
// we have one.
final View target = mMotionTarget;
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);
}
雖然我們在onInterceptTouchEvent(ev)
中返回true
攔截了這個事件,但這并不代表我們當(dāng)前MyLinearLayout
就是消費(fèi)當(dāng)前事件的消費(fèi)者杀赢,因?yàn)槲覀儧]有target
烘跺,所以進(jìn)入了上面代碼中,最后一行return super.dispatchTouchEvent(ev);
即進(jìn)入了View
的dispatchTouchEvent(ev)
脂崔,同樣跟我們上一篇Android View的事件分發(fā)機(jī)制(上)原理一致滤淳,雖然和上面for循環(huán)中一樣也是在View
中,但是是有區(qū)別的砌左,這個是MyLinearLayout
這個(ViewGroup)
繼承的View
,其操作是針對MyLinearLayout
的脖咐,即之后所走的onTouch
和onTouchEvent
也都是針對MyLinearLayout
的;而for
循環(huán)中的是MyButton
這個(View)
繼承的View
汇歹,其操作是針對MyMutton
的屁擅,即之后所走的onTouch
和onTouchEvent
也都是針對MyBUtton
的。
既然我們在onInterceptTouchEvent(ev)
中返回true
攔截了這個事件产弹,攔截后派歌,就走到了MyLinearLayout的onTouch
方法,默認(rèn)返回false
痰哨,然后執(zhí)行了MyLinearLayout
的onTouchEvent
方法胶果,所以剛好符合我們上面的log
。
總結(jié)
下面我們總結(jié)一下ViewGroup
的事件分發(fā)機(jī)制的關(guān)鍵點(diǎn)作谭,以及和View的事件分發(fā)機(jī)制的異同
ViewGroup比View多了一個攔截事件的方法onInterceptTouchEvent(ev)
ViewGroup比View中最先執(zhí)行的方法稽物,都是dispatchTouchEvent方法,然后View執(zhí)行了onTouch方法
但是ViewGroup中在執(zhí)行onTouch方法之前多了一個onInterceptTouchEvent(ev)判斷折欠,這個判斷決定了事件在本ViewGroup中消費(fèi)贝或,還是將事件繼續(xù)分發(fā)給它的子View.
而View是沒有onInterceptTouchEvent(ev)的,所以沒有攔截锐秦,事件能不能在View中消費(fèi)掉咪奖,關(guān)鍵是看這個View中的onTouch方法或者onTouchEvent的返回值,如果返回值都是false酱床,那就會將事件返回給其父View的onTouch和onTouchEvent方法羊赵,直到找到一個能消費(fèi)此次事件的ViewGroup。
好了關(guān)與Android中View和ViewGroup的事件分發(fā)機(jī)制兩篇都講完了扇谣,如果有不足或者不理解的地方可在評論中回復(fù)昧捷,我們共同進(jìn)步。
最后附上ViewGroupDemo源碼:github下載