Android View深入解析事件分发机制
Posted Ruffian-痞子
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android View深入解析事件分发机制相关的知识,希望对你有一定的参考价值。
系列文章
Android View深入解析(一)基础知识VelocityTracker,GestureDetector,Scroller
Android View深入解析(二)事件分发机制
Android View深入解析(三)滑动冲突与解决
事件分发机制,一听好像很难的样子,但是我们时常说:不要怕,怕你就开始输了。
首先,心里要有一个信念,这个不难,别人都能搞定,自己也肯定没问题。其次,要先明白我们将要学习的是什么,不要搜到一篇文章上手就各种源码分析,看的一脸懵逼,关了网页连看过什么都不知道。最后,我们最好要弄明白原理,为什么会是这样的。通过这三个步骤,我们基本上就可以掌握一个知识点,或者了解一件事情。
信念
事件分发机制,是进阶成高手的必经之路,狠一点说,如果这个知识点没掌握,那你可能永远就是个菜鸟。嚯~~这个好狠,谁都不愿意永远是一个菜鸟,要进阶,要成为高手,要年薪百万,,,那就干。
用个例子说明事件分发机制:公司接到一个任务(事件MotionEvent
),于是公司决策层(顶层)就开会讨论这个任务(分发dispatchTouchEvent
),根据任务的重要难易程度决定是否将任务安排给下级人员处理,如果这个任务很重要很难完成,那么就把任务拦截掉(onInterceptTouchEvent
),由决策层的人处理(onTouchEvent
);如果这个任务不难,那就往下分发给下一级的员工。第二级的员工同样开会讨论是否拦截处理任务,还是分发给再下一级的员工。一直往下,直到有一级的员工把这个任务处理完成为止。当然,下发下去的任务也有处理不了的,那就只能往上一级抛,再不行,再往上一级,如果整个系统都没有能力处理,到最后这个任务只能是废弃掉。
通过上述的例子,可以清楚的知道,事件分发中涉及到三个方法和一个对象:dispatchTouchEvent
,onInterceptTouchEvent
,onTouchEvent
和MotionEvent
。接下来通过一段伪代码看看他们之间的关系:
public boolean dispatchTouchEvent(MotionEvent ev)
boolean consume = false;
if (onInterceptTouchEvent(ev))
consume = onTouchEvent(ev);
else
consume = child.dispatchTouchEvent(ev);
return consume;
这段代码摘自任玉刚老师,个人觉得这段伪代码写的非常好,很清晰。判断当前控件是否拦截事件,拦截:由当前控件处理触摸事件;不拦截,则分发给子控件的 dispatchTouchEvent
上面说到的事件分发包括了多个层级之间的传递,先认识两个对象:ViewGroup
,View
ViewGroup
:可以包含子对象
View
:不可包含子对象,最小控件
其中View是处理事件的最小控件,它没有拦截事件onInterceptTouchEvent
方法。
View 事件分发
先从最小控件 View 看看单一层级之间的事件分发。自定义一个MyView继承View把事件传播有关的方法重写,打印log,查看手指触摸屏幕或者点击时事件的分发
MyView
public class MyView extends View
public final String TAG = "MyView";
public MyView(Context context, AttributeSet attrs)
super(context, attrs);
/**
* 开启可点击
* 注:对于默认不可点击的View,TextView,ImageView...需要开启点击
*/
setEnabled(true);
setFocusable(true);
setLongClickable(true);
@Override
public boolean dispatchTouchEvent(MotionEvent event)
int action = event.getAction();
switch (action)
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "ACTION_DOWN dispatchTouchEvent");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "ACTION_MOVE dispatchTouchEvent");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "ACTION_UP dispatchTouchEvent");
break;
return super.dispatchTouchEvent(event);
@Override
public boolean onTouchEvent(MotionEvent event)
int action = event.getAction();
switch (action)
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "ACTION_DOWN onTouchEvent");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "ACTION_MOVE onTouchEvent");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "ACTION_UP onTouchEvent");
break;
return super.onTouchEvent(event);
在 onTouchEvent
和 dispatchTouchEvent
中打印了日志
特别注意:在构造函数中添加了
setEnabled(true);
setFocusable(true);
setLongClickable(true);
因为View,TextView,ImageView 等默认可不点击的View在执行完 ACTION_DOWN
后返回 true 导致ACTION_MOVE
,ACTION_UP
不被执行,只有在设置可点击的情况下才会执行 MOVE 和 UP
布局文件
<?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:orientation="vertical">
<com.r.view.MyView
android:id="@+id/my_view"
android:layout_width="200dp"
android:layout_height="200dp"
android:background="@color/colorAccent" />
</LinearLayout>
再看一下Activity代码
public class MyViewActivity extends Activity
public final String TAG = "MyView";
@Override
protected void onCreate(Bundle savedInstanceState)
super.onCreate(savedInstanceState);
setContentView(R.layout.my_view);
MyView myView = (MyView) findViewById(R.id.my_view);
myView.setOnTouchListener(new View.OnTouchListener()
@Override
public boolean onTouch(View v, MotionEvent event)
int action = event.getAction();
switch (action)
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "ACTION_DOWN onTouch");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "ACTION_MOVE onTouch");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "ACTION_UP onTouch");
break;
return false;
);
好了,跟View事件相关一般就这三个地方了,一个onTouchEvent
,一个dispatchTouchEvent
,一个setOnTouchListener
,接着点击一下View(稍微蹭一下制造MOVE
事件),查看Log日志
通过LOG我们可以看出,无论是DOWN
,MOVE
,UP
都会按照下面的顺序执行
1、
dispatchTouchEvent
2、setOnTouchListener
的onTouch
3、onTouchEvent
View 的 dispatchTouchEvent
根据log的顺序,首先进入 View 的 dispatchTouchEvent
看看源码
public boolean dispatchTouchEvent(MotionEvent event)
...
boolean result = false;
if (onFilterTouchEventForSecurity(event))
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event))
result = true;
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event))
result = true;
if (!result && onTouchEvent(event))
result = true;
...
return result;
为了方便查看,剔除了一些无关的代码,直接看11行,如果 li != null
,li.mOnTouchListener != null
,并且 view 是 enable 的状态,同时li.mOnTouchListener.onTouch(this, event)
返回true
,此时result=true
,那么 17行的 if
条件就不成立,也就是不会执行 onTouchEvent(event)
在源码中追溯一下mOnTouchListener
public void setOnTouchListener(OnTouchListener l)
getListenerInfo().mOnTouchListener = l;
发现原来它就是我们在Activity中设置的setOnTouchListener
通过上述分析,得出:
先调用
setOnTouchListener
的onTouch
再调用onTouchEvent
如果onTouch
返回true
,不会调用onTouchEvent
;onTouch
返回false
,调用onTouchEvent
到这里,我们通过打印log和查看源码分析了View的事件分发机制。
ViewGroup 事件分发
分析完View的事件分发,接着看看ViewGroup,自定义一个MyViewGroup继承LinearLayout,在事件分发相关的方法添加log
MyViewGroup
public class MyViewGroup extends LinearLayout
public final String TAG = "MyViewGroup";
public MyViewGroup(Context context, AttributeSet attrs)
super(context, attrs);
@Override
public boolean dispatchTouchEvent(MotionEvent event)
int action = event.getAction();
switch (action)
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "ACTION_DOWN dispatchTouchEvent");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "ACTION_MOVE dispatchTouchEvent");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "ACTION_UP dispatchTouchEvent");
break;
return super.dispatchTouchEvent(event);
@Override
public boolean onInterceptTouchEvent(MotionEvent ev)
int action = ev.getAction();
switch (action)
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "ACTION_DOWN onInterceptTouchEvent");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "ACTION_MOVE onInterceptTouchEvent");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "ACTION_UP onInterceptTouchEvent");
break;
return super.onInterceptTouchEvent(ev);
@Override
public boolean onTouchEvent(MotionEvent event)
int action = event.getAction();
switch (action)
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "ACTION_DOWN onTouchEvent");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "ACTION_MOVE onTouchEvent");
break;
case MotionEvent.ACTION_UP:
Log.e(TAG, "ACTION_UP onTouchEvent");
break;
return super.onTouchEvent(event);
ViewGroup跟View有所不同,多一个拦截事件的方法onInterceptTouchEvent
布局代码
<?xml version="1.0" encoding="utf-8"?>
<com.r.view.MyViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.r.view.MyView
android:id="@+id/my_view"
android:layout_width="200dp"
android:layout_height="200dp"
android:background="@color/colorAccent" />
</com.r.view.MyViewGroup>
MyViewGroup
中包含一个 MyView
,Activity直接引入布局运行即可
根据log我们可以看到 DOWN,MOVE,UP 调用流程
1.ViewGroup dispatchTouchEvent
2.ViewGroup onInterceptTouchEvent
3.View dispatchTouchEvent
4.View onTouchEvent
ViewGroup 的 dispatchTouchEvent
// Check for interception.
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
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
这个方法的代码比较多,也比较复杂,这里只抽取关键地方分析
mFirstTouchTarget
是否为空,初始按下时,会寻找触摸目标,当事件由ViewGroup的子元素成功处理时,mFirstTouchTarget
会被赋值并指向子元素,也就是说 ViewGroup 不拦截事件并且交由子元素处理时mFirstTouchTarget!=null
所以外层 IF 代码表示,如果当前不是 ACTION_DOWN
并且没有触摸目标,ViewGroup内没有找到目标View,事件交由ViewGroup拦截处理(比如点击空白处),如果是ACTION_DOWN
同时存在触摸目标,判断disallowIntercept
(子View通过requestDisallowInterceptTouchEvent
方法可设置是否允许 ViewGroup拦截事件),允许拦截:交由ViewGroup的onInterceptTouchEvent,不允许拦截:分发给子元素
接着看,如果ViewGroup不拦截事件,交由子View处理的逻辑
源码
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;
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
if (preorderedList != null) preorderedList.clear();
分析上述代码,首先遍历ViewGroup的所有子元素,然后判断子元素是否能够接收事件,是否能够接收事件的判断条件有两个:子元素是否正在执行动画,触摸事件的坐标是否落在子元素的区域内,如果某一个子元素满足这两个条件,就把事件交由它处理。
对于onInterceptTouchEvent
我们一般重写,根据业务逻辑确定是否要拦截当前事件。
接下来看一下总结性的图(图片来自网络)
在 ViewGroup 中可以通过 onInterceptTouchEvent
方法对事件传递进行拦截。返回 true / false
true : 拦截事件,触发当前 ViewGroup 的 onTouchEvent()
,进行事件的消费;
false:不拦截,事件向下传递
默认返回 false
再结合开篇提到的任玉刚老师写的伪代码,好好理解一下View的事件分发
public boolean dispatchTouchEvent(MotionEvent ev)
boolean consume = false;
if (onInterceptTouchEvent(ev))
consume = onTouchEvent(ev);
else
consume = child.dispatchTouchEvent(ev);
return consume;
纸上得来终觉浅,绝知此事要躬行,或许看完觉得知道个大概,3天之后呢?半个月之后呢?所以还是要上手打打代码,或许能有新的发现,最不济就是加深印象,巩固知识。
以上是关于Android View深入解析事件分发机制的主要内容,如果未能解决你的问题,请参考以下文章
Android View体系从源码解析View的事件分发机制
Android View体系从源码解析View的事件分发机制