Android programmer event distribution mechanism study notes

 

 

To learn something through a problem is a good way. Learning system in Android View event, I give myself by asking questions at the same time solve the problem of them will know principle.

First, let's start a few questions:

  • What is the event? What is an event distribution mechanism?

When we interact with the phone through the screen, each click, press, etc. are a move event. According to object-oriented thinking, one of these events have been packaged in a MotionEvent.

Distribution mechanism that is passed from one event to the screen view of each app View, then View by some of them to use the event or ignore this event, which is to control the whole process of the distribution mechanism.

Note that the event distribution mechanism, the event is in the form of a sequence of events distributed to the View. This sequence begins ACTION_DOWN, after a series of events such as ACTION_MOVE, finally ACTION_UP event ends. All events this sequence, are either ignored, or can only have one event can be used. If the same sequence, such as from press to move this series of actions, different View acceptable, then the entire interface will be very confusing, but the logic is very complicated.

Next, I asked these three questions:

One incident from the screen has been transferred to the general flow of the process on the View What? He said earlier event distribution is actually a sequence of events. So with so many events in a sequence, what is the mechanism only to a View of? OnClick OnLongClick listening when we usually application development, outside of the set to View, where the View is processed?

One problem: the event delivery process like?

Android View is in the tree structure, as shown below:

Android programmer event distribution mechanism study notes

 

Each view contains internal Activity to manage a Window to be displayed. Window and is an abstract class, which is a specific implementation PhoneWindow class. DecovrView PhoneWindow as an internal class, the actual management of the specific display view. He is a subclass of FrameLayout, our full bloom with the title bar and the root view. We wrote some columns View and ViewGroup are managed by him. So when the event distribution, the top of the "Big View" They actually do not have any events operations, they just continue to submit events down until we can use these events.

So, top-down event delivery process should look like this:

The Activity (no treatment) -> Root View -> the ViewGroup one layer (if any) -> child View

If you pass in the end we did not have to deal with the sub-View event how to do it? This time it will backtrack and finally to Activity. Only when the Activity did not deal with this event, this event will be discarded.

The Activity (without discarding process) <- Root View <- one layer at the ViewGroup (if any) <- Sub View

Specifically, when the transfer event is controlled by the following three methods:

  • dispatchTouchEvent: Distribution event onInterceptTouchEvent: intercept events onTouchEvent: consumer event

These three methods have in common is that they specifically whether their function (distribution, interception, consumption) is completely determined by the value of their return, said he would return true complete their function (distribution, interception, consumer ). In addition to the different functions, there is used a scene. dispatchTouchEvent () and onTouchEvent () These two methods, whether or Activity ViewGroup View, will be used. The onInterceptTouchEvent () method because it is only in order to intercept events, Activity and a View at the top level, one in the bottom, but also no need to use. Activity View and therefore is not onInterceptTouchEvent () method.

I was here a few ViewGroup and Custom View, respectively rewrite their methods, time stamped log in rewriting. Look without adding any listener (ie, no consumption View event) conditions the results:

Click External ViewGroup:

Android programmer event distribution mechanism study notes

 

Click on sub-View:

Android programmer event distribution mechanism study notes

 

可以看到,事件分发首先由ViewGroup的dispatchTouchEvent()方法开始,先调用自己的onInterceptTouchEvent()方法判断是否拦截,返回false表示自己没有拦截,那么接下来直接把事件传给子View。子View调用自己的dispatchTouchEvent()方法进行分发,因为View没有onInterceptTouchEvent()方法,所以不存在拦截操作,因此直接将事件交给自己的onTouchEvent()方法消费。因为我的子View没有使用这个事件,因此onTouchEvent()方法直接返回了false表示自己没有消费,那么这个事件此时就算是传到底了。因为自己没有消费,因此自己就没有分发出去,那么子View的dispatchTouchEvent()方法返回false,把这个事件交还给上一层的ViewGroup。ViewGroup发现这个事件没有子View消费,那么就自己动手吧!将事件传给自己的onTouchEvent()方法消费。可是ViewGroup也没有消费,那么onTouchEvent()方法只能是再返回false了。同理,ViewGroup自己没有消费事件,因此他的dispatchTouchEvent()方法也返回了false。这段文字说得可能有点乱,那么就贴一张图来演示一下:(图中红色箭头表示事件自顶向下分发的过程,黄色则表示自底向上返回的过程)

Android programmer event distribution mechanism study notes

 

接下来,我在子View上添加OnClick监听,再看一下点击子View时的运行结果:

Android programmer event distribution mechanism study notes

 

乍一看,呀,怎么重复打印了两遍log?其实并不是哪里写错了。前面我说了,事件分发分发的是一个事件序列,我添加了点击事件,那么我就要消费点击事件。而点击事件其实是要分成两个事件的,即ACTION_DOWN + ACTION_UP ,只有这样才算是一次点击事件。因此打印了“两遍”log其实是先打印了ACTION_DOWN的分发流程,再打印了一遍ACTION_UP的分发流程,因此会看到最后一行打印了click事件。即,click事件是在ACTION_UP事件发生后才发生的。

然后看看各个方法的返回值。果然由于我的子View明确表示要消费这个事件序列,因此从ACTION_DOWN开始的所有事件就都交给他消费了。所以子View的onTouchEvent的返回值为true,表示自己需要消费这个事件,然后他的dispatchTouchEvent也返回了true,表示这一事件被自己分发了。既然自己的子View消费了事件,ViewGroup就认为这一事件是被自己分发了,因此他的dispatchTouchEvent也就返回了true。还是来一张图更清楚一点:最后,我在上一步的基础上,给ViewGroup的onInterceptTouchEvent()方法返回值强行改为true,表示事件传到这一层的时候就被拦截了,看一下log:

果然,虽然我要在子View消费事件,但是事件在传到子View之前就被ViewGroup拦截了,那么事件就只会由ViewGroup来消费了,所以ViewGroup就把事件传给了自己的onTouchEvent()来消费。再来一张图:

综上,事件分发的大致流程就是这样。

问题二:如何保证统一序列的事件都交给一个View来处理

先上结论:在传递过程中,只要有一个View主动去消费了第一个事件(ACTION_DOWN),那么ViewGroup会将这个View保存起来,之后同一事件序列的其他事件都直接交给这个View来处理。具体怎么操作,需要看一下源码:

//这是ViewGroup dispatchTouchEvent()的源码:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//省略前面一部分无关代码
//handled是返回的结果,表示是否被分发,默认当然是
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// 判断一下是不是ACTION_DOWN,如果是的话,代表一个新的事件序列来临了
if (actionMasked == MotionEvent.ACTION_DOWN) {
//要注意一下这两个方法,在这里会做一下相当于是“清零”的操作
//在这里包含了诸如mFirstTouchTarget=null这样的初始化操作
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// intercepted是用来记录是否被拦截的结果
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;
}
} else {
// 没有mFirstTouchTarget,同时事件为非ACTION_DOWN,那么就算要在这里拦截了
intercepted = true;
}
//忽略部分拦截相关的代码
//这两个对象记一下,后面会碰到
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
// 这里就开始对事件类型区分了,如果是ACTION_DOWN,那么就算是一个新的事件序列开始
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
// 准备一下,接下来开始遍历自己的子View们
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
// 获取到点击的坐标,用来从子View中筛选出点击到的VIEW
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// 按从后向前的顺序开始遍历子View们
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);
// 其实筛选只是将不合适的View们过滤掉
//一个一个continue就表示在发现View不合适的时候直接进入下一次循环
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
//终于找到了合适的子View,注意这里将子View封装为一个target
//要是返回的结果不为空就跳出循环
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;
}
//就算返回结果为空也没关系,在这里继续递归的调用子View的dispatchTransformedTouchEvent()
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;
}
}
if (preorderedList != null) preorderedList.clear();
}
//没有找到要接受事件的View
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
//接下来就是对于非ACTION_DOWN事件的分发了,这里有两种情况
if (mFirstTouchTarget == null) {
// 1.压根就没有找到要接受事件的view,或者被拦截了,调用了自身的dispatchTransformedTouchEvent()且穿了一个null的View进去,这样有什么用呢?需要后面分析dispatchTransformedTouchEvent()
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
//2.有View接受ACTION_DOWN事件,那么这个View也将接受其余的事件
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
//alreadyDispatchedToNewTouchTarget这个变量在前面View接受ACTION_DOWN事件时设为了true
//同时这个mFirstTouchTarget也就是那个View封装好的target
//那么这个返回值handled就为true
handled = true;
} else {
//对于非ACTION_DOWN事件,依然是递归调用dispatchTransformedTouchEvent
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
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;
}
}
// 处理ACTION_UP和ACTION_CANCEL
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);
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}

接下来看看dispatchTransformedTouchEvent()的源码:

//前面在分析dispatchTouchEvent()的时候发现有多处调用了这个dispatchTransformedTouchEvent(),而且有的地方传来的第三个参数是null
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
//处理ACTION_CANCEL
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
//忽略部分代码……
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
//如果传来的参数child为空时,调用自身dispatchTouchEvent()
handled = super.dispatchTouchEvent(event);
} else {
//不为空,那么就调用他的dispatchTouchEvent()
handled = child.dispatchTouchEvent(event);
}
return handled;
}
} else {
//...
}
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}

上面是对dispatchTouchEvent()和dispatchTransformedTouchEvent()的分析,看起来有点乱,这里梳理一下:

  • 首先明确一点,事件分发是从ViewGroup的dispatchTouchEvent()开始的ViewGroup在遇到一个新的事件序列,即事件ACTION_DOWN时,开始遍历自己的所有子View,找到需要接收到事件的View无论是否找到,都会调用dispatchTransformedTouchEvent()方法,区别在于如果找到了,那么在这个方法中传入的是那个View,否则就是nulldispatchTransformedTouchEvent()方法中第三个参数child为空时,会调用父类的dispatchTouchEvent()方法,否则会调用那个child的dispatchTouchEvent()方法。总而言之,都会去调用View类的dispatchTouchEvent()方法。dispatchTransformedTouchEvent()方法是进行具体的事件分发,除了OnClick()等事件外,onTouchEvent()方法就是在这里调用的只要找到了要接受事件的View,就会将他封装为一个target,保存起来,后续的其他事件都由他来接受

问题三:OnClick OnLongClick等对外的监听是在哪里处理的?

首先想一想一个很简单的逻辑,OnClick事件是先ACTION_DOWN之后再ACTION_UP,所以必定要在onTouchEvent()处理。同理,OnLongClick是在保持ACTION_DOWN一段时间后发生,因此也要在onTouchEvent()中处理。看看源码,发现果然是在这里:

//以下源码均为忽略了不想关部分,只保留了重点
public boolean onTouchEvent(MotionEvent event) {
//...
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// 处理click
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
}
break;
case MotionEvent.ACTION_DOWN:
// a short period in case this is a scroll.
if (isInScrollingContainer) {
//...
} else {
// 处理longclick
setPressed(true, x, y);
checkForLongClick(0, x, y);
}
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
//...
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_MOVE:
//...
break;
}
return true;
}
return false;
}

根据前面的分析,在View的dispatchTouchEvent()方法中,会对

public boolean dispatchTouchEvent(MotionEvent event) {
//...
boolean result = false;
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
//...
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//只要获取到的ListenerInfo不为空,就说明我们设置了监听,那么就会认为我们想让这个View处理所有事件
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {//所以会在这里执行onTouch()
result = true;
}
// If there is no deal, then another call onTouchEvent (), until onTouchEvent () returns false would think that not spending View event
IF {(the Result && onTouchEvent (Event)!)
The Result = to true;
}
}
return the Result;
}

It can be seen in View of dispatchTouchEvent () method, will be set by the listener whether other methods to determine whether or not to consume events by viewing. onTouchEvent () method will always call, click and longclick are there. Regardless of how to deal with internal, as long as the return to the true, the consumer will consider this event.

Analysis on this, and as a side dish of chicken, the analysis process is inevitable that some errors and omissions, welcome to the discussion in the comments section -

Guess you like

Origin www.cnblogs.com/Androidqin/p/11593877.html