博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
从另一个思路来学习安卓事件分发机制
阅读量:6992 次
发布时间:2019-06-27

本文共 11119 字,大约阅读时间需要 37 分钟。

从另一个思路来学习安卓事件分发机制

前言

事件分发机制是一个安卓老生常谈的话题了,从前几年的面试必问题到如今的基本当成默认都会的基础知识。关于这方面的博客网上已经有很多很多了,有从源码分析的,有从实际出发开始分析的等等。面对这么多的教程,小白可能一头雾水不知道从哪里看起,而且看完之后感觉啥也没留下。那么我打算从一个全新的角度全新的思路来讲解这个问题。

全新的角度

If I have seen further, it is by standing on the shoulders of giants.

站在前辈的肩膀上,我们能看得更远。所以我们先来看看大佬们已经为我们总结出来的事件分发流程

(图片转自 的博客 )

从图上来看我们可以总结出这么几个相关方法

public boolean dispatchTouchEvent(MotionEvent ev) {...}public boolean onInterceptTouchEvent(MotionEvent ev) {...}public boolean onTouchEvent(MotionEvent event) {...}复制代码

那么我们就来重点看下这几个方法:

//首先是Activity中的这个方法    public boolean dispatchTouchEvent(MotionEvent ev) {        if (ev.getAction() == MotionEvent.ACTION_DOWN) {            //这是一个空方法,里面没有任何逻辑,不会对事件分发造成影响            onUserInteraction();        }        //重点是这里的调用Window的superDispatchTouchEvent方法        //具体逻辑中这里会调用的顶级view的dispatchTouchEvent方法        if (getWindow().superDispatchTouchEvent(ev)) {            return true;        }        return onTouchEvent(ev);    }复制代码

上面的Window中的superDispatchTouchEvent方法的代码我就不贴了,我们只要知道他会调用Activity中顶级视图view的dispatchTouchEvent方法。接下来的事件传递都在View中了,这部分才是我们需要关心的。

//接着是ViewGroup中的dispatchTouchEvent方法,由于代码太多,我们直接看和onInterceptTouchEvent方法有关的部分.    //应为我们从大佬的博客中得知ViewGroup里的dispatchTouchEvent方法中会调用onInterceptTouchEvent方法来判断是否拦截事件.            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);                } else {                    intercepted = false;                }            } else {                intercepted = true;            }复制代码

从上面代码可以看出来当actionMasked为ACTION_DOWN或者mFirstTouchTarget != null时会调用onInterceptTouchEvent。那么这个mFirstTouchTarget到底是个什么玩意呢? 从后面的代码中可以看出这个玩意在 事件被ViewGroup的子元素消费的时候会被复制并指向子元素。 也就是事件被子元素消费后mFirstTouchTarget != null,而当ViewGroup拦截事件时,mFirstTouchTarget为空,所以当接下来的Move和Up一些列事件到来之时,不会再调用onInterceptTouchEvent方法,而是直接交由ViewGroup处理。(应为onInterceptTouchEvent不是每次都会被调用的,所以我们想处理所有事件是,应该选择dispatchTouchEvent方法)

说了半天,这个onInterceptTouchEvent看起来这么重要,那我们就来看下他里面到底是啥样的

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;    }复制代码

可以看到里面的代码很简单,基本上相当于默认返回false。所以可以总结出ViewGroup中的onInterceptTouchEvent方法默认不拦截事件。

接下来看没被拦截时会执行的代码

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
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)) { // 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(); }复制代码

这段代码看着很长,其实逻辑很简单(不需要读懂每一行代码,大概了解思路和执行过程就行)。 遍历ViewGroup中的所有子元素,判断子元素是否能够接受这个事件,如果能够接受则这个事件会被交给该子元素处理。具体代码在这个方法中dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)

if (child == null) {                handled = super.dispatchTouchEvent(event);            } else {                handled = child.dispatchTouchEvent(event);            }复制代码

如果这个这个child.dispatchTouchEvent(event)方法返回true,代表事件被子view消费,mFirstTouchTarget将会被赋值同时跳出循环,如下部分代码:

newTouchTarget = addTouchTarget(child, idBitsToAssign);    alreadyDispatchedToNewTouchTarget = true;    break;//mFirstTouchTarget在此被赋值    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);        target.next = mFirstTouchTarget;        mFirstTouchTarget = target;        return target;    }复制代码

而如果所有子view的dispatchTouchEvent方法都返回false或者没有子view时,会调用如下代码

handled = dispatchTransformedTouchEvent(ev, canceled, null,                        TouchTarget.ALL_POINTER_IDS);复制代码

注意这里传入的第三个参数也就是child为空,根据上面部分的代码来看,当子view为空时会调用ViewGroup的父类的dispatchTouchEvent。那么ViewGroup的部分就到此为止了,接下来是View的部分。

老规矩先来看下View的dispatchTouchEvent方法

//省略了部分无关代码,读者可以自行查看源码    public boolean dispatchTouchEvent(MotionEvent event) {                boolean result = false;                final int actionMasked = event.getActionMasked();        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;    }复制代码

View的dispatchTouchEvent方法比较简单,代码量少了很多。 重点在于这部分代码

ListenerInfo li = mListenerInfo;            if (li != null && li.mOnTouchListener != null                    && (mViewFlags & ENABLED_MASK) == ENABLED                    && li.mOnTouchListener.onTouch(this, event)) {                result = true;            }复制代码

当li!=null且当li.mOnTouchListener != null的时候会去掉用li.mOnTouchListener.onTouch(this, event)方法。并且当onTouch方法返回为true时,dispatchTouchEvent方法返回true。

那么什么时候上诉条件成立呢? 答案是当我们掉用view的setOnTouchListener()方法的时候,而这个onTouch方法就是我们在此设置的。

那么当我们没设置该方法或者onTouch返回为false的时候呢?

if (!result && onTouchEvent(event)) {                result = true;    }复制代码

上述代码解答了我们问题,当result为false的时候回去调用View本身的onTouchEvent方法。所以我们知道onTouch方法会在onTouchEvent之前调用,并且能够在此通过返回true完全消费掉事件使得onTouchEvent方法不会被调用。

按照大佬们博客的说法,当view为可点击时onTouchEvent方法默认返回true表示消费掉了事件,那么我们就去看看该方法里面到底是怎么一回事

//该部分代码比较长,删除了不相关的代码,只保留重要部分,读者可以自行查看源码    public boolean onTouchEvent(MotionEvent event) {        final int action = event.getAction();        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {            switch (action) {                case MotionEvent.ACTION_UP:                    if (mPerformClick == null) {                        mPerformClick = new PerformClick();                    }                    if (!post(mPerformClick)) {                        performClick();                     }                    break;            }            return true;        }        return false;    }//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;        }        return result;    }复制代码

从上面的代码可以看出当view可以点击时会调用performClick()方法,而这个方法会在li != null && li.mOnClickListener != null时去调用onClick方法(也就是我们对view的setOnClickListener方法)。 而不管onClick方法返回什么,只要view是可点击的onTouchEvent方法都会返回true,看样子大佬说的一点都没错(屁话,大佬还能说错了?(╯‵□′)╯︵┻━┻)。

那么这个时候安卓中事件的分发流程好像渐渐清晰了,翻到文章开头的流程图,读者是不是感到已经理解了呢?(如果没有理解,请抛开博客,打开源码,对上述的三个方法的源码细细看一遍)

尾声

其实我感觉这篇写的有点繁琐和乱,主要还是我功力不够,虽然自己能够理解了这部分内容,但是写的时候总是不能完全表达清楚,有种只可意会不可言传的感觉。新手朋友可以多参考几篇博客,多看几遍源码,一次不行就两次,两次不行就三次。世上无难事只怕有心人,只要肯有心总有能学会理解的那一天。

同时安卓新手朋友们可以看看我的另外两篇博客分别是关于AsyncTask和SpannableStringBuilder的。

转载地址:http://awdvl.baihongyu.com/

你可能感兴趣的文章
PHP常用函数
查看>>
界面 切换
查看>>
linux输入及简单的for循环使用
查看>>
Java自带的性能监测工具之jstat
查看>>
我的友情链接
查看>>
通过rsync清除目录的shell脚本
查看>>
我的友情链接
查看>>
vim基本操作
查看>>
快速划分子网的方法
查看>>
Servlet的Web访问名称与Servlet家族简介
查看>>
第一次来
查看>>
Linux命令记录神器history命令基础6
查看>>
TurboMail邮件系统再次通过涉密软件认证
查看>>
JAVA之JNI从头完整实例
查看>>
win10安装解压缩版mysql5.7.25(图解)
查看>>
python redis 分页
查看>>
nmap教程之nmap命令使用示例(nmap使用方法) 服务器***利器
查看>>
[转载] 七龙珠第一部——第118话 天津饭的决心
查看>>
ORACLE DATAGUARD 11G R2 RAC TO RAC
查看>>
AIX slibclean
查看>>