Android View之事件分发

Android事件分发机制详解
本文深入解析Android的事件分发机制,包括Activity、ViewGroup和View之间的事件传递流程,探讨事件分发、拦截和处理的原理,以及如何解决滑动冲突等问题。

Android view的事件分发机制是开发中的一个重点,因此掌握它的真正意义是非常必要的。

一.什么是view的事件分发机制呢?

将点击事件传递到具体某个view处理的整个过程,就叫做事件分发。


二.为什么要有事件分发呢?

因为Android的view是树形结构的,view可能会重叠,当点击的地方多的时候,为了解决点击事件传递给谁的时候,就要用到事件分发了,因此也可以看出,事件分发采用的是责任链的设计模式。


三.事件分发的传递对象是哪些?

主要是Activity->ViewGroup->View的传递过程。


四.事件分发中的三个重要方法?

1.public boolean dispatchTouchEvent(MotionEvent ev)

事件传递方法,把事件依照顺序往下传递。

2.public boolean onInterceptTouchEvent(MotionEvent ev)

事件拦截方法,该方法是在事件分发的 dispatchTouchEvent 方法内部进行调用。是用来判断在触摸事件传递过程中,是否拦截某个 事件。如果方法true表示将时间拦截,交给当前view的onTouchEvent进行处理,如果返回false不拦截,继续往下传递。

3.public boolean onTouchEvent(MotionEvent ev)

事件响应方法,这是用来处理具体事件的,在dispatchTouchEvent中调用。


五.了解了事件分发的一些基本概念后,就开始根据事件传递对象的流程来一步步分析事件的分发过程:

1.Activiy事件分发过程:

首先按下一个页面的按钮后,事件第一个传递的对象就是Activity,

因此先分析事件是怎么到达Activity又是怎么往下传递的?

写个touch事件来看下调用栈:


我们知道Android采用handler的消息机制,消息存在messagequeue消息队列中,同过loope轮询的方式来调用消息,我们从调用栈中的最下面的红框中看到,当我们按下按钮时,nativePollOnce()会收到消息,并将事件发送给InputEventReceiverdispatchInputEvent()方法,然后继续往下传,流程是这样的:

nativePollOnce(收到消息)->InputEventReceiver(的dispatchInputEvent)-> ViewRootImpl(view的根节点)的WindowInputEventReceiver

->Activity的dispatchTouchEvent。

到达Activity,接下来分析,事件由activity到达view的过程:


这个过程我们从调用栈中就可以很清楚的看出来,流程是这样的:

Activity(dispatchTouchEvent)-> PhoneWindow(superDispatchTouchEvent) -> DecorView(superDispatchTouchEvent) -> ViewGroup(dispatchTouchEvent)


为什么中间会经过PhoneWindow和DecorView呢?

因为Activity中持有一个Window对象,Window中包含一个PhoneWindow实例,PhoneWindow中又持有一个DecorView对象。

2.ViewGroup事件分发过程:

我们通过一个例子来看下事件传递过程,以下是自定义的一个 ViewGroup:

public class CustomLinearLayout extends LinearLayout {
private final String TAG = "CustomLinearLayout";

public CustomLinearLayout(Context context) {
super(context);
}

public CustomLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}

public CustomLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.

ACTION_DOWN
:
Log.
e
(TAG,"onTouchEvent ACTION_DOWN");
break;
case MotionEvent.
ACTION_UP
:
// Log.e(TAG,"onTouchEvent ACTION_UP");
break;
}

return super.onTouchEvent(event);
}

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.
ACTION_DOWN
:
Log.
e
(TAG,"dispatchTouchEvent ACTION_DOWN");
break;
case MotionEvent.
ACTION_UP
:
// Log.e(TAG,"dispatchTouchEvent ACTION_UP");
break;
}
return super.dispatchTouchEvent(ev);
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.
ACTION_DOWN
:
Log.
e
(TAG,"onInterceptTouchEvent ACTION_DOWN");
break;
case MotionEvent.
ACTION_UP
:
// Log.e(TAG,"onInterceptTouchEvent ACTION_UP");
break;
}
return super.onInterceptTouchEvent(ev);
}

}

使用这个自定义 ViewGroup:

<com.example.linwenbing.demo.CustomLinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
>
<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="50dp"
android:text="点击"
/>

</com.example.linwenbing.demo.CustomLinearLayout>

点击button:

btn.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()){
case MotionEvent.

ACTION_DOWN
:
Log.
e
(TAG,"onTouch ACTION_DOWN");
break;
}
return false;
}
});


看下log的打印:


根据log的打印我们知道,当我们点击button时首先调用ViewGroup的调用流程是:

ViewGroup的dispatchTouchEvent->ViewGroup的onInterceptTouchEvent->子view的dispatchTouchEvent

现在我们在ViewGroup中对事件进行拦截,即在onInterceptTouchEvent方法中返回true:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.

ACTION_DOWN
:
Log.
e
(TAG,"onInterceptTouchEvent ACTION_DOWN");
break;
case MotionEvent.
ACTION_UP
:
// Log.e(TAG,"onInterceptTouchEvent ACTION_UP");
break;
}
return true;


这时看下log的打印:

调用流程:

ViewGroup的dispatchTouchEvent->ViewGroup的onInterceptTouchEvent->ViewGroup的onTouchEvent

说明拦截事件后,事件就不往子view中传递了,就在ViewGroup的onTouchEvent进行处理。这就很好解释了三个方法的作用:

dispatchTouchEvent:首先调用的方法,对事件进行分发

onInterceptTouchEvent:拦截事件,如果拦截返回true,并执行改VierGroup的onTouchEvent,如果返回false则传给下一个view.

onTouchEvent:对事件的处理

搞懂了流程接下来我们来看下源码,这里主要看部分重要的代码:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mInputEventConsistencyVerifier != null) {

mInputEventConsistencyVerifier.onTouchEvent(ev, 1);

}...

// Handle an initial down.
if (actionMasked == MotionEvent.

ACTION_DOWN
) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();

}

首先我们看dispatchTouchEvent方法中,view按下时的这两个方法,因为

ACTION_DOWN是事件的开始,所以
cancelAndClearTouchTargets这个方法是进行初始化,而resetTouchState是重置触摸状态,全新开始。

接下来看:

final boolean intercepted;
if (actionMasked == MotionEvent.

ACTION_DOWN

|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags &
FLAG_DISALLOW_INTERCEPT
) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;

}


这里判断是否拦截事件,这里intercepted = onInterceptTouchEvent(ev);用来敷值,onInterceptTouchEvent默认返回false,不拦截的,所以我们可以重写onInterceptTouchEvent来对事件进行拦截.

if (actionMasked == MotionEvent.

ACTION_DOWN

|| (split && actionMasked == MotionEvent.
ACTION_POINTER_DOWN
)
|| actionMasked == MotionEvent.
ACTION_HOVER_MOVE
) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.
ALL_POINTER_IDS
;

// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
removePointersFromTouchTargets(idBitsToAssign);

final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child =
getAndVerifyPreorderedView
(
preorderedList, children, childIndex);

// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}

if (!
canViewReceivePointerEvents
(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}

newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}

resetCancelNextUpFlag
(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;

break;


这一大段代码主要就是当ViewGroup不拦截事件时,把事件分发到子view,循环遍历完所有的子view后,如果点击事件都没有被消耗,ViewGroup就会自己处理点击事件,如下代码:

// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.

ALL_POINTER_IDS
);

} else {


3.View事件分发过程:

这里我们说的view就是最后一层的子view了,所以并不存在拦截不往下分发的方法,这里我们主要分析onTouch中处理事件的分发过程:

btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.

e
(TAG,"onClick");
}
});

btn.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()){
case MotionEvent.
ACTION_DOWN
:
Log.
e
(TAG,"onTouch ACTION_DOWN");
break;
case MotionEvent.
ACTION_MOVE
:
Log.
e
(TAG,"onTouch ACTION_MOVE");
break;
case MotionEvent.
ACTION_UP
:
Log.
e
(TAG,"onTouch ACTION_UP");
break;
}
return false;
}
});


上面的代码我们看一下log的执行(这里点击按钮的时候有稍微移动下):


我们看到执行流程是:

ACTION_DOWN->ACTION_MOVE>ACTION_UP>onClick

可以看到ACTION_DOWN最开始执行onClick最后执行,如果我们在onTouch中返回true会发现onClick就不执行了。

这里源码不做具体分析,看源码的时候主要发现MotionEvent中有个MotionEvent.ACTION_CANCEL需要注意一下:

case MotionEvent.

ACTION_CANCEL
:
if (clickable) {
setPressed(false);
}
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
mPrivateFlags3 &= ~
PFLAG3_FINGER_DOWN
;

break;

MotionEvent.ACTION_CANCEL官方分析:

当你的手指(或者其它)移动屏幕的时候会触发这个事件,比如当你的手指在屏幕上拖动一个listView或者一个ScrollView而不是去按上面的按钮时会触发这个事件。



一个解决冲突的重要方法使用说明:

requestDisallowInterceptTouchEvent方法:

requestDisallowInterceptTouchEvent方法用于影响父元素的事件拦截策略,requestDisallowInterceptTouchEvent(true),表示不允许父元素拦截事件,这样事件就会传递给子View。一般这个方法子View用的多,可以用来处理滑动冲突问题。

如:

(1)在子View的dispatchTouchEvent方法中,对于ACTION_DOWN事件,通过调用requestDisallowInterceptTouchEvent(true)默认不允许父布局拦截事件,这样后续事件都交给子View处理。

(2)在子View的dispatchTouchEvent方法中,对于ACTION_MOVE事件,默认是子View处理,在需要父布局处理时,调用requestDisallowInterceptTouchEvent(false)方法来让父布局拦截事件,交给父布局处理。


转载于:https://juejin.im/post/5c26e1a86fb9a04a0604f1c2

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值