View事件体系

时间:2023-03-08 22:31:37

View事件体系

文章目录

一.Android View基础知识

1.1 View简介

在Android中View是非常重要的一个基类,是所有与用户交互的控件的基类,代表了用户界面组成里的控件,在屏幕上体现为一个矩形区域。无论是常见的控件TextView,ImageView等,还是像LinearLayout等ViewGroup都直接或者间接的继承自View(ViewGroup也派生自View),所以View类也是用户界面控件的一种抽象。

1.2 View分类

单一视图(View) 视图组(ViewGroup)
单独的一个View,比如TextView,内部不包含其他View 由多个View组成的ViewGroup,比如LinearLayout,内部可以包含其它View或者ViewGroup

1.3 View的结构

见得最多的View树结构图:

View事件体系

一个View树可以理解为是一个ViewGroup,ViewGroup中除了可以包含子View还可以包含子ViewGroup,以此类推得到当前视图结构中全部的View。二者形成了View树,该树的上层节点负责下层节点的测量,放置,绘制等工作,以及触摸事件的传递反馈等工作。

除了上面这个图,还有一个图也是见得很多的:

View事件体系

每个activity都对应一个窗口window,这个窗口是PhoneWindow的实例,PhoneWindow对应的布局是DecorView,是一个FrameLayout,DecorView内部一般又分为两部分,一部分是title(可以通过API或者主题设置为是否有这一部分),另一部分是content(即activity在setContentView对应的布局,对应的资源id是android.R.id.content,可以通过这个id获取ContentView对应的ViewGroup实例)。

1.4 View的坐标

Android中坐标系的原点是(屏幕)左上角,x轴向右增大,y轴向下增大。

1.4.1 Android中的坐标系

View事件体系

之所以把屏幕打上括号,原因在于不仅有想对于屏幕的坐标系,还有想对于View的坐标系。

区别在于:

1.相对于屏幕的坐标系,可以理解为绝对坐标系,通过view的getRawX以及getRawY这两个方法获得触摸点的绝对坐标值

2.相对于View的坐标系,通过view的getX和getY获得,得到的值是View控件内部想对于View左上角为原点得到的坐标。

1.4.2 View坐标的获取

上面提到了获取一个触摸点的想对于屏幕和View控件的坐标值,还有View控件相对于ViewGroup的位置,可以看这张图:

View事件体系

getTop:获取view自身的顶边到其父布局顶边的距离

getLeft:获取view自身的左边到其父布局左边的距离

getRight:获取view自身的右边到其父布局左边的距离

getBottom:获取view自身的底边到其父布局顶边的距离

二.View事件分发机制

2.1 事件分发基础

2.1.1 事件分发对象:

或者说事件分发到底分发的是什么东西,需要了解什么是View事件:

a. View事件

当用户触摸屏幕的时候,将会产生一系列触摸事件,这些事件的细节(触摸点的坐标,触摸的时间等)被封装成一个MotionEvent对象,View事件直观体现就是点击,长按等触摸事件。

View事件有很多种,应用场景最多的是:

事件类型 具体动作
MotionEvent.ACTION_DOWN 按下屏幕(所有事件的开始)
MotionEvent.ACTION_UP 手指从屏幕上抬起(与DOWN对应,事件结束标志)
MotionEvent.ACTION_MOVE 手指在屏幕上移动
MotionEvent.ACTION_CANCEL 结束事件(同为事件结束标志,但是是非人为的原因造成事件结束)
b. 事件序列

由基本的事件组成的序列

例如

点击屏幕即:DOWN->(MOVE)->UP

因为可能手指点击屏幕存在轻微抖动,所以这里的MOVE事件一般也能被系统捕获。

手指在屏幕上滑动:DOWN->MOVE->…->MOVE->UP

事件序列是一串的,从DOWN开始到UP结束(或CANCLE)。

View对于事件的处理,也是从DOWN开始,到UP(CANCLE) 结束,如果不处理DOWN事件,即不处理该对应的事件序列。

2.1.2 事件分发本质

事件分发的本质就是系统通过层层传递,将一个事件序列最终传递到某个View,并最终由其处理的这个过程(这只是一般情况);当最终传递到的那个View也不对此事件进行处理(或者说消费),那么还有一个沿原路返回继续寻找可处理节点的过程。

2.1.3 事件分发顺序

分发的本质是对事件的传递,这个传递所需要经过部分以及顺序为:

Activity->Window->ViewGroup->View

其实ViewGroup和View可以视为一体,因为本质上ViewGroup也是属于View的嘛。所以事件最先传递给Activity,然后Acitvity传递给Window,再由Window传递给View,到了View就是从根节点的View到最终的目的View啦。

2.1.4 事件分发的三个核心方法

返回值均为boolean,

方法 作用
dispatchTouchEvent 分发事件,当前View如果能够获取到这个事件,该方法一定会被调用
onInterceptTouchEvent 拦截事件,在上面一个方法中调用,决定当前View是否要把该事件拦截下来处理
onTouchEvent 在分发事件方法中调用,用来处理事件(或者说消耗事件)

这三个方法的关系可以用伪代码表示为:

View事件体系

简单描述一下这个过程:

点击事件产生后,ParentViewGroup获取到这个事件,将会调用dispath方法,分发方法内调用onIntercept拦截方法,如果ParentViewGroup决定拦截这个事件,那么就会调用他的onTouchEvent方法对事件进行处理;否则就调用ChildViewGroup或者ChildView的分发方法,然后如此逐层调用分发拦截处理,最终将事件处理掉。

2.1.5 为什么要有事件分发机制?

因为在Android中,视图的布局结构是树形结构,这就会导致一些View(也包括ViewGroup)可能会重叠在一起,当我们手指点击的地方在很多个View的(也包括ViewGroup)范围之内,这个时候有好多个View可以响应我们的触摸事件,解决这之间存在的冲突,让用户希望的view来响应对应的触摸事件,就是事件分发机制存在的意义。

2.2 事件分发源码

Activity最先获取到一个事件,然后他的分发方法被调用,方法内部将事件交由window来分发,window实现将事件分发给DecorView,再由它往下分发。

2.2.1 Activity对事件的分发

a. Activity.java内部dispatchTouchEvent方法如下:

public boolean dispatchTouchEvent(MotionEvent ev) {
		//时间序列的开始就是DOWN事件
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {、
        	//该方法是一个空方法,从源码注释来看,当用户和设备交互的时候会被调用
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

b. getWindow().superDispatchTouchEvent(ev)

get方法获取到Window实力对象,这里是PhoneWindow(Window本身是一个抽象类,PhoneWindow是它的实现类),PhoneWindow又将事件传递给了DecorView。

如果该方法返回的是true,就表示事件分发完毕,否则就表示事件没有View可以处理(或者说所有的View都不处理这个事件),将会调用Activity的onTouchEvent方法,这也就是在很多博客或者书上看到的如果点击事件层层传递都不能被处理最终将会回到Activity的原因。

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

关于DecorView,前面也提到过本质上是一个FrameLayout,是View树的根节点所在。

c. mDecor.superDispatchTouchEvent

public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

而FrameLayout又派生自ViewGroup,最终调用到了

ViewGroup的dispatch方法

至此,事件在Activity这一部分就走完了。

d. 补充:

还有Activity的ionTouchEvent没有提到,具体看源码:

/**
     * Called when a touch screen event was not handled by any of the views
     * under it.  This is most useful to process touch events that happen
     * outside of your window bounds, where there is no view to receive it.
     *
     * @param event The touch screen event being processed.
     *
     * @return Return true if you have consumed the event, false if you haven't.
     * The default implementation always returns false.
     */
    public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }

        return false;
    }

从注释可以看出,被调用的情况就是所有的View都不处理对应的事件,无论返回的结果是什么,这一次的事件分发都结束。

2.2.2 ViewGroup对事件的分发

从前面分析Activity的事件传递来看,分发的关键在于ViewGroup的分发方法,下面就来看下ViewGroup对于事件分发又是怎样一个流程。

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        ...
        //这个变量标记是不是处理该事件
        boolean handled = false;
        // 条件里面方法决定事件是应该被分发还是被抛弃掉
        //------>见补充1
            if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // ViewGroup会在down事件到来的时候对自身的状态进行重置
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                //因为down事件是事件序列开始的标志,当其进来的时候,立即重置触摸事件状态开始一次新的触摸事件
                //关于TouchTarget下面注明了,该方法将清除掉所有的TouchTarget对象并重置触摸状态
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

            // 下面就是确定是不是要拦截事件了
            // mFirstTouchTarget是TouchTarget对象,而TouchTarget是一个链表,描述了被Touch的View的相关信息
            //------>见补充2
            // 或者这样理解,当ViewGroup的子元素决定处理事件的时候,mFirstTouchTarget的值就是该子元素
            // 否则就为null
            final boolean intercepted;
            // 进入的条件就是是down事件或者有子View决定处理对应事件
            // 如果事件既不是down事件,ViewGroup也有子元素决定处理对应事件
            // ViewGroup的拦截方法将不会被调用,也就不会拦截后续的MOVE和UP事件
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                //不允许ViewGroup拦截除Down事件外的标记
                //FLAG_DISALLOW_INTERCEPT这个标记位可以由子View通过requestDisallowInterceptTouchEvent方法设置
                //设置之后ViewGroup将不会拦截除了down事件以外的其他事件
                //------>见补充3
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    //到这里就表示ViewGroup决定拦截事件了,并调用了自身的拦截方法
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                //代码执行到这里即ViewGroup没有子元素可以处理这一次的事件,那么ViewGroup就要拦截掉这次事件
                intercepted = true;
            }

            ...

            if (不拦截) {

                // 如果父View不拦截,那么就把事件分发给子View
               ...
                    if (newTouchTarget == null && childrenCount != 0) {
                        ...
                        //对子View列表进行遍历,找到一个可以接收这个事件的子View,
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            //下面两个方法获取到对应子View索引和子View引用
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);
                            ...
                            //判断子View是否可以接受事件(VISIABLE 没有动画)以及触点是否在子View的坐标范围之内
                            //------>见补充4
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }

                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                //子View接受了事件,同事件序列的后续事件也将增加给它
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }
                            ...
                            //这个方法,将事件传递给子View,如果子View为空,事件将回到父ViewGroup的分发方法
                            //否则就将调用子View的分发方法
                            //------>见补充5
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                ...
                                //完成对mFirstTouchTarget的赋值(在add方法内部完成)然后跳出循环
                                //------>见补充6
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                            ev.setTargetAccessibilityFocus(false);
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }

                    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;
                    }
                }
            }

            // 如果遍历所有的子元素都没有处理事件,那么事件还是回到了ViewGroup本身
            if (mFirstTouchTarget == null) {
                //没有子元素可以处理的情况之一就是这个ViewGroup是没有子元素的
                //就需要将事件交给ViewGroup自身来处理。
                //还有另外一种情况就是:(参照Android开发艺术探索)
                //子元素虽然处理了点击事件,但是在其dispatch方法中返回了false
                //在dispatch方法中返回false的一种可能是touchEvent方法返回了false
                //------>见补充7
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            }
        ...
    }
针对上述源码的几处补充说明:
补充1 onFilterTouchEventForSecurity

onFilterTouchEventForSecurity,该方法决定事件应该是执行分发过程还是直接抛弃掉,条件是当前View有没有被覆盖掉,如果被覆盖掉了(另有其他的View在他之上,那么这个时间就要被抛弃掉),返回true表示事件要被分发,false表示被抛弃。

补充2 TouchTarget

TouchTarget:描述的是一个被触摸的视图View和一系列被捕获的触点的id,是一个单链表形式的结构。mFirstTouchTarget的意义是当前view的childview是否处理了消耗了touch事件,如果消耗掉了,mFirstTouchTarget不为null;而如果ViewGroup决定拦截该事件,事件就没法被childView所消耗,自然mFirstTouchTarget就是个空值,(或者反过来说mFirstTouchTarget是null的话,ViewGroup将拦下同事件序列中的所有的点击事件)。

补充3 FLAG_DISALLOW_INTERCEPT

FLAG_DISALLOW_INTERCEPT,这个标志位比较特殊,子View可以通过调用对应的方法对父View进行设置要求父View不再拦截事件;这个FLAG的存在为解决滑动冲突提供了一种办法。

补充4 canViewReceivePointerEvents

根据View的VISIBILITY标签是否是VISIBLE以及是否在播放动画来决定View是否能接受到触点事件。

private static boolean canViewReceivePointerEvents(@NonNull View child) {
        return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                || child.getAnimation() != null;
}

另一个方法计算的是触点在不在View的约束范围之内,在就返回true,否则就返回false。

补充5 dispatchTransformedTouchEvent

dispatchTransformedTouchEvent源码:

 /**
     * Transforms a motion event into the coordinate space of a particular child view,
     * 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;
        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;
        }
        ...
        return handled;
    }

这里我省略了一部分后面的代码,重点关注在if-else语句中,如果child不为null,那么就将事件传递给子元素处理,以此,完成一轮事件的分发。如果子View是空的,事件会回到ViewGroup本身。

补充6 mFirstTouchTarget

mFirstTouchTarget在addTouchTarget方法中被赋值

/**
     * 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;
    }
补充7

在补充7出同样调用了dispatchTransformedTouchEvent方法,但传入的child参数是null,这就回到了补充5中所描述的情况,最终事件因为没有子View去处理而回到了ViewGroup(或者说是上层View更好理解)。

2.2.3 View对事件的处理

dispatchTransformedTouchEvent方法最终将事件传递给了View(无论是子View还是上层ViewGroup),都调用到了View类的dispatchTouchEvent方法。下面就来看看这个方法:

a.dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
		...
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            ListenerInfo li = mListenerInfo;
            //这里通过一系列的且运算,为result赋值
            //赋值条件就是子View设置了OnTouchListener
            //且listener的onTouch方法返回的是true
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
			//如果上面的result赋值成功,下面的onTouchEvent方法就不会被调用
			//所以也就是OnTouchListener的优先级高于OnTouchEvent
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
		...
    }
b.onTouchEvent

如果调用到了onTouchEvent方法,其代码如下:

public boolean onTouchEvent(MotionEvent event) {
        ...
        //判断View是否可点击状态
        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 doesn't respond to them.
            //即便View设置了disable标签,同样会消耗这次触摸事件,只是不对此进行回应
            return clickable;
        }
        //判断View是否设置了代理,View代理的处理过程同View本身类似
        //------>见补充8
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
        //条件:可点击或者可长按(ToolTip指可以显示悬浮提示)
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    if ((viewFlags & TOOLTIP) == TOOLTIP) {
                        handleTooltipUp();
                    }
                    ...
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        ...
                        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
                            // 个人推测这里是值只有当前View的状态已经是pressed的时候才会去调用click方法
                            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.
                                // 并非直接就调用performClickInternal(),该方法内部调用了performClick方法
                                // 而是借助Runnable在点击事件开始执行之前更新View的其他可视状态
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClickInternal();
                                }
                            }
                        }
                        ...
                    }
                    ...
                    break;
                // 其他case语句;
                ...
            }
            //设置了click或者longClick就一定返回true
            return true;
        }
        //否则就返回false
        return false;
    }

省略了其他时间语句,只有UP事件才会触发performClick方法,所以对着一个按钮按着不松开并不会有什么事情发生(除了按下的视图状态效果变化了之外,clicklistener中的代码并不会执行)。

c.performClick

performClick中就是直接调用OnClickListener中的onClick方法了。

public boolean performClick() {
        ...
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }
        ...
        return result;
    }

综合了上面的情况来看,也就得出了见了很多的:优先级onTouchListener>onTouchEvent>onClick啦

d.setOnClickListener

前面提到的click flag与longclick flag的设置决定了View消耗响应对应的事件,默认情况下,根据View的类型不同,click flag(以及long)也有不同的值,比如说button就是默认为true,textview则需要额外设置,当然这里的额外并不需要我们自己去set,因为在设置click监听器的时候,已经设置上了。

public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }

longClick监听器的设置方法内部也是如此就不贴出来了。

2.2.4 事件分发机制流程图:

事件分发机制的几大顺序分析完了,最后用流程图的方式更形象的理解一下整个过程:

View事件体系

总的来说,View事件分发机制的运行规则可以概述为:

a.自上而下传递

点击事件产生之后交给Activity来处理,Activity将其传递给PhoneWindow,再传递给DecorView,再由其传递给根ViewGroup,(首先传递给其分发方法)由ViewGroup的拦截方法决定是否拦截(ViewGroup默认是不拦截事件的,即onInterceptTouchEvent默认返回false,如果拦截的话事件就交由自身来处理),如果不拦截,那么事件来到了子元素的分发方法,如果这个子元素是ViewGroup,那么过程同上一步类似;而如果是View,因为其没有拦截方法,就直接调用到了其处理事件方法(方法的优先级上面也提到了),如果消费了此次事件那么这一轮的事件分发就走完了。

b.自下而上传递

事件来到了最下层的View,交由其处理事件方法来处理,如果处理方法返回了true,就表示消耗并处理了这次事件,事件分发就完了;但是如过处理方法返回的是false,那么就表示它不对这个事件进行处理,要将事件再“传”回去,会调用到父View的事件处理方法,如果父View的事件处理方法仍旧返回false,就表示父View也不对这个事件进行处理,继续往上传递,传递的终点就是有父View处理了(那么就结束这一次的事件分发),或者最终回到Activity打止。


参考资料:

《Android开发艺术探索》

《Android进阶之光》

《Android群英传》