Android View的事件分发机制

摘要:
当我单击按钮时,Android系统如何确定我单击的是按钮而不是TextView?会有很多搬家事件。之后,顶级视图将根据事件分发机制分发事件。此时,将调用其dispatchTouchEvent方法。假设此ViewGroup的onInterceptTouchEvent方法返回true,这意味着它将拦截当前事件。然后调用getWindow方法获取与当前活动相关联的窗口,然后调用superDispatchTouchEvent方法将事件传入以进行分发。整个事件周期结束。假设返回false,则没有视图将处理此事件。PrivatedefinalclassDecorViewextendsFrameLayoutoutmplementsRootViewSurfaceMaker可以看到DecorView继承自FrameLayout。

准备了一阵子,一直想写一篇事件分发的文章总结一下。这个知识点实在是太重要了。

一个应用的布局是丰富的,有TextView,ImageView,Button等。这些子View的外层还有ViewGroup。如RelativeLayout。LinearLayout。作为一个开发人员,我们会思考。当点击一个button,Android系统是如何确定我点的就是button而不是TextView的?然后还正确的响应了button的点击事件。

内部经过了一系列什么过程呢?

先铺垫一些知识能更加清晰的理解事件分发机制:
1. 通过setContentView设置的View就是DecorView的子view,即DecorView是父容器。
2. 点击屏幕时,在手指按下和抬起间,会产生非常多事件。down…move…move…up。中间会有非常多的move事件。这一系列的事件为一个事件序列
3. dispatchTouchEvent方法用于分发事件
4. onInterceptTouchEvent方法用于拦截事件
5. onTouchEvent方法用于处理事件

当一个点击事件(MotionEvent)产生后,事件最先传递给当前的界面(Activity),这点是非常好理解的。 Activity再将事件传递给窗体(Window),然后Window将事件传递给顶级View(DecorView)。

此时,事件已经到达了View了。之后顶级View就会依照事件分发机制去分发事件。详细是这种:

对于一个根ViewGroup来说。点击事件产生后,首先会传递给它。这时它的 dispatchTouchEvent 方法就会被调用,假设这个ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前事件。接着事件就会交给这个ViewGroup处理,即它的onTouchEvent方法就会被调用。假设这个ViewGroup的onInterceptTouchEvent方法返回false,就表示它不拦截当前事件。这时当前事件就会继续传递给它的子元素。接着子元素的dispatchTouchEvent方法就会被调用。如此重复直到事件被终于处理。
假设一个View的onTouchEvent方法返回false,那么它的父容器的onTouchEvent方法会被调用。假设它的父容器的onTouchEvent方法还是返回false。那就继续往上抛。当全部的元素都不处理这个事件,那么这个事件会终于传递给Activity处理,即Activity的onTouchEvent方法会被调用。

好了,如今已经铺垫了基础,那么接下来就从源代码的角度来分析事件分发机制。

当然是从Activity的dispatchTouchEvent方法開始分析。源代码例如以下:

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

假设当前事件是down的话,就调用onUserInteraction方法,onUserInteraction是一个空方法,我们能够临时不搭理。然后调用getWindow方法获取到当前Activity关联的Window,Window再调用superDispatchTouchEvent方法将事件传入进行分发。
假设superDispatchTouchEvent方法返回true的话, view已经处理了事件。整个事件循环结束。假设返回false,没有view处理这个事件。

事件往上抛,那就Activity自己处理了。即Activity的onTouchEvent方法会被调用。

由于想要知道事件的整个分发过程,如今关注的是Window的superDispatchTouchEvent方法。那么就跟进去看看:

public abstract boolean superDispatchTouchEvent(MotionEvent event);

Window是一个抽象类,superDispatchTouchEvent是一个抽象的方法,那么我们必须要找到window的实现类才行。但是茫茫人海怎么找呢?看到window类的说明就明确了

* <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

意思是Window存在唯一的实现是android.view.PhoneWindow

那么PhoneWindow里的superDispatchTouchEvent方法就是我们要找的信息。例如以下:

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

直接将事件传递给了DecorView。

这时事件已经是到达View了哦。

那么跟进DecorView的superDispatchTouchEvent方法看看,例如以下:

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

内部调用了父类的dispatchTouchEvent方法。那么DecorView的父类是什么呢?DecorView肯定是View的,那么刚才开篇提到,我们通过setContentView设置的View,是DecorView的子View。

那么更加准确的说DecorView是一个ViewGroup。

private final class DecorView extends FrameLayout implements RootViewSurfaceTaker

能够看到DecorView是继承自FrameLayout。FrameLayout是ViewGroup。也就是说DecorView是一个ViewGroup。

那么如今仅仅须要关注ViewGroup的dispatchTouchEvent方法。

继续前进

ViewGroup的事件分发

ViewGroup的dispatchTouchEvent方法例如以下:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {

        //代码省略

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

        //代码省略

        if (!canceled && !intercepted) {

        //代码省略

                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 = buildOrderedChildList();
                    final boolean customOrder = preorderedList == null
                            && isChildrenDrawingOrderEnabled();
                    final View[] children = mChildren;
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        final int childIndex = customOrder
                                ? getChildDrawingOrder(childrenCount, i) : i;
                        final View child = (preorderedList == null)
                                ?

children[childIndex] : preorderedList.get(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(); } 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; } } } // Dispatch to touch targets. if (mFirstTouchTarget == null) { // No touch targets so treat this as an ordinary view. handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } //代码省略 return handled; }

代码比較长,一点一点分析,先看到一開始的推断

if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) 

mFirstTouchTarget != null的意义是ViewGroup不拦截事件并将事件交由子元素处理,先这样记着。这从后面的addTouchTarget方法能够得出结论的。

然后又会来到这个if推断。

if (!disallowIntercept) {
    intercepted = onInterceptTouchEvent(ev);
    ev.setAction(action); // restore action in case it was changed
}

那我们看看disallowIntercept。而disallowIntercept的赋值过程中。有一个 FLAG_DISALLOW_INTERCEPT 标记位

final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

这个 FLAG_DISALLOW_INTERCEPT 标记位是能够通过requestDisallowInterceptTouchEvent方法来设置的。

回到if (!disallowIntercept)的推断,进入这个if推断后,就会来到

intercepted = onInterceptTouchEvent(ev);

调用onInterceptTouchEvent方法,询问ViewGroup是否拦截事件。

读到这里,能够回顾下开篇时铺垫的结论,对于ViewGroup,点击事件产生后,首先会传递给它。这时它的 dispatchTouchEvent 方法就会被调用。接着会调用它的onInterceptTouchEvent方法。假设这个ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前事件,接着事件就会交给这个ViewGroup处理。即它的onTouchEvent方法就会被调用。

假设返回false表示不拦截,通常ViewGroup也是不拦截事件的。

那如今先分析不拦截的情况。不拦截那就好办了的。

经过一系列的推断。就会来到一个for循环遍历。

for (int i = childrenCount - 1; i >= 0; i--)

这时ViewGroup開始分发传递事件,遍历子元素了。

首先肯定须要过滤掉一些无关点击事件的子元素的,推断子元素能否够接收点击事件,点击事件的坐标是否落在子元素区域内。

if (!canViewReceivePointerEvents(child)
        || !isTransformedTouchPointInView(x, y, child, null)) {
    ev.setTargetAccessibilityFocus(false);
    continue;
}

假设不能够接收点击事件或者点击事件的坐标没有落在子元素区域。就会跳出当前循环。继续遍历下一个子元素。这下就知道了Android系统为什么能够知道点击的是Button而不是TextView。事实上内部就仅仅是做了一个推断嘛。

那么继续分析。子元素符合以上两个条件后,就将事件传递给这个子元素。会来到了这个推断。

if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))

运行dispatchTransformedTouchEvent方法,将子元素传进去。这种方法非常重要。那么跟进看看

/**
 * 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 don't 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;
    }

    //代码省略
}

我们看到child!

=null的情况,假设子元素不为空,调用子元素的dispatchTouchEvent方法继续分发事件,同一时候返回处理结果布尔值,这时就将事件传递到了子View处理。完毕了一轮的事件分发。这种方法先到这里就好。

再看回ViewGroup的dispatchTouchEvent方法,假设dispatchTransformedTouchEvent方法返回true的话,这时事件已经传递给子元素处理,ViewGroup已经无论这个事件了。
那么就会进入if语句。最后会来到addTouchTarget方法。这种方法之前是提到过的,用于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(View child, int pointerIdBits) {
    TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}

事实上就是让mFirstTouchTarget指向子元素。

运行完这个addTouchTarget方法后,终于会到break语句,那么就会跳出整个for循环体。ViewGroup结束分发过程。

又回到dispatchTransformedTouchEvent方法,假设dispatchTransformedTouchEvent方法返回false,那么if语句的一大段代码都不运行了。而是回到for循环继续遍历子元素进行分发。如此重复完毕事件的传递过程。

如今分析ViewGroup拦截事件的情况。假设ViewGroup拦截事件的话,那么就会进入下面这个推断

if (mFirstTouchTarget == null) {
   // No touch targets so treat this as an ordinary view.
   handled = dispatchTransformedTouchEvent(ev, canceled, null,
   TouchTarget.ALL_POINTER_IDS);
}

注意到dispatchTransformedTouchEvent方法的第三个參数child传入的是null,那么就是在dispatchTransformedTouchEvent方法中走下面的语句

if (child == null) {
   handled = super.dispatchTouchEvent(event);
}

而ViewGroup是继承自View的,那么就是ViewGroup自己处理事件了。

这点我们下面分析了View的事件分发过程就能搞明确了。

以上就是ViewGroup的事件分发

那么如今分析已经将事件传递给了子View的情况,View继续调用dispatchTouchEvent方法,那我们看看View的dispatchTouchEvent方法。

View的事件分发

View的dispatchTouchEvent方法源代码例如以下:

/**
 * 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 (onFilterTouchEventForSecurity(event)) {
        //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;
        }
    }

    if (!result && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
    }

    // Clean up after nested scrolls if this is the end of a gesture;
    // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
    // of the gesture.
    if (actionMasked == MotionEvent.ACTION_UP ||
            actionMasked == MotionEvent.ACTION_CANCEL ||
            (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
        stopNestedScroll();
    }

    return result;
}

相比于ViewGroup的dispatchTouchEvent方法,View的dispatchTouchEvent方法代码量少了。也相对简单些了。
首先会来到例如以下推断:

if (li != null && li.mOnTouchListener != null
        && (mViewFlags & ENABLED_MASK) == ENABLED
        && li.mOnTouchListener.onTouch(this, event))

li变量在哪里被赋值的呢?一般是在setOnClickListener方法或setOnTouchListener方法的时候。

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

public void setOnTouchListener(OnTouchListener l) {
    getListenerInfo().mOnTouchListener = l;
}

而这个getListenerInfo()例如以下:

ListenerInfo getListenerInfo() {
    if (mListenerInfo != null) {
        return mListenerInfo;
    }
    mListenerInfo = new ListenerInfo();
    return mListenerInfo;
}

ListenerInfo是一个内部类,里面存放的是各种监听事件的引用。

之后会推断例如以下条件:

li.mOnTouchListener != null

同理仅仅要setOnTouchListener方法设置了,这个引用就不空。

这些都是好理解的。那关键到了。

li.mOnTouchListener.onTouch(this, event)

到了最后一个条件。

这个onTouch方法是我们去实现的,它也返回一个布尔值,假设返回true的话,那么就会进入这个if推断终于返回true,跳出整个方法,那么我们能够看到接下来的onTouchEvent方法是不会得到运行的。
也就是onTouch的运行在onTouchEvent之前。那么假设我们也调用了setOnClickListener方法监听点击事件的话,onClick方法是在哪里调用的呢?我们有理由相信是在onTouchEvent方法里调用的。那么就跟进看看。

public boolean onTouchEvent(MotionEvent event) {

    //代码省略

    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
            (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;

   //代码省略

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

             //代码省略  

                break;
        }

        return true;
    }

    return false;
}

仅仅要CLICKABLE或LONG_CLICKABLE不空, 就会处理这个事件,然而怎么保证CLICKABLE或LONG_CLICKABLE不空呢?事实上细心的你会发现,刚才上面贴出的setOnClickListener源代码中。会将CLICKABL属性设置会true

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

这样就能进入if推断去处理这个事件了,之后就会来到performClick()方法,应该就是它了。跟进去看看吧。

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

    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
    return result;
}

熟悉接口回调机制的你,一定也读懂了performClick()方法的源代码。

li.mOnClickListener.onClick(this);

是在运行这行代码时,调用了我们熟悉的onClick方法

以上就是View的事件分发机制。

此时已经将事件分发机制分析完了。由于我的技术的原因。驾驭的不好,有些关键点还是没分析清晰,但我相信学完了这篇文章能让我和你都对事件分发机制的实现有一个大致的认识,有这个已经能够了。之后还能够一点点去强化锻炼。深入理解事件分发机制。

才干为自己定义控件铺垫良好的基础

免责声明:文章转载自《Android View的事件分发机制》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇Android编程获取手机型号,本机*,sdk版本号及firmware版本号号(即系统版本号号)在excel中如何利用vba通过网址读取网页title(网址是https的)?下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

随便看看

python爬取图片遇见src乱码: data:image/png;base64

Python抓取图像并遇到src乱码:data:image/png;Base64会抓取您喜欢的图像,但图像的src在抓取的代码中出现了乱码:data:image/png;base64.)“”头1,编码=字符串。splitdata=b64decodedwithopenasf:f.写入。close()注意:我还没有成功。。。。。。...

Element-ui tabs标签标题添加自定义图标

关键点:slot="label"{{item.label}}˂iclass="el-icon-questi...

最新版Swagger 3升级指南和新功能体验!

因此,本期将为您带来一篇关于Swagger最新版本的文章。本文将向您展示Swagger最新版本的变化?如何将旧版本的Swagger升级到新版本?Swagger是一个用于生成、描述和调用RESTful接口的Web服务。Swagger 2.9.2的使用分为以下四个步骤:添加依赖项、启用Swagger功能、配置Swagger文档摘要信息和调用接口访问。让我们分别来...

【转】力控的60个经典问题

这是因为在赋值前没有对数组元素做对其指向的变量进行指定。数组元素指定形式:IV[i]=&VAR//表示间接变量IV的第i个元素指向变量VAR。运行后即可在力控的画面中播放Flash动画。支持,力控可以通过PPI、MPI、自由口等方式与S7-200通讯。...

关于异常STATUS_IN_PAGE_ERROR(0xC0000006)

表示0x%p处的指令引用了位于0x%p的内存。由于0x%x的I/O错误状态,未将所需数据放入内存。设备如果读取出错,驱动程序可以返回此异常。其定义如下:////MessageId:STATUS_IN_PAGE_ERROR////MessageText:////Theinstructionat0x%preferencedmemoryat0x%p.Therequ...

Linux终端使用aplay播放wav

Linux终端使用aplay播放wavplay,这是ALSA声音文件记录器的驱动程序。在Linux中,您可以使用以下命令检查用法:manaplay可以用于播放。wav音频文件aplay Dplughw:0,0xxx。wavplughw后面的0,0表示声卡ID和设备ID,这取决于您自己的设备。...