Android 事件分发机制分析及源码详解

Posted xyTianZhao

tags:

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

android 事件分发机制分析及源码详解

文章目录

一般在实际开发中,我们很少主动去处理相关滑动处理,所以就很少关注事件分发相关机制。因为系统已经帮我们处理好了,如:ScrollView、ViewPage、ListView 等。
这里我们就以事件分发为入口,来分析一下事件分发机制与滑动冲突的解决方法。

学习 Android 开发过程中,或多或少都有听过事件分发机制,就算没听过,如果有面试经历,也大有几率被问到过吧。毕竟在现在,这个分发机制以及算是烂大街的题目了。

事件的定义

当用户触摸屏幕时,产生的一系列行为( Touch 事件)。

系统源码中通过 MotionEvent 这个类来描述这一系列相关行为。这个类中也定义了许多与事件相关的常量与变量,如事件类型、事件的相关属性等。这里就只贴出主要的四个相关类型

public final class MotionEvent extends InputEvent implements Parcelable 
    //按下事件:手指刚触碰屏幕
    public static final int ACTION_DOWN = 0;
    //松开事件:手指从屏幕松开
    public static final int ACTION_UP = 1;
    //移动事件:手指在屏幕上滑动
    public static final int ACTION_MOVE = 2;
    //取消事件:非人为因素取消
    public static final int ACTION_CANCEL = 3;

事件分发序列模型

分发序列

正常情况下,一次手指触摸屏幕的行为会发出一系列点击事件,可以分为如下两种情况:

  • 点击屏幕后立即松开:DOWN->UP
  • 点击屏幕滑动后再松开:DOWN->MOVE···->MOVE->UP

分发模型

我们在从宏观角度了解一下事件的分发总流程,总体来说就是:U型模型

事件先从 Activity 出发,然后传递给 DecorView(ViewGroup),经过 ViewGroup 然后在传递给相应的子 View 进行处理。如果不处理,则按照原路径逐级向上返回,直至传递到 Activity 结束。

如下流程图表述应该比较容易理解。

事件分发对象及相关方法

通过上面的分发序列和分发模型,可以看出,一个事件的分发主要涉及三个对象:Activity、ViewGroup和View。

  • Activity:控制生命周期和处理事件
  • ViewGroup:一组 View 的集合(可以包含多个子 View)
  • View:所有 UI 组件的基类

而这三个分发对象都有相应的事件处理方法

  • dispatchTouchEvent(MotionEvent event)
    用来进行事件分发
  • onInterceptTouchEvent(MotionEvent ev)
    判断是否拦截事件(值存在于 ViewGroup 中)
  • onTouchEvent(MotionEvent event)
    处理相关事件

源码分析

到这里我们对整个大致流程已经有了相关的印象了,接下来我们在从源码的角度来看看,事件是怎么传递的。代码省略了不重要的部分,并且加上了相关注释,就不过多进行解释了。

事件从 ActivitydispatchTouchEvent 方法开始向下分发进行处理。

public class Activity
   public boolean dispatchTouchEvent(MotionEvent ev) 
        ...
        //调用 window 实现类 PhoneWindow 的 superDispatchTouchEvent 方法
        if (getWindow().superDispatchTouchEvent(ev)) 
            return true;
        
        //如果上面不进行处理,则调用 Activity 的 onTouchEvent 进行处理
        return onTouchEvent(ev);
    
    public boolean onTouchEvent(MotionEvent event) 
        //如果点击区域外,则销毁当前界面,否则不进行任何处理
        if (mWindow.shouldCloseOnTouch(this, event)) 
            finish();
            return true;
        

        return false;
    

接下来在看看 PhoneWindow 怎么处理

public class PhoneWindow extends Window implements MenuBuilder.Callback 
    // This is the top-level view of the window, containing the window decor.
    //窗口的顶层视图,相当于上面模型中的 跟ViewGroup
    private DecorView mDecor;

    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) 
        return mDecor.superDispatchTouchEvent(event);
    

public class DecorView extends FrameLayout
    public boolean superDispatchTouchEvent(MotionEvent event) 
        //最终会调用 ViewGroup 的 dispatchTouchEvent 方法
        return super.dispatchTouchEvent(event);
    

先看看 ViewGroup 中的拦截逻辑

public abstract class ViewGroup extends View
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) 
        ...
        boolean handled = false;
        ...
        //进行安全校验和过滤,一般都为 true
        if (onFilterTouchEventForSecurity(ev)) 
            ...
            // 检测是否需要拦截
            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;
            
            ...
            if (!canceled && !intercepted) 
                ...
                if (newTouchTarget == null && childrenCount != 0) 
                    for (int i = childrenCount - 1; i >= 0; i--) //倒叙遍历 子View
                        ...
                        //将事件分发给 子View
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) 
                            ...
                            //返回 true 表示该 view 处理事件,然后将该 view 赋值给 目标view
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            ...
                        
                        ...
                    
                
                ...
            
        
        ...
        return handled;
    
    //进行一些安全性校验和过滤
    public boolean onFilterTouchEventForSecurity(MotionEvent event) 
        //noinspection RedundantIfStatement
        if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
                && (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) 
            // Window is obscured, drop this touch.
            return false;
        
        return true;
    
    //是否拦截事件,在 ViewGroup 中一般返回的 false
    public boolean onInterceptTouchEvent(MotionEvent ev) 
        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                && ev.getAction() == MotionEvent.ACTION_DOWN
                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                && isOnScrollbarThumb(ev.getX(), ev.getY())) 
            return true;
        
        return false;
    

在看看 ViewGroup 的分发逻辑

public abstract class ViewGroup extends View
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) 
        final boolean handled;
        ...
        if (child == null) 
            //调用 View 的 dispatchTouchEvent 方法
            handled = super.dispatchTouchEvent(event);
         else 
            //调用 子View 的 dispatchTouchEvent 方法
            handled = child.dispatchTouchEvent(event);
        
        event.setAction(oldAction);
        ...
        return handled
    

上面事件已经从 Activity 分发到了 ViewGroup,再从 ViewGroupdispatchTransformedTouchEvent 方法调用了子 ViewdispatchTouchEvent 方法,接下来在看看 View 中对于事件是怎么处理的。

public class View implements Drawable.Callback, KeyEvent.Callback,
        AccessibilityEventSource 
    public boolean dispatchTouchEvent(MotionEvent event) 
        ...
        boolean result = false;
        //当前View是否可见(未被其他窗口遮盖住,且未隐藏)
        if (onFilterTouchEventForSecurity(event)) 
            ...
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) 
                //如果设置了 OnTouchListener
                //则先响应 OnTouchListener.onTouch 方法
                result = true;
            
            // 当 onTouch 返回 false,在执行 onTouchEvent 处理相关事件,如果处理,则返回 true
            if (!result && onTouchEvent(event)) 
                result = true;
            
            ...
        
        ...
        return result;
    
    public boolean onTouchEvent(MotionEvent event) 
        ...
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
        ...
        //如果 View 可点击 或者 可长按,则最终一定 return true
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) 
            switch (action) 
                //抬起,判断是否处理点击事件
                case MotionEvent.ACTION_UP:
                break;
                //按下,处理长按事件
                case MotionEvent.ACTION_DOWN:
                break;
                //移动,检测触摸是否划出了控件,移除响应事件
                case MotionEvent.ACTION_MOVE:
                break;
            
            return true;
        
        ...
        return false;
    

事件分发总结

  • 事件的传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子View中干预父元素的事件分发过程,但ACTION_DOWN除外
  • ViewGroup默认不拦截任何事件,即onInterceptTouchEvent默认返回false, View没有onlnterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用。
  • View的onTouchEvent默认会消耗事件(返回true),除非它是不可点击的clickable和longClickable同时为false), View的longClickable默认都为false, clickable要分情况,比如Button的clickable默认为true,TextView的clickable默认为false.
  • View的enable属性不影响onTouchEvent的默认返回值。哪怕一个View是disable状态的,只要它的clickable或者longClickable有一个为true,那么它的onTouchEvent就返回true
  • onClick会响应的前提是当前View是可点击的,并且收到了ACTION-DOWN和ACTION-_UP的事件,并且受长按事件影响,当长按事
    件返回true时, onClick不会响应。
  • onLongClick在ACTION-DOWN里判断是否进行响应,要想执行长按事件该View必须是longClickable的并且设置了OnLongClickListener
  • 一个事件序列从手指接触屏幕到手指离开屏幕,在这个过程中产生一系列事件,以DOWN事件开始,中间含有不定数的MOVE事件,以UP事件结束正常情况下,一个事件序列只能被一个view拦截并且消耗。
  • 某个View一旦决定拦截,那么这个事件序列都将由它的onTouchEvent处理,并且它的onlnterceptTouchEvent不会再调用。
  • 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回false),那么同一事件序列中其他事件都不会再交给它处理。并且重新交由它的父元素处理(父元素onTouchEvent被调用)。

以上是关于Android 事件分发机制分析及源码详解的主要内容,如果未能解决你的问题,请参考以下文章

从源码角度分析Android 事件分发机制以及常见滑动冲突解决方案

从源码角度分析Android 事件分发机制以及常见滑动冲突解决方案

Android6.0 ViewGroup/View 事件分发机制详解

Android查缺补漏(View篇)--事件分发机制源码分析

朝花夕拾Android自定义View篇之Android事件分发机制(中)从源码分析事件分发逻辑及经常遇到的一些“诡异”现象

Android Touch事件分发处理机制详解