Android读书笔记--从源码角度剖析View事件分发机制

栏目: Android · 发布时间: 7年前

内容简介:在开始描述问题之前先说点题外话,写这篇文章的初衷一方面为了构建Android知识体系,另一方面是真心觉得这个是Android面试必问的知识点。网上这方面的博客和书籍讲解这方面的知识也不少,讲的也很到位。正所谓只有自己理解了才是自己的,所以在阅读了他们的文章后,加上自己的理解特此记录一篇~,以便加深理解和记忆!如理解有误的地方请留言说明,我们一起探讨,谢谢!联系方式:邮箱(ixiyan.li@gmail.com)事件的分发说白了,就是用户与应用的交互过程(手指与屏幕接触)中,发生的一系列事件传递与处理过程。

在开始描述问题之前先说点题外话,写这篇文章的初衷一方面为了构建Android知识体系,另一方面是真心觉得这个是Android面试必问的知识点。网上这方面的博客和书籍讲解这方面的知识也不少,讲的也很到位。正所谓只有自己理解了才是自己的,所以在阅读了他们的文章后,加上自己的理解特此记录一篇~,以便加深理解和记忆!如理解有误的地方请留言说明,我们一起探讨,谢谢!

联系方式:邮箱(ixiyan.li@gmail.com)

1.必备知识点

事件的分发说白了,就是用户与应用的交互过程(手指与屏幕接触)中,发生的一系列事件传递与处理过程。

1.1 事件分发涉及的对象--MotionEvent

典型事件类型:

ACTION_DOWN——手指刚触碰屏幕那一刻(按下)
ACTION_MOVE——手指在屏幕上移动(移动)
ACTION_UP——手指抬起那一刻(抬起)
复制代码

一个事件序列:就是从手指按下 View 开始直到手指离开 View 产生的一系列事件。

ACTION_DOWN-> ACTION_UP
ACTION_DOWN->...ACTION_MOVE...->ACTION_UP
复制代码

1.2 事件分发涉及的方法

1. dispatchTouchEvent(MotionEvent ev)

用来进行事件分发。返回结果受当前 View 的 onTouchEvent 和子 View 的 dispatchTouchEvent 方法的影响,表示是否消耗当前事件。

2. onInterceptTouchEvent(MotionEvent ev)

在上述 dispatchTouchEvent 方法内部调用,用来进行当前事件是否拦截校验。这里有一点要注意的地方就是如果当前View拦截了某个事件(一般指ACTION_DOWN),那么在同一个 事件序列 (上面讲过这个概念)当中,此方法不会被再次调用——即不会做二次拦截校验。 注:Activity和View内部没有此方法

3. onTouchEvent(MotionEvent ev)

在上述 dispatchTouchEvent 方法内部调用,返回结果表示是否消耗当前事件。这里同上也有一点要注意,如果当前方法返回 false (不消耗),那么同一个 事件序列 中,当前View无法再次接收到事件。

上述方法的关系可用下面的一段伪代码表示:

public boolean dispatchTouchEvent(MotionEvetn e){ 
	if(onInterceptTouchEvent(ev)){//是否拦截
		return onTouchEvent(e);//拦截事件处理:是否消耗
	}
	return child.dispatchTouchEvent(e);//不拦截:子类View分发
}
复制代码

通过上面的伪代码可以大致了解到事件的传递规则:对于一个根 ViewGroup 来说,点击事件产生后,首先会传递给它,这时它的 dispatchTouchEvent 就会被调用,如果这个 ViewGrouponInterceptTouchEvent 方法返回 true ,就说明拦截当前事件,接着事件就会交给这个 ViewGrouponTouchEvent 方法处理。反之 onInterceptTouchEvent 方法返回 false ,就不拦截当前事件,这时当前事件就会传递给它的子 View ,接着 ViewdispatchTouchEvent 方法就会调用,如此反复直到事件最终被处理。

1.3 事件传递过程遵循如下过程

Activity -> Windown(PhoneWindow) -> DecorView(FrameLayout) -> contentView(setContentView) ->..ViewGroup..->View
复制代码

2. 事件分发源码解析

根据上面了解到的事件传递的过程分析,下面我们就一步一步撕开它神秘的面纱,从内部了解它的调用关系。

2.1 Activity对点击事件的分发过程

点击事件用 MontionEvent 表示,当一个点击操作发生时,最先传递给当前 Activity ,由 ActivitydispatchTouchEvent 方法进行事件分发,具体的工作由 Window 来完成。 Window 会将事件传递给 DecorViewDecorView 一般就是当前界面的底层容器(即setContentView所设置的 View 的父容器),通过Activity.getWindow().getDecorView()可以获得。因此我们先从 ActivitydispatchTouchEvent 开始分析。

源码-1:Activity#dispatchTouchEvent

/**
* Called to process touch screen events.  You can override this to
* intercept all touch screen events before they are dispatched to the
* window.  Be sure to call this implementation for touch screen events
* that should be handled normally.
*
* @param ev The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
    
public boolean onTouchEvent(MotionEvent event) {
    if (mWindow.shouldCloseOnTouch(this, event)) {
        finish();
        return true;
    }

    return false;
}    
复制代码

现在分析上述代码,通过源码了解到事件交给Activity所附属的Window进行分发,如果 getWindow().superDispatchTouchEvent(ev) 返回 true ,事件到此结束,返回 false ,说明下级所有View的 onTouchEvent 都返回了 false ,则Activity的 onTouchEvent 将会被调用(如上)

通过上面了解到 getWindow().superDispatchTouchEvent(ev) 这个才是分发的关键,看源码:

源码-2:Window#superDispatchTouchEvent

/**
 * Abstract base class for a top-level window look and behavior policy.  An
 * instance of this class should be used as the top-level view added to the
 * window manager. It provides standard UI policies such as a background, title
 * area, default key processing, etc.
 *
 * <p>The only existing implementation of this abstract class is
 * android.view.PhoneWindow, which you should instantiate when needing a
 * Window.
 */
public abstract class Window {
	/**
	 * Used by custom windows, such as Dialog, to pass the touch screen event
	 * further down the view hierarchy. Application developers should
	 * not need to implement or call this.
	 *
	 */
	public abstract boolean superDispatchTouchEvent(MotionEvent event);
	...
}
复制代码

看上面贴的源码发现贴了好多注释说明,因为这里 Window 是个抽象类,那么它的实现类是什么呢,是 PhoneWindow ,为什么呢?到这里您可以详细阅读下上面 Window 类的说明,发现此处已经指明了 Window 的唯一实现就是 android.view.PhoneWindow ,好家伙,隐藏的够深的,那么请移驾,谢谢~

源码-3:PhoneWindow#superDispatchTouchEvent相关代码

// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}
复制代码

到这里逻辑就清晰了吧!虽然代码只有一行,但已经足以说明问题了,此处具体逻辑移交给 DecorView (这就是我们前面说的窗口的顶级View-->ViewGroup),即 Activity#setContentView 设置的 View 就是 DecorView 的子View。目前事件传递到了 DecorView 这里,由于 DecorVieW 即成自 FrameLayout 且是父 View ,那么得出结论--最终事件会传递给 View ,到这一步并不是我们的重点,事件如何通过顶级 View 进行传递消费才是我们的重头戏,请继续,谢谢~

2.2 顶级View对点击事件的分发过程

关于点击事件如何在 View 中进行分发,上面已经做了描述,这里就直接上 ViewGroup 源码,源码如下:

dispatchTouchEvent 方法内容较多分如下几个片段说明:

源码-4:ViewGroup#dispatchTouchEvent——拦截逻辑处理

// 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();
}
// 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;
}
复制代码
  1. 是否拦截条件:事件类型为 ACTION_DOWN || mFirstTouchTarget != null ;
  2. mFirstTouchTarget:每次开始( ACTION_DOWN )都会被初始化为 null ,当事件由 ViewGroup 的子元素成功处理时,它指向子元素;
  3. 当事件由 ViewGroup 拦截时,条件 mFirstTouchTarget != null 不成立,即当 ACTION_MOVEACTION_UP 事件到来时,由于第一条拦截条件不满足,则 onInterceptTouchEvent 不再调用:应证了一旦当前View拦截事件,那么同一事件序列的其它事件都不再进行拦截校验,直接交给它处理。
  4. FLAG_DISALLOW_INTERCEPT 标记位:这个标记位一旦设置后( requestDisallowInterceptTouchEvent ), ViewGroup 将无法拦截除了 ACTION_DOWN 以外的其它点击事件( ACTION_DOWN 事件会重置此标记位,将导致子View中设置的这个标记位无效)。
  5. 面对 ACTION_DOWN 事件时, ViewGroup 总是会调用自己的 onInterceptTouchEvent 方法来询问自己是否要拦截事件,这一点从上面的源码中可以看出来。

源码-5:ViewGroup#dispatchTouchEvent——初始化

// 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);//重置 mFirstTouchTarget = null
    resetTouchState();//重置FLAG_DISALLOW_INTERCEPT标记位
}
复制代码

从上面的代码可以看出, ViewGroup 会在 ACTION_DOWN 事件到来时会做重置状态的操作,因此子 View 调用 requestDisallowInterceptTouchEvent 并不能影响 ViewGroupACTION_DOWN 事件的处理。

总结:

  1. ViewGroup 决定拦截事件( ACTION_DOWN )后,那么后续的点击事件将会默认交给它处理且不再调用它的 onInterceptTouchEvent 方法。
  2. FLAG_DISALLOW_INTERCEPT 这个标志的作用是让 ViewGroup 不再拦截事件,当然前提是 ViewGroup 不拦截 ACTION_DOWN 事件。
  3. FLAG_DISALLOW_INTERCEPT 为解决滑动冲突解决提供了新的思路。

源码-6:ViewGroup#dispatchTouchEvent——不拦截,遍历子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)) {//*子元素调用dispatchTouchEvent方法*
            // 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();
            //保存当前子View:mFirstTouchTarget
            newTouchTarget = addTouchTarget(child, idBitsToAssign);
            alreadyDispatchedToNewTouchTarget = true;
            break;
        }

        // The accessibility focus did not handle the event, so clear
        // the flag and do a normal dispatch to all children.
        ev.setTargetAccessibilityFocus(false);
    }
    if (preorderedList != null) preorderedList.clear();
    //...
}
复制代码

源码-7:ViewGroup#dispatchTouchEvent——子View下发主要逻辑调用

/**
 * Transforms a motion event into the coordinate space of a particular child view,
 * filters out irrelevant pointer ids, and overrides its action if necessary.
 * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
 */
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;

    // Canceling motions is a special case.  We do not need to perform any transformations
    // or filtering.  The important part is the action, not the contents.
    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;
    }
    //...
}
复制代码

View 是否能够接收点击事件有以下两点衡量:

  • 子元素是否在播放动画
  • 点击事件的坐标是否落在子元素的区域内

上面这部分代码说明的是 ViewGroup 不拦截情况下,事件向子 View 下发的过程.即主要调用方法为 dispatchTransformedTouchEvent ,它的内部实际上调用的就是子元素的 dispatchTouchEvent 方法(可通过上面的 源码-7 看得出来).通过具体分析可看出,如果 child.dispatchTouchEvent(event) 返回 true ,那么 mFirstTouchTarget ( addTouchTarget 方法内部操作)就会被赋值同时跳出for循环,这里是否对 mFirstTouchTarget 赋值,将会影响 ViewGroup 的拦截策略,如下所示:

源码-8:ViewGroup#dispatchTouchEvent——赋值mFirstTouchTarget

/**
 * Adds a touch target for specified child to the beginning of the list.
 * Assumes the target child is not already present.
 */
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}
复制代码

mFirstTouchTarget 如果为 null ,将会默认拦截接下来同一序列的所有事件。(不做二次拦截校验)

遍历所有子元素,都没有处理包含两种情况:

ViewGroup

此时 ViewGroup 将会调用 super.dispatchTouchEvent(evet) ,这一点可以从上述 源码-8 可以看出,很显然这里 ViewGroup 继承自 View ,所以这里就转到 ViewdispatchTouchEvent 方法,即点击事件交由 View 处理,那么请继续看下面的分析。

2.3 View对点击事件的处理过程

View(不包含ViewGroup)对点击事件的处理稍微简单,它没有 onInterceptTouchEvent 方法且无法向下传递事件,只能自己处理,请看它的 dispatchTouchEvent 方法,如下:

源码-9:View#dispatchTouchEvent——View点击事件处理

/**
* Pass the touch screen motion event down to the target view, or this
* view if it is the target.
*
* @param event The motion event to be dispatched.
* @return True if the event was handled by the view, false otherwise.
*/
public boolean dispatchTouchEvent(MotionEvent event) {
// If the event should be handled by accessibility focus first.
//...
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;
}
复制代码

从上面的代码可以看出: OnTouchListener的onTouchonTouchEvent(event) 优先级高,如果设置了 OnTouchListenermOnTouchListener.onTouch 返回 true 那么 onTouchEvent(event) 将不会调用,反之将会调用 onTouchEvent(event) ,见下文:

源码-10:View#onTouchEvent——点击事件具体处理

public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
	
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
	
if ((viewFlags & ENABLED_MASK) == DISABLED) {
	if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
	    setPressed(false);
	}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
	// A disabled view that is clickable still consumes the touch
	// events, it just does not respond to them.
	return clickable;
}
if (mTouchDelegate != null) {
	if (mTouchDelegate.onTouchEvent(event)) {
	    return true;
	}
}
	
	if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
	switch (action) {
	    case MotionEvent.ACTION_UP:
	        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
	        if ((viewFlags & TOOLTIP) == TOOLTIP) {
	            handleTooltipUp();
	        }
	        if (!clickable) {
	            removeTapCallback();
	            removeLongPressCallback();
	            mInContextButtonPress = false;
	            mHasPerformedLongPress = false;
	            mIgnoreNextUpEvent = false;
	            break;
	        }
	        boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
	        if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
	            // take focus if we do not have it already and we should in
	            // touch mode.
	            boolean focusTaken = false;
	            if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
	                focusTaken = requestFocus();
	            }
	
	            if (prepressed) {
	                // The button is being released before we actually
	                // showed it as pressed.  Make it show the pressed
	                // state now (before scheduling the click) to ensure
	                // the user sees it.
	                setPressed(true, x, y);
	            }
	
	            if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
	                // This is a tap, so remove the longpress check
	                removeLongPressCallback();
	
	                // Only perform take click actions if we were in the pressed state
	                if (!focusTaken) {
	                    // Use a Runnable and post this rather than calling
	                    // performClick directly. This lets other visual state
	                    // of the view update before click actions start.
	                    if (mPerformClick == null) {
	                        mPerformClick = new PerformClick();
	                    }
	                    if (!post(mPerformClick)) {
	                        performClick();
	                    }
	                }
	            }
	
	            if (mUnsetPressedState == null) {
	                mUnsetPressedState = new UnsetPressedState();
	            }
	
	            if (prepressed) {
	                postDelayed(mUnsetPressedState,
	                        ViewConfiguration.getPressedStateDuration());
	            } else if (!post(mUnsetPressedState)) {
	                // If the post failed, unpress right now
	                mUnsetPressedState.run();
	            }
	
	            removeTapCallback();
	        }
	        mIgnoreNextUpEvent = false;
	        break;
	
	//...
	   	}
	
	return true;
	}
	
	return false;
}
复制代码

从上面的代码看出:影响事件的消耗因素有两个: CLICKABLELONG_CLICKABLE 只要有一个为 true ,那么它就会消耗这个事件,即 onTouchEvent 方法返回 true ,实际调用方法为 performClick(); ,在其内部调用 OnClickListener#onClick 方法。

到此点击事件的分发机制的源码分析就完了,但是Android 的学习才刚开始,还有很长的路要走,下面附上从别处盗来的图,觉得不错可以看下


以上所述就是小编给大家介绍的《Android读书笔记--从源码角度剖析View事件分发机制》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Making Things See

Making Things See

Greg Borenstein / Make / 2012-2-3 / USD 39.99

Welcome to the Vision Revolution. With Microsoft's Kinect leading the way, you can now use 3D computer vision technology to build digital 3D models of people and objects that you can manipulate with g......一起来看看 《Making Things See》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

在线进制转换器
在线进制转换器

各进制数互转换器

随机密码生成器
随机密码生成器

多种字符组合密码