前言
“大哥大哥,快来,领导叫你过去面试!!”
“来了来了!”
看了看简历,面试的是中级安卓开发。
“你来说一下安卓事件分发机制吧!”
…
“大哥,老实说,事件分发我也不咋会,还幸亏你面试我的时候高抬贵手,没问我,要问我的话我就进不来咱们公司了!”
“抬啥手,你面试的时候面试的是实习生,我问你这干啥?”
“奥,好吧。。。。话说回来,大哥,你能给我讲讲事件分发嘛?”
“行吧,那就给你从头到尾说一遍吧,这个以后都会有用的。”
“谢谢大哥,我去给大哥泡咖啡!!!”
正文
事件分发,安卓开发中老生常谈的一个问题了,不仅仅是为了应付面试,实际工作中也会经常用到(这里要说明一下,平时尽量要把知识学习的牢固一些,不要等到面试了才临时抱佛脚)。下面我会将事件分发好好扒一扒!
Activity构成
“小子,给我说一下Activity的构成吧!”
“构成?Activity不就是Activity嘛!啥构成啊?”
“。。。。。。记好了。。。。。。”
一个Activity包含了一个Window对象,Window是由PhoneWindow来实现的。PhoneWindow将DecorView作为整个应用窗口的根View,而这个DecorView又将屏幕划分为两个区域:一个是 TitleView,另一个是ContentView,而我们平时所写的就是展示在ContentView中的。
“好像有点明白了,但是大哥,这和事件分发有什么关系呢?”
“猴急的性子就是改不了,马上要说了!”
事件类型
触摸事件对应的是MotionEvent类,事件的类型主要有以下三种:
- ACTION_DOWN 在屏幕按下时触发
- ACTION_MOVE 在屏幕上滑动时触发(移动的距离超过一定的阈值才会被判定为ACTION_MOVE操作)
- ACTION_UP 在屏幕抬起时触发
- ACTION_CANCLE 当滑动超出控件边界时触发
“大哥,你刚刚说的阈值到底是多少啊?我记得以前看别人写的博客也这样说的,但一直没搞明白阈值到底是多少?”
Log.i("ViewConfiguration", "TouchSlop=" + ViewConfiguration.get(this).scaledTouchSlop)
“不错不错,小子,求知欲很强嘛!”
TouchSlop(系统常量)表示最小移动阈值,默认值一般为8dp。这个相信都能明白,意思就是如果你滑动小于这个值的话系统就会认为你这不属于滑动事件。这里需要注意,有的手机厂商为了“提高用户体验”,重新设置了这个值的大小,如果不确定可以打印下看看。
下面是执行结果:
2020-05-07 22:12:35.939 12599-12599/com.zj.weather I/ViewConfiguration: TouchSlop=21
“明白了吗?”
“呃呃。。。大哥你继续讲吧,我能听懂!”
其实View事件分发的本质很简单,当一个MotionEvent事件发生后,系统将这个点击事件传递到一个具体的View上,就是对MotionEvent事件分发的过程。
事件分发流程
“小子,来给我说说事件分发过程由哪几个方法完成的!”
“嗯。。。有dispatchTouchEvent,还有onInterceptTouchEvent和onTouchEvent三个方法完成的。”
“不错不错,那你给我说说这三个方法的执行过程吧”
“大哥,你直接说吧,我只知道有这三个方法。。。。”
“好,不为难你了,下面要认真听了。”
咱们就一个一个方法来说吧!
**dispatchTouchEvent(分发):**当方法返回值为true时表示事件被当前视图消费掉;如果返回false时则表示交给父类中的onTouchEvent进行处理;如果返回为super.dispatchTouchEvent时则表示会继续分发该事件。
**onInterceptTouchEvent(拦截):**当方法返回值为true时表示拦截这个事件并交由自身的onTouchEvent方法进行消费;如果返回false则表示不拦截,需要继续传递给子视图。如果返回super.onTouchEvent(ev),这块就有点麻烦了,分为两种情况:
- 如果该View存在子View且点击到了该子View, 则不拦截, 继续分发给子View 处理, 此时相当于返回 false
- 如果该View没有子View或者有子View但是没有点击中子View(此时ViewGroup 相当于普通View), 则交由该View的onTouchEvent响应,此时相当于返回true。
这块需要注意的是:LinearLayout、 RelativeLayout、FrameLayout等ViewGroup默认不拦截, 而 ScrollView、ListView等ViewGroup则可能拦截,需要看具体情况。
**onTouchEvent(消费):**方法返回值为true表示当前视图可以处理对应的事件;返回值为false表示当前视图不处理这个事件,它会被传递给父视图的onTouchEvent方法进行处理。如果返回super.onTouchEvent(ev),事件处理又分为了两种情况:
- 如果该View是clickable或者longclickable的,则会返回true, 表示消费 了该事件, 与返回true一样
- 如果该View不是clickable或者longclickable的,则会返回false, 表示不 消费该事件,将会向上传递,与返回false一样。
“小子,听懂了吗?所谓的事件分发就这么简单”
“啊?这都啥是啥啊!根本不理解,大哥,你能再好好说说吗?”
“真拿你没办法,行,听好了!”
你看啊,安卓中有事件传递处理能力的也就有Activity、ViewGroup、和View三种,你就想,Activity和View拦截事件干嘛呢?他们又不管,所以他们有分发和消费不就够了嘛。但是ViewGroup不一样啊,它里面会包含有子View,所以才要有拦截事件,没问题吧?算了,给你写一个伪代码吧!
fun dispatchTouchEvent(ev: MotionEvent?): Boolean { var result = false if (onInterceptTouchEvent(ev)) { result = onTouchEvent(ev) } else { result = child.dispatchTouchEvent(ev) } return result }
“小子,这样总行了吧?”
“额。。。。大哥,你再说说吧。。。”
你看上面的代码,对应一个根ViewGroup来说,点击事件产 生后,首先会传递给它,这个时候它的dispatchTouchEvent方法就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前事件,接着事件就会交给这个ViewGroup处 理,这时如果它的mOnTouchListener被设置,则onTouch会被调用,否则的话onTouchEvent会被调用。 在onTouchEvent中,如果设置了mOnCLickListener,则onClick会被调用。这里需要注意,只要View的CLICKABLE和 LONG_CLICKABLE有一个为true,onTouchEvent就会返回true消耗这个事件;如果onInterceptTouchEvent方法返回false就表示它不拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的dispatchTouchEvent方法就会被调用,就这样一直反复直到事件被最终处理。
“小子,是不是很简单?”
“听你说的很简单,但我还要再好好消化消化!”
“嗯,确实,这块其实很麻烦,简单的还好说,如果多层滑动布局进行嵌套要处理滑动冲突就比较麻烦了。帮你总结一下吧!”
小总结
- 事件传递优先级:onTouchListener.onTouch > onTouchEvent > onClickListener.onClick
- ViewGroup默认不拦截任何事件(返回false)
- View的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable和 longClickable同时为false)
- View的longClickable属性默认都为false,clickable属性要分情况,比如 Button的clickable属性默认为true,而TextView的clickable默认为false
- View的enable属性不影响onTouchEvent的默认返回值
通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是 ACTION_DOWN事件除外
“记着上面这几点,你理解起来会更快一些的!”
“感谢大哥!”
开发中的问题
“对了大哥,ACTION_CANCEL什么时候触发啊,如果触摸button然后滑动到外部抬起会触发点击事件吗,再滑动回去抬起会么?”
“你能不能一个问题一个问题问。。。。”
- 一般ACTION_CANCEL和ACTION_UP都作为View一段事件处理的结束。如果在父View中拦截 ACTION_UP或ACTION_MOVE,在第一次父视图拦截消息的瞬间,父视图指定子视图不接受后续消息了,同时子视图会收到ACTION_CANCEL事件。
- 如果触摸某个控件,但是又不是在这个控件的区域上抬起(移动到别的地方),就会出现 ACTION_CANCEL。
“理解了嘛?”
“嗯嗯,这个知道了,但我还有问题,如果点击事件被拦截了,但是我又想传到下面的View,该怎么搞啊?”
“这个简单!只要重写子类的requestDisallowInterceptTouchEvent()方法返回true就不会执行父类的 onInterceptTouchEvent(),就可以将点击事件传到下面的View了。”
“大哥,那我平时遇到的滑动冲突该怎么解决啊?”
“这个说起来就比较麻烦了,大概给你说下思路吧,这个需要你以后开发中慢慢体会!”
滑动冲突的处理规则:
- 对于由于外部滑动和内部滑动方向不一致导致的滑动冲突,可以根据滑动的方向判断谁来拦截事件。
- 对于由于外部滑动方向和内部滑动方向一致导致的滑动冲突,可以根据业务需求,规定何时让外部 View拦截事件,何时由内部View拦截事件。
- 对于上面两种情况的嵌套,相对复杂,可同样根据需求在业务上找到突破点。
滑动冲突的实现方法:
- 外部拦截法:指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,否则就不拦 截。具体方法:需要重写父容器的onInterceptTouchEvent方法,在内部做出相应的拦截。
- 内部拦截法:指父容器不拦截任何事件,而将所有的事件都传递给子容器,如果子容器需要此事件 就直接消耗,否则就交由父容器进行处理。具体方法:需要配合 requestDisallowInterceptTouchEvent方法
总结
“小子,这样总可以了吧?”
“大哥,我还有问题。。。。”
“放过我吧,这么晚了,有问题明天再说吧!”
本文简单描述了一下安卓的事件分发机制,描述的不是特别详细,大家可以自己写个小Demo试试,加深一下理解。