从源码看 Android 事件分发
Android 事件分发是安卓中一个很精妙的地方,很多开发人员有些不解,为什么呢?因为不知道怎么去运用,其实这个地方理解起来比较简单,我写这么一篇文章就是为了串一串这个知识点。
脱离源码浅谈分发
- 事件分发是在最上层 View 一直往下层传递事件,并通过 onTouchEvent(MotionEvent ev) 这个方法来分发的,如果某个 View 想处理这个事件就需要重写这个 onTouchEvent() 并且返回 True ,然后写自己的逻辑处理事件。
- 如果是 ViewGroup 想处理这个事件呢,那么他就要重写 onInterceptTouchEvent(MotionEvent ev) 方法并返回 True ,来拦截这个事件并交给 ViewGroup 的 onTouchEvent() 来处理这个事件。
- 如果 ViewGroup 里面的子 View 想处理这个事件怎么办,不要担心 Android 给我们提供了 requestDisallowInterceptTouchEvent(boolean b) 方法然后传入 True 就会阻止上一级 ViewGroup 拦截事件,然后子 View 自己来处理这个事件。
- 上面三条结论我们怎么得出的呢?这就是 dispatchTouchEvent(MotionEvent ev) 方法的具体实现
- 处理 ACTION_DOWN 事件的 View 就是事件分发的目标,剩余的事件会直接分发到当前 View
滑动冲突有两种办法
- 如果两个上下级 View 冲突,那么给上级 View 设置 setOnTouchListener() 然后先把 ACTION_DOWN 分发给子 View ,然后在 ACTION_MOVE 中去判断具体的业务逻辑,确定到底那个 View 去处理本次事件。根据业务决定返回 True,False。
- 如果两个上下级 View 冲突,那么在下级 View 重写 onTouchEvent() ,然后先把 ACTION_DOWN 用 requestDisallowInterceptTouchEvent(true) 把上层事件处理拦截掉 ,然后在 ACTION_MOVE 中去判断具体的业务逻辑,确定那个 View 去处理本次事件,如果确定是上层 View 处理事件,则用 requestDisallowInterceptTouchEvent(false) 让上层 View 继续处理事件。
从源码看待事件分发
本次源码基于 Android 8.0 API 27
首先我们要知道事件分发的事件先发到 window 然后调用 Activity 或 Dialog 等组件的事件分发,然后分发到 window 再分发到 DecorView,再往我们自己的布局分发,这个流程我们将到 window 解析的时候讲,我们现在就当他是在 Activity 直接分发到我们自己的布局
1. 我们先从 ViewGroup 的 dispatchTouchEvent() 方法来说起
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
//检查点击是否合法
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
if (actionMasked == MotionEvent.ACTION_DOWN) {
//如果之前有未处理完的事件则取消掉,具体实现看方法里面的源码,本质是往下分发 ACTION_CANCEL 事件
cancelAndClearTouchTargets(ev);
//清除 requestDisallowInterceptTouchEvent() 方法的标志,也就是每次 ACTION_DOWN 的时候这个标志都会取消
resetTouchState();
}
//检查是否拦截事件
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 如果 onInterceptTouchEvent 返回 True 则拦截事件,系统控件一般情况都是 False
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
//没有触摸目标默认为拦截掉事件
intercepted = true;
}
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
// 检查是否取消事件
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
//没有取消没有拦截
if (!canceled && !intercepted) {
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
//查找点击的 View
for (int i = childrenCount - 1; i >= 0; i--) {
...
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
mLastTouchDownTime = ev.getDownTime();
...
//把新找到的触摸对象放到链表里
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
//没有找到接受事件的 View,交给最近处理事件的 View
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
if (mFirstTouchTarget == 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) {
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;
}
}
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
// 遇到事件取消、手指抬起、鼠标移过取消按钮状态
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
mFirstTouchTarget 是记录点击的一个链表
我们再看一下分发事件的方法 dispatchTransformedTouchEvent()
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
final int oldAction = event.getAction();
//如果事件被取消了则往下分发取消的事件
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
...
//继续放下分发
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
...
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
}
// Perform any necessary transformations and dispatch.
if (child == null) {
//如果没有子 View 则调用 View 的 dispatchTouchEvent() 方法,注意 View 和 ViewGroup 的 dispatchTouchEvent() 不一样,具体看下面
handled = super.dispatchTouchEvent(transformedEvent);
} else {
...
//往下分发,调用子 View/ViewGroup 的 dispatchTouchEvent() 方法
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
欧克,到此为止 ViewGroup 的两个主要的方法就差不多分析完了,我感觉我的注释打的也很全,有人会问了为什么没有 onTouchEvent() 方法,因为 ViewGroup 没有这个方法,等会我们分析 View 会将这个方法。
2. 接下来我们再看一下 View 的 dispatchTouchEvent() 方法
public boolean dispatchTouchEvent(MotionEvent event) {
...
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 如果开始 ACTION_DOWN 事件则取消当前的滑动
stopNestedScroll();
}
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//这个对象的作用就如他的名字,存储各种监听接口
ListenerInfo li = mListenerInfo;
//首先 ListenerInfo 不为空(则里面有监听对象),并且有 TouchListener 不为空,
//并且控件的状态是启用的,
//并且 TouchListener 的 onTouch 返回 true 说明事件被外面的 TouchListener 处理了,
//这就说明了外面设置监听接口优先级高于自己本身的 onTouchEvent() 方法,
//这就是我们解决滑动冲突第一种方法的依据
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//如果上述没有处理事件,则调用自身的 onTouchEvent() 方法
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
//如果手指抬起、时间取消、手指按下并由本 View 处理了事件,则停止 View 的滑动
stopNestedScroll();
}
return result;
}
接下来我们再看一看我们心心念念的 onTouchEvent() 方法
public boolean onTouchEvent(MotionEvent event) {
...
//判断事件点击
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
//如果当前 View 没有启用则不处理该事件
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
return clickable;
}
//代理对象
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp();
}
if (!clickable) {
removeTapCallback();
//取消长按点击的延时操作
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
setPressed(true, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
removeLongPressCallback();
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
//处理点击事件
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
}
mHasPerformedLongPress = false;
if (!clickable) {
//处理长按点击,此处是用延时队列实现的,所有很多不符合的条件下要删除掉这个延时事件
checkForLongClick(0, x, y);
break;
}
if (performButtonActionOnTouchDown(event)) {
break;
}
//能否在容器内滚动
boolean isInScrollingContainer = isInScrollingContainer();
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
setPressed(true, x, y);
checkForLongClick(0, x, y);
}
break;
case MotionEvent.ACTION_CANCEL:
...
//清除各种状态
break;
case MotionEvent.ACTION_MOVE:
if (clickable) {
drawableHotspotChanged(x, y);
}
if (!pointInView(x, y, mTouchSlop)) {
//如果手指移动到 View 的外部则停止长按等事件
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
break;
}
return true;
}
return false;
}
我们事件分发的内容基本也就这么多了,其实光讲事件分发的话 onTouchEvent() 这个方法将的有点多余了,其实与我们当前内容也没有多大的关系。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
-
上一篇
Android Recyclerview 实现画廊功能
你有你的各种需(借)求(鉴),我有我的各种措施,不是很强,但是很酷! 安排~(听说这句话最近很火) 最近啊,公司有个需求,想实现一个画廊效果,我找了两个App去给他看,问他是要类似于viewpager的有翻页的效果,还是想要跟列表似的,手滑动到哪里就停留在哪里。 然后产品回复我说:就跟它的一样(某个app),按照他的做就行,我的心里有些波澜,甚至想****;好了不说了。写效果吧。 gif.gif 不知道为什么是反着的,啊,凑合看吧。就是这种卡片轮播。 其他的不说了,直接上代码。 //recyclerview滑动监听 hignHeadRecycler.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); int childCount = recyclerView.getChildCount();...
-
下一篇
Spring_总结_01_Spring概述
一、前言 从今天开始,重新总结一下Spring的相关知识,主要是结合《Spring实战(第四版)》和 《JavaEE开发的颠覆者——SpringBoot实战》这两本书以及官方文档,同时加入自己的一点点点小小小理解,如有不当之处,还请指正。 二、Spring最根本的使命 Spring框架是一个轻量级的企业级开发的一站式解决方案,所谓解决方案就是可以基于Spring解决JavaEE开发的所有问题。 事实上,Spring也确实在不断朝这个方向努力,从官方上列出的Spring产品就可以看出,同时这也反映出Spring最根本的使命:简化Java开发。 三、Spring框架的四大原则 为了简化Java开发,Spring框架所有功能的设计和实现都基于四大原则: (1)使用POJO进行轻量级和最小侵入式开发 (2)通过依赖注入和基于接口编程实现松耦合 (3)通过AOP和默认习惯进行声明式编程 (4)使用AOP和模板(template)减少模式化代码 四、Spring 的六大模块 Spring是模块化的,这意味着你可以只使用你需要的 Spring 的模块。 ...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS7,8上快速安装Gitea,搭建Git服务器
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- MySQL8.0.19开启GTID主从同步CentOS8
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- 面试大杂烩
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS7设置SWAP分区,小内存服务器的救世主
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- SpringBoot2全家桶,快速入门学习开发网站教程