Android View深入解析事件分发机制

Posted Ruffian-痞子

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android View深入解析事件分发机制相关的知识,希望对你有一定的参考价值。

系列文章
Android View深入解析(一)基础知识VelocityTracker,GestureDetector,Scroller
Android View深入解析(二)事件分发机制
Android View深入解析(三)滑动冲突与解决

事件分发机制,一听好像很难的样子,但是我们时常说:不要怕,怕你就开始输了。
首先,心里要有一个信念,这个不难,别人都能搞定,自己也肯定没问题。其次,要先明白我们将要学习的是什么,不要搜到一篇文章上手就各种源码分析,看的一脸懵逼,关了网页连看过什么都不知道。最后,我们最好要弄明白原理,为什么会是这样的。通过这三个步骤,我们基本上就可以掌握一个知识点,或者了解一件事情。

信念
事件分发机制,是进阶成高手的必经之路,狠一点说,如果这个知识点没掌握,那你可能永远就是个菜鸟。嚯~~这个好狠,谁都不愿意永远是一个菜鸟,要进阶,要成为高手,要年薪百万,,,那就干。

用个例子说明事件分发机制:公司接到一个任务(事件MotionEvent),于是公司决策层(顶层)就开会讨论这个任务(分发dispatchTouchEvent),根据任务的重要难易程度决定是否将任务安排给下级人员处理,如果这个任务很重要很难完成,那么就把任务拦截掉(onInterceptTouchEvent),由决策层的人处理(onTouchEvent);如果这个任务不难,那就往下分发给下一级的员工。第二级的员工同样开会讨论是否拦截处理任务,还是分发给再下一级的员工。一直往下,直到有一级的员工把这个任务处理完成为止。当然,下发下去的任务也有处理不了的,那就只能往上一级抛,再不行,再往上一级,如果整个系统都没有能力处理,到最后这个任务只能是废弃掉。

通过上述的例子,可以清楚的知道,事件分发中涉及到三个方法和一个对象:dispatchTouchEventonInterceptTouchEventonTouchEventMotionEvent。接下来通过一段伪代码看看他们之间的关系:

public boolean dispatchTouchEvent(MotionEvent ev) 
        boolean consume = false;
        if (onInterceptTouchEvent(ev)) 
            consume = onTouchEvent(ev);
         else 
            consume = child.dispatchTouchEvent(ev);
        
        return consume;
    

这段代码摘自任玉刚老师,个人觉得这段伪代码写的非常好,很清晰。判断当前控件是否拦截事件,拦截:由当前控件处理触摸事件;不拦截,则分发给子控件的 dispatchTouchEvent

上面说到的事件分发包括了多个层级之间的传递,先认识两个对象:ViewGroupView
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);
    

onTouchEventdispatchTouchEvent 中打印了日志

特别注意:在构造函数中添加了

setEnabled(true);
setFocusable(true);
setLongClickable(true);

因为View,TextView,ImageView 等默认可不点击的View在执行完 ACTION_DOWN 后返回 true 导致ACTION_MOVEACTION_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我们可以看出,无论是DOWNMOVEUP都会按照下面的顺序执行

1、dispatchTouchEvent
2、setOnTouchListeneronTouch
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 != nullli.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
通过上述分析,得出:

先调用 setOnTouchListeneronTouch
再调用 onTouchEvent
如果 onTouch 返回true,不会调用onTouchEventonTouch 返回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的事件分发机制

Android事件分发机制完全解析,带你从源码的角度彻底理解(下)

深入聊聊Android事件分发机制

View的事件分发机制解析

Android View深入解析滑动冲突与解决