大领导给小明安排了任务——Android触摸事件

这是Android触摸事件系列文章的第一篇。

  1. 大领导给小明安排任务——Android触摸事件
  2. 大领导又给小明安排任务——Android触摸事件

大领导安排任务会经历一个“递”的过程:大领导先把任务告诉小领导,小领导再把任务告诉小明。也可能会经历一个“归”的过程:小明告诉小领导做不了,小领导告诉大领导任务完不成。然后,就没有然后了。。。。

Android触摸事件和领导安排任务的过程很相似,也会经历“递”和“归”。这一篇会试着阅读源码来分析ACTION_DOWN事件的这个递归过程。

(ps: 下文中的 粗斜体字 表示引导源码阅读的内心戏)

分发触摸事件起点

写一个包含ViewGroupViewActivity的demo,并在所有和touch有关的方法中打log。当触摸事件发生时,Activity.dispatchTouchEvent()总是第一个被调用,就以这个方法为切入点:

public class Activity{
    private Window mWindow;
    
    //分发触摸事件
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        //让PhoneWindow帮忙分发触摸事件
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
    
    //获得PhoneWindow对象
    public Window getWindow() {
        return mWindow;
    }
    
    //参数太长,省略了
    final void attach(...) {
        ...
        //构造PhoneWindow
        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        ...
    }
}

Activity将事件传递给PhoneWindow

public class PhoneWindow extends Window implements MenuBuilder.Callback {

    // This is the top-level view of the window, containing the window decor.
    //一个窗口的顶层视图
    private DecorView mDecor;
    
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        //将触摸事件交给DecorView分发
        return mDecor.superDispatchTouchEvent(event);
    }
}

//DecorView继承自ViewGroup
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks{

    public boolean superDispatchTouchEvent(MotionEvent event) {
        //事件最终由ViewGroup.dispatchTouchEvent()分发触摸事件
        return super.dispatchTouchEvent(event);
    }
}
  • PhoneWindow继续将事件传递给DecorView,最终调用了ViewGroup.dispatchTouchEvent()
  • 至此可以做一个简单的总结:触摸事件的传递从Activity开始,经过PhoneWindow,到达顶层视图DecorViewDecorView调用了ViewGroup.dispatchTouchEvent()

触摸事件之“递”

  • 在分析View绘制时,也遇到过“dispatchXXX”函数ViewGroup.dispatchDraw(),它用于遍历孩子并触发它们自己绘制自己。dispatchTouchEvent()会不会也遍历孩子并将触摸事件传递给它们? 带着这个疑问来看下源码:
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (!canceled && !intercepted) {
            ...
            //遍历孩子
            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 (!canViewReceivePointerEvents(child)
                      || !isTransformedTouchPointInView(x, y, child, null)) {
                      ev.setTargetAccessibilityFocus(false);
                      continue;
                }
                ...
                //转换触摸坐标并分发给孩子(child参数不为null)
                if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                    //这里的代码也很关键,先埋伏笔1
                }
                 ...
            }
        }
        if (mFirstTouchTarget == null) {
                //这里的代码也很关键,先埋伏笔2
        } else {
            //这里的代码也很关键,先埋伏笔3
        }
    }
    
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;
        ...
        // Perform any necessary transformations and dispatch.
        //进行必要的坐标转换然后分发触摸事件
        if (child == null) {
            //这里的代码也很关键,先埋伏笔3
        } else {
            //将ViewGroup坐标系转换为它孩子的坐标系(坐标原点从ViewGroup左上角移动到孩子左上角)
            final float offsetX = mScrollX - child.mLeft;
            final float offsetY = mScrollY - child.mTop;
            transformedEvent.offsetLocation(offsetX, offsetY);
            if (! child.hasIdentityMatrix()) {
                transformedEvent.transform(child.getInverseMatrix());
            }
            //将触摸事件分发给孩子
            handled = child.dispatchTouchEvent(transformedEvent);
        }
        ...
        return handled;
    }
}

果然没猜错!父控件在ViewGroup.dispatchTouchEvent()中会遍历孩子并将触摸事件分发给被点中的子控件,如果子控件还有孩子,触摸事件的“递”将不断持续,直到叶子结点。 最终View类型的叶子结点调用的是View.dispatchTouchEvent()

public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
    public boolean dispatchTouchEvent(MotionEvent event) {
        ...
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            //1.通知触摸监听器OnTouchListener
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            //2.调用onTouchEvent()
            //只有当OnTouchListener.onTouch()返回false时,onTouchEvent()才有机会被调用
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
        ...
        //返回值就是onTouch()或者onTouchEvent()的返回值
        return result;
    }
    
    ListenerInfo mListenerInfo;
    
    //监听器容器类
    static class ListenerInfo {
        ...
        private OnTouchListener mOnTouchListener;
        ...
    }
    
    //设置触摸监听器
    public void setOnTouchListener(OnTouchListener l) {
        //将监听器存储在监听器容器中
        getListenerInfo().mOnTouchListener = l;
    }
    
    //获得监听器管理实例
    ListenerInfo getListenerInfo() {
        if (mListenerInfo != null) {
            return mListenerInfo;
        }
        mListenerInfo = new ListenerInfo();
        return mListenerInfo;
    }
}
    
  • View.dispatchTouchEvent()是传递触摸事件的终点,消费触摸事件的起点。
  • 消费触摸事件的标志是调用OnTouchListener.onTouch()View.onTouchEvent(),前者优先级高于后者。只有当没有设置OnTouchListener或者onTouch()返回false时,View.onTouchEvent()才会被调用。
  • 读到这里,画一张图总结一下触摸事件之“递”:


    图1
  • 图中ViewGroup层后面的N表示在Activity层和View层之间可能有多个ViewGroup层。
  • 图中自上而下一共有三类层次,触摸事件会从最高层次开始沿着箭头往下层传递。
  • 为简单起见,图中省略了另一种触摸事件的处理方式:OnTouchListener.onTouch
  • 图示触摸事件的传递只是众多传递场景中的一种:被点击的View嵌套在ViewGroup中,ViewGroup在Activity中。

触摸事件之“归”

触摸事件之所以在“递”之后还会发生“归”是因为:分发触摸事件的函数还没有执行完。沿着刚才调用链相反的方向重新看一遍源码:

public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
   /**
     * Implement this method to handle touch screen motion events.
     *
     * @param event The motion event.
     * @return True if the event was handled, false otherwise.
     * 返回true表示触摸事件被消费,否则表示未被消费
     */
    public boolean onTouchEvent(MotionEvent event) {
       ...
       if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            //省略了对不同触摸事件的默认处理
            ...
            //只要控件是可点击的,就表示触摸事件已被消费
            return true;
        }
        //若控件不可点击则不消费触摸事件
        return false;
    }
}

View.dispatchTouchEvent()调用了View.onTouchEvent()后并没有执行完。View.onTouchEvent()的返回值会影响View.dispatchTouchEvent()的返回值:

public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
    public boolean dispatchTouchEvent(MotionEvent event) {
        ...
        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;
            }
        }
        //返回当前View是否消费触摸事件的布尔值
        return result;
    }

同样的,ViewGroup.dispatchTouchEvent()调用了View.dispatchTouchEvent()后也没有执行完,View.dispatchTouchEvent()的返回值会影响ViewGroup.dispatchTouchEvent()的返回值:

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    //触摸链头结点
    private TouchTarget mFirstTouchTarget;
    ...
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (!canceled && !intercepted) {
            ...
            //遍历孩子
            for (int i = childrenCount - 1; i >= 0; i--) {
                ...
                //转换触摸坐标并分发给孩子(child参数不为null)
                if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                      ...
                      //有孩子愿意消费触摸事件,将其插入“触摸链”
                      newTouchTarget = addTouchTarget(child, idBitsToAssign);
                      //表示已经将触摸事件分发给新的触摸目标
                      alreadyDispatchedToNewTouchTarget = true;
                      break;
                }
                 ...
            }
        }
        if (mFirstTouchTarget == null) {
                //如果没有孩子愿意消费触摸事件,则自己消费(child参数为null)
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
        } else {
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                //遍历触摸链分发触摸事件给所有想接收的孩子
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        //如果已经将触摸事件分发给新的触摸目标,则返回true
                        handled = true;
                    } else {
                        //这里的代码很重要,继续埋伏笔,待下一篇分析。
                    }
                    predecessor = target;
                    target = next;
                }
        }
        ...
        //返回触摸事件是否被孩子或者自己消费的布尔值
        return handled;
    }
    
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;
        ...
        // Perform any necessary transformations and dispatch.
        //进行必要的坐标转换然后分发触摸事件
        if (child == null) {
            //ViewGroup孩子都不愿意消费触摸事件 则其将自己当成View处理(调用View.dispatchTouchEvent())
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            //将触摸事件分发给孩子
        }
        ...
        return handled;
    }
    
    /**
     * Adds a touch target for specified child to the beginning of the list.
     * Assumes the target child is not already present.
     * 添加View到触摸链头部
     * @param child  View
     * @param pointerIdBits
     * @return 新触摸目标
     */
    private TouchTarget addTouchTarget(View child, int pointerIdBits) {
        TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }
}
  • 上面这段代码补全了上一节中买下的伏笔。原来当孩子愿意消费触摸事件时,ViewGroup会将其接入“触摸链”,如果触摸链中没有结点则表示没有孩子愿意消费事件,此时ViewGroup只能自己消费事件。ViewGroupView的子类,他们消费触摸事件的方式一摸一样,都是通过View.dispatchTouchEvent()调用View.onTouchEvent()OnTouchListener.onTouch()
  • 沿着回溯链,再向上“归”一步:
public class Activity {
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            //如果布局中有控件愿意消费触摸事件,则返回true,onTouchEvent()不会被调用
            return true;
        }
        return onTouchEvent(ev);
    }
}

ViewViewGroupActivity,虽然它们分发触摸事件的逻辑不太一样,但基本结构都和上面这段代码神似,用伪代码可以写成:

//“递”
if(分发事件给孩子){
    如果孩子消费了事件 直接返回(将触摸事件被消费这一事实往上传递)
}
//“归”
如果孩子没有消费事件,则自己消费事件

“分发事件给孩子”这个函数的调用表示“递”,即将触摸事件传递给下层。“分发事件给孩子”这个函数的返回表示“归”,即将触摸事件的消费结果回溯给上层,以便上层采取进一步的行动。

同样的套路,用图片总结下触摸事件之“归”:


图2
  • 这张图是对图1描述场景的补全。图中黑色的线表示触摸事件的传递路径,灰色的线表示触摸事件回溯的路径。
  • 因为View.onTouchEvent()返回true,表示消费触摸事件,所以ViewGroup.onTouchEvent()以及Activity.onTouchEvent()都不会被调用。
图3
  • 这张图是对图1描述场景的扩展。图中黑色的线表示触摸事件的传递路径,灰色的线表示触摸事件回溯的路径。
  • 图示所对应的场景是:被点击的View不消费触摸事件,而ViewGrouponTouchEvent()中返回true自己消费触摸事件。
图4
  • 这张图是对图1描述场景的扩展。图中黑色的线表示触摸事件的传递路径,灰色的线表示触摸事件回溯的路径。
  • 图示所对应的场景是:被点击的ViewViewGroup都不消费触摸事件,最后只能由Activity来消费触摸事件。

总结

  • Activity接收到触摸事件后,会传递给PhoneWindow,再传递给DecorView,由DecorView调用ViewGroup.dispatchTouchEvent()自顶向下分发ACTION_DOWN触摸事件。
  • ACTION_DOWN事件通过ViewGroup.dispatchTouchEvent()DecorView经过若干个ViewGroup层层传递下去,最终到达View
  • 每个层次都可以通过在onTouchEvent()OnTouchListener.onTouch()返回true,来告诉自己的父控件触摸事件被消费。只有当下层控件不消费触摸事件时,其父控件才有机会自己消费。
  • 触摸事件的传递是从根视图自顶向下“递”的过程,触摸事件的消费是自下而上“归”的过程。

读到这里可能对于触摸事件还充满诸多疑问:

  1. ViewGroup层是否有办法拦截触摸事件?
  2. ACTION_DOWN只是触摸序列的起点,后序的ACTION_MOVEACTION_UPACTION_CANCEL是如何传递的?

这些问题会在下一篇继续分析。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,875评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,569评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,475评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,459评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,537评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,563评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,580评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,326评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,773评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,086评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,252评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,921评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,566评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,190评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,435评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,129评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,125评论 2 352

推荐阅读更多精彩内容