不够深入,绝对浅出的 Android 触摸事件机制

引子

现代人每天看的和触摸的最多的,莫过于手机屏幕,安卓开发中触摸事件的分发机制也是很有意思的一部分,本文就和大家一起不深入但浅出的学习一下Android触摸事件机制。

一、触摸事件

对于触摸(Touch)触发的事件,在Android中,事件主要包括点按(onClick)、长按(onLongClick)、拖拽(onDrag)、滑动(onScroll)等,点按又包括单击和双击,另外还包括单指操作和多指操作。其中Touch的第一个状态是 ACTION_DOWN, 表示按下了屏幕。之后,touch将会有后续事件,比如移动、抬起等,一个Action_DOWN, n个ACTION_MOVE, 1个ACTION_UP,就构成了Android中众多的事件。触摸事件的产生序列通常是这样的:

触摸事件产生序列
  • 按下(ACTION_DOWN) //表示用户按下了屏幕
  • 移动(ACTION_MOVE) //表示用户在屏幕移动
  • 抬起(ACTION_UP) //表示用户离开屏幕
  • 取消手势(ACTION_CANCEL) //表示,不会由用户产生,而是由程序产生的

这几个事件在代码中如此定义:

public final class MotionEvent extends InputEvent implements Parcelable {
    // 代码省略
    
    public static final int ACTION_DOWN             = 0;    // 按下事件
    
    public static final int ACTION_UP               = 1;    // 抬起事件 
    
    public static final int ACTION_MOVE             = 2;    // 手势移动事件
    
    public static final int ACTION_CANCEL           = 3;    // 取消
  // 代码省略
}

当然触摸产生的事件远远不止这几个,只不过一般接触的最多的是这几个罢了。

所有的操作事件首先必须执行的是按下操作(ACTION_DOWN),之后所有的操作都是以此作为前提,当按下操作完成后,接下来可能是一段移动(ACTION_MOVE)然后抬起(ACTION_UP),或者是按下操作执行完成后没有移动就直接抬起。

二、与触摸有关的组件及操作

2.1 组件

所有的事件操作都发生在触摸屏上,而在屏幕上与用户交互的就是各种各样的视图组件(View),在Android中,所有的视图都继承于View,另外通过各种布局组件(ViewGroup)来对View进行布局,ViewGroup也继承于View。所有的UI控件例如Button、TextView都是继承于View,而所有的布局控件例如RelativeLayout、容器控件例如ListView都是继承于ViewGroup。所以,事件操作主要就是发生在View和ViewGroup之间。

2.2 操作

与触摸事件有关的操作有如下3个方法:

  1. public boolean dispatchTouchEvent(MotionEvent event)
  2. public boolean onTouchEvent(MotionEvent event)
  3. public boolean onInterceptTouchEvent(MotionEvent event)

在View和ViewGroup中都存在dispatchTouchEventonTouchEvent方法,特别的,在ViewGroup中还有一个onInterceptTouchEvent方法。这些方法的返回值全部都是boolean型,都返回true或者是false,这是因为事件传递的过程就是一个接一个,某一个点后根据方法boolean的返回值判断是否要继续往下传递。

三个方法可以总结为一张表格:

Touch 事件相关方法 方法功能 ViewGroup View Activity
public boolean dispatchTouchEvent(MotionEvent ev) 事件分发 Yes Yes Yes
public boolean onInterceptTouchEvent(MotionEvent ev) 事件拦截 Yes Yes No
public boolean onTouchEvent(MotionEvent ev) 事件响应 Yes Yes Yes

从这张表中我们可以看到 ViewGroup 和 View 对与 Touch 事件相关的三个方法均能响应,而 Activity 对 onInterceptTouchEvent(MotionEvent ev) 也就是事件拦截不进行响应。

另外需要注意的是 View 对 dispatchTouchEvent(MotionEvent ev)onInterceptTouchEvent(MotionEvent ev)的响应的前提是可以向该 View 中添加子 View,如果当前的 View 已经是一个最小的单元 View(比如 TextView),那么就无法向这个最小 View 中添加子 View,也就无法向子 View 进行事件的分发和拦截,所以它没有dispatchTouchEvent(MotionEvent ev)onInterceptTouchEvent(MotionEvent ev),只有 onTouchEvent(MotionEvent ev)

三、事件分发、拦截和消费

我们先从整体脉络上了解触摸事件机制的整个流程,大致可以按一个倒置的U型图来理解整个机制:

触摸事件机制

触摸事件从Activity触发事件然后传递到布局文件,一层一层的往子容器传递到最底层的view,如果每层布局文件未对该事件进行处理或者消费那么该事件会从最底层开始往上传到Activity进行消费。类似于一个倒置的U型。

我们按这么一个例子来理解这个U型流程:

例子

将其中的细节略微展开,则可以得到这么一张流程图:

触摸事件分发、拦截和消费

3.1 事件的消费

事件的消费主要由view类中的dispatchTouchEvent(MotionEvent ev)函数和onTouchEvent(MotionEvent event)完成,而具体的消费函数则是用户自己定义的OnTouchListener()onClickListener()

dispatchTouchEvent函数位于View.java类中。可被继承View的ViewGroup重写。

public boolean dispatchTouchEvent(MotionEvent event) {
        // If the event should be handled by accessibility focus first.
        if (event.isTargetAccessibilityFocus()) {
            // We don't have focus or no virtual descendant has it, do not handle the event.
            if (!isAccessibilityFocusedViewOrHost()) {
                return false;
            }
            // We have focus and got the event, then use normal event dispatch.
            event.setTargetAccessibilityFocus(false);
        }
        boolean result = false;

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }

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

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

这部分源码最重要的部分在于

             //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判断语句中前3个条件一般都是true, 关键在于li.mOnTouchListener.onTouch(this, event),这个就是我们可以自己复写的OnTouchListener()的内容,返回值也是由我们定义的。如果这个返回值为true,那么就会将resut赋值为true,从而不会执行接下来的onTouchEvent(event)函数。如果返回值为false,则会执行onTouchEvent(event)函数。onTouchEvent(event)主要执行我们为组件添加的ClickListener中的方法。

3.2 事件的分发

事件的拦截由ViewGroup中的dispatchTouchEvent()完成。伪代码可以这么描述:

public boolean dispatchTouchEvent(MotionEvent event) {
    if(onInterceptTouchEvent(ev)){
        consume = onTouchEvent(ev)
    }else{
        consume = child.dispatchTouchEvent(ev)
    }
    return consume;
}

源代码有这么一段:

while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }

这里就是将触摸事件传递给子View的代码部分,其中的handled是整个dispatchTouchEvent的返回值,由View中定义的处理函数决定。

3.3 事件的拦截

在ViewGroup中,如果定义了拦截器,那么将不会将触摸事件进行分发,而是检测自己是否消费,然后根据返回值返回给父ViewGroup。

事件的拦截通过onInterceptTouchEvent(MotionEvent event)实现。

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

源码就简单多了,而且是肯定被之前的分发函数dispatchTouchEvent调用。

四、总结

关于触摸事件的分发、拦截和消费,我们不必纠结源码中的详细实现,重点是了解View和ViewGroup的分发和消费逻辑,保证最终只有一个View会消费事件并成功返回一个布尔值。

参考文章

Android touch 事件传递机制 - 易术军 - 博客园 (cnblogs.com)

Android Touch事件分发过程_Mr.Simple的专栏-CSDN博客

Touch事件传递学习笔记 - 简书 (jianshu.com)

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

推荐阅读更多精彩内容