专栏首页ClericYi's BlogAndroid必知必会--事件分发机制

Android必知必会--事件分发机制

事件序列解析

所谓的安卓事件是什么?具体来说的就是点击和滑动两个操作;抽象着来说就是下面的表格。

MotionEvent/事件类型

具体操作

ACTION_DOWN

点下View

ACTION_UP

抬起View

ACTION_MOVE

滑动View

ACTION_CANCEL

非人为因素取消

事件序列一般组成: 点击的事件组成就是:Down --> Up 滑动的事件组成就是:Down --> Move --> Move .... --> Up

事件分发

  1. 使用到的函数
    • dispatchTouchEvent():用于事件分发
    • onTouchEvent():消费事件
    • onInterceptTouchEvent():判断是否拦截事件,仅存在于ViewGroup
  2. 分发对象
    • Activity
    • ViewGroup
    • View

Activity的事件分发

public boolean dispatchTouchEvent(MotionEvent ev) {
    // 从判断语句中可以得出所有事件的起点就是Down
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        // 实现屏保功能
        onUserInteraction();
    }
    // 向上传递至ViewGroup,调用其dispatchTouchEvent
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

文章中我已经添加了注释内容,其中getWindow()获得就是一个Window抽象类,根据其子类PhoneWindow我们可以很容易得知最后调用的其实就是ViewGroupdispatchTouchEvent()方法

/**
 * 实际上就是判断事件是否是DOWN事件,event的坐标是否在边界内等
 */
public boolean onTouchEvent(MotionEvent event) {
    if (mWindow.shouldCloseOnTouch(this, event)) {
        finish();
        return true;
    }
    return false;
}

最后就是Activity中的onTouchEvent()方法了,这个模块干的事情在注释中也就很清晰明了了。

ViewGroup的事件分发

public boolean dispatchTouchEvent(MotionEvent ev) {
    // ········
    // 初始化Down事件
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        // 丢弃之前手头上干的事情,重新开始响应Down事件
        cancelAndClearTouchTargets(ev);
        resetTouchState();
    }
    // 检查是否需要拦截
    final boolean intercepted;
    if (actionMasked == MotionEvent.ACTION_DOWN
            || mFirstTouchTarget != null) {
        // 这个与运算是用于影响除Down以外的事件的
        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 {
        // 当前按压的位置没有控件,或者当前控件并不可被点击,直接被ViewGroup拦截
        intercepted = true;
    }
    // ········
    /**
     *这个判断里面同样的还是判断响应的事件,然后就是通过一个for循环判断位置来判断当前的子控件是否在对应的位置内
     * 还有非常重要的一点就是这个循环的判断还是倒叙的
     */
    if (actionMasked == MotionEvent.ACTION_DOWN
        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
        || actionMasked ==MotionEvent.ACTION_HOVER_MOVE) {
        ········
        if (newTouchTarget == null && childrenCount != 0) {
            ········
            for (int i = childrenCount - 1; i >= 0; i--) {
                final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
                final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
                // 后面的篇幅主要用于判断当前的控件的各种属性是否是满足需要的。比如说位置、是否可以点击、是否隐藏等一系列信息
                ········ 
            }
   ········
}

倒序是为了什么呢? 这个问题同样是一个开发过程中常见,但是却很容易被忽略的问题。

你是否有见过这样的一段代码,如果是ButtonAButtonB这两个按钮是在同一个位置出现,ButtonA略大于ButtonB,也就是下图所示

对应在XML布局文件中的代码一般类似于下面这段。

<Layout>
    <Button id="A"/>
    <Button id="B"/>
</layout>

如果出现点击事件发生在ButtonA上时,只要它有足够的能力势必会被ButtonA所消费。但是如果时出现在ButtonB的区域呢? 这也就体现了倒序去进行事件消费的作用了,如果正序去进行消费的话,那这个事件最后的消费者一定是ButtonA,而我们需要的实际效果是ButtonB在有足够能力消费时交由它进行完成。

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

onInterceptTouchEvent(MotionEvent ev)函数可知,默认其实并不会去拦截。所以就一般情况而言,dispatchTouchEvent()方法是需要去循环遍历子控件集合去寻找对应的控件的。

使用一个伪代码解释以上的逻辑

public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean consume = false;
    if(onInterceptTouchEvent(ev)){
        // 自己拦截,自己消费
        onTouchEvent(ev);
    }else{
        // 不拦截,分发给子View进行消费
        consume = child.dispatchTouchEvent(ev);
    }
    return consume;
}

View事件分发

    public boolean dispatchTouchEvent(MotionEvent event) {
        // ·····
        boolean result = false;

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }
        
        if (onFilterTouchEventForSecurity(event)) {
            // ·····
            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);
        }
        return result;
    }

通过以往的实践,我们知道只有通过设置了监听器的View才能够去监听事件,那么在dispatchTouchEvent()方法中也是一样的,如果View并没有被设置监听器,变量result也不会被赋值成为true。

从代码中很容易看出onTouch()方法的优先级大于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 (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    // ·····
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {

                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            removeLongPressCallback();

                            if (!focusTaken) {
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClickInternal();
                                }
                            }
                        }
                        // ·····
                    }
                    // ·····
                    mIgnoreNextUpEvent = false;
                    break;
                // ·····
            }
            return true;
        }
        return false;
    }

onTouchEvent()方法中其实具体干了一件事情,那就是区别到底是长按事件还是点击事件。

那么先行判断的是长按事件还是点击事件呢?答案很明显,在代码行中removeLongPressCallback();有一个这样的函数,这就是去除长按事件回调的函数,所以答案就是长按事件是第一个被判断的事件,然后才是点击事件。

判断这个方法的事件的方法就是通过做出Up动作时的时间和做出Down动作时的时间间隔。如果Down和Up两个动作之间的时间间隔小于500ms,就是点击事件。

总结

本文分享自微信公众号 - DevGW(ClericYi_Android),作者:ClericYi

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-07-15

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 锦囊篇|一文摸懂ButterKnife

    于2020年3月26号,Jack Wharton 官宣 ButterKnife 废弃了。但是我们就没有必要去学习了吗?显然并不是这样的,一个开源库背后的每一分思...

    ClericYi
  • 关于Python的小小分享

    但是为了论证Python背后社区的强大性,显然还需要一个对比,这里拿StackOverflow上的不同tag来进行比较。

    ClericYi
  • 锦囊篇|Android自定义View

    View,有很多的名称。不论是你熟知的布局,还是控件,他们全部都继承自View。

    ClericYi
  • 「R」R 的控制结构

    编程语言学多了你会发现虽然每一门编程语言都有其各自的语法、特性,但它们都有着一些程序内秉的框架:数据类型、条件判断、循环,所以才有了那句言简意赅的定义:

    王诗翔呀
  • python如何求100以内的素数

    到此这篇关于python如何求100以内的素数的文章就介绍到这了,更多相关如何用python求100以内的素数内容请搜索ZaLou.Cn

    砸漏
  • 给你一份Spring Boot核心知识清单

    在过去两三年的 Spring 生态圈,最让人兴奋的莫过于 Spring Boot 框架。或许从命名上就能看出这个框架的设计初衷:快速的启动 Spring 应用。...

    java架构师
  • 给你一份SpringBoot知识清单

    在过去两三年的Spring生态圈,最让人兴奋的莫过于Spring Boot框架。或许从命名上就能看出这个框架的设计初衷:快速的启动Spring应用。因而Spri...

    java思维导图
  • 这份3万字的Spring Boot知识清单,请查收!

    在过去两三年的Spring生态圈,最让人兴奋的莫过于Spring Boot框架。或许从命名上就能看出这个框架的设计初衷:快速的启动Spring应用。因而Spri...

    黄泽杰
  • 给你一份Spring Boot核心知识清单

    在过去两三年的 Spring 生态圈,最让人兴奋的莫过于 Spring Boot 框架。或许从命名上就能看出这个框架的设计初衷:快速的启动 Spring 应用。...

    一个优秀的废人
  • HDUOJ------Daydream字符查找-并求其始末位置

    2013-07-17  10:50:38 Daydream Time Limit: 2000/1000 MS (Java/Others)    Memory L...

    Gxjun

扫码关注云+社区

领取腾讯云代金券