Android View的事件分发机制深入分析讲解
作者:Hdnw 发布时间:2023-06-18 21:28:49
1.分发对象-MotionEvent
事件类型有:
1.ACTION_DOWN-----手指刚接触屏幕
2.ACTION_MOVE------手指在屏幕上移动
3.ACTION_UP------手指从屏幕上松开的一瞬间
4.ACTION_CANCEL-----事件被上层拦截时触发
MotionEvent主要的方法:
getX() | 得到事件发生的x轴坐标(相对于当前视图) |
getY() | 得到事件发生的y轴坐标(相对于当前视图) |
getRawX() | 得到事件发生的x轴坐标(相对于屏幕左顶点) |
getRawY() | 得到事件发生的y轴坐标(相对于屏幕左顶点) |
2.如何传递事件
1.传递流程
底层IMS->ViewRootImpl->activity->viewgroup->view
2.事件分发的源码解析
1.Activity对点击事件的分发过程
Activity#dispatchTouchEvent()
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
//事件交给Activity所附属的Window进行分发,如果返回true,循环结束,返回false,没人处理
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
//所有View的onTouchEvent都返回false,那么Activity的onTouchEvent就会被调用
return onTouchEvent(ev);
}
Window#superDispatchTouchEvent
public abstract boolean superDispatchTouchEvent(MotionEvent event);
PhoneWindow#superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
DecorView#superDispatchTouchEvent()
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
ViewGroup#dispatchTouchEvent()
public boolean dispatchTouchEvent(MotionEvent ev) {<!--{cke_protected}{C}%3C!%2D%2D%20%2D%2D%3E-->
2.顶级View对点击事件的分发过程
把ViewGroup的dispatchTouchEvent()方法中的代码进行分段说明
第一段:
描述的是View是否拦截点击事件这个逻辑
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {//事件类型为down或者mFirstTouchTarget有值
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);//询问是否拦截,方法返回true就拦截
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;//直接拦截了
}
当事件类型为down或者mFirstTouchTarget有值时,就不拦截当前事件,否则直接拦截了这个事件。那么mFirstTouchTarget什么时候有值?当ViewGroup不拦截事件并且把事件交给子元素处理时,mFirstTouchTarget就有值并且指向子元素。所以当事件类型为down并且拦截事件,那么mFirstTouchTarget为空,这会让后面的事件move和up无法满足mFirstTouchTarget有值的条件,直接无法调用onInterceptTouchEvent方法。
特殊情况:通过requestDisallowInterceptTouchEvent方法来设置标记位FLAG_DISALLOW_INTERCEPT,ViewGroup就无法拦截除了ACTION_DOWN以外的点击事件,这个标记位无法影响ACTION_DOWN事件,因为当事件为ACTION_DOWN时,就会重置这个标记位,将导致子View设置的这个标记位无效。
总结:
1.当ViewGroup决定拦截事件后,那么后续的点击事件将会默认交给它处理并且不再调用它的onInterceptTouchEvent方法。
2.当ViewGroup不拦截ACTION_DOWN事件,那么标记位FLAG_DISALLOW_INTERCEPT让ViewGroup不再拦截事件。
第二段:
当ViewGroup不拦截事件时,分发事件给子View,看哪个子View处理事件
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x =
isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
final float y =
isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
//对子元素进行排序
final ArrayList<View> 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 (!child.canReceivePointerEvents()
|| !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;
}
遍历ViewGroup的所有的子元素,判断子元素是否在播动画和点击事件的坐标是否落在子元素的区域内,如果是,就能接收到点击事件,并且事件会传递给它来处理。
我们来看一下dispatchTransformedTouchEvent方法
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
...
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
...
}
dispatchTransformedTouchEvent实际上调用的是子元素的dispatchTouchEvent方法。
如果子元素的dispatchTouchEvent方法返回true,那么mFirstTouchTarget就会被赋值同时跳出for循环
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
这几行代码完成了mFirstTouchTarget的赋值并终止对子元素的遍历,如果子元素的dispatchTouchEvent方法返回false,那么ViewGroup就会把事件分发给下一个子元素。
其实mFirstTouchTarget真正的赋值是在addTouchTarget方法里面,mFirstTouchTarget是一种单链表结构。
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
第三段:
执行事件
//当前View的事件处理代码
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
//子View的事件处理代码
...
dispatchTransformedTouchEvent方法的第三个参数为null,则会调用super.dispatchTouchEvent方法,也就是View的dispatchTouchEvent方法,所以点击事件给View处理。
View对点击事件的处理过程
View(不包含ViewGroup)是一个单独的元素,没有子元素,只能自己处理事件。
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;
}
}
...
return result;
}
首先判断是否设置了OnTouchListener,如果OnTouchListener的onTouch方法返回true,就不会调用onTouchEvent方法,否则就会调用onTouchEvent方法。
public boolean onTouchEvent(MotionEvent event) {
...
if ((viewFlags & ENABLED_MASK) == DISABLED
&& (mPrivateFlags4 & PFLAG4_ALLOW_CLICK_WHEN_DISABLED) == 0) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
...
}
不可用状态下的View照样会消耗点击事件
switch (action) {
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp();
}
...
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
...
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
}
...
mIgnoreNextUpEvent = false;
break;
当ACTION_UP事件发生时,会触发performClick方法,如果View设置了OnClickListener,那么performClick方法内部会调用它的onClick方法。
private boolean performClickInternal() {
// Must notify autofill manager before performing the click actions to avoid scenarios where
// the app has a click listener that changes the state of views the autofill service might
// be interested on.
notifyAutofillManagerOnClick();
return performClick();
}
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
//关键代码,判断是否设置了onClickListener
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
...
return result;//最终返回执行结果
}
点击事件的分发机制的源码实现已经分析完了。
3.主要方法
1.dispatchTouchEvent:用来进行事件的分发,如果事件可以传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。
2.onInterceptTouchEvent:用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。
3.onTouchEvent:用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。
4.requestDisallowInterceptTouchEvent:一般用于子View中,要求父View不拦截事件。
5.dispatchTransformedTouchEvent:如果child不为空,就发到child的dispatchTouchEvent中,否则发给自己。
4.事件传递中listener
onTouch,performClick和onClick调用的顺序以及onTouch返回值的影响?
当一个View需要处理事件时,View的dispatchTouchEvent方法中,如果设置了OnTouchListener,那么OnTouchListener的onTouch方法会被调用,当onTouch方法返回true时,onTouchEvent就不会被调用,当onTouch方法返回false时,onTouchEvent方法就被调用,在onTouchEvent方法里面进入performClick方法,在performClick方法里面判断是否设置onClickListener,并且如果设置了onClickListener,那么onClick方法就被调用,performClick方法就返回true,如果没有设置了onClickListener,performClick方法就返回false。
总的来说方法调用的顺序为
5.滑动冲突如何用事件分发处理
滑动冲突定义:当有内外两层View都可以响应事件时,事件由谁来决定。
滑动冲突类型:1.当内外两层View滑动方向不一致
2.当内外两层滑动方向一致的时候
3.两种情况叠加
解决思路:
内部拦截:dispatchTouchEvent+dispatchTransformedTouchEvent
重写子元素的dispatchTouchEvent方法
down事件分发给子元素,move事件是看条件的,如果不满足条件,就把事件交给子元素处理,如果满足条件,就会取消子元素的处理事件,然后把事件交给父元
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
//down事件,父容器不要拦截我
parent.requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE: {
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (父容器需要此类点击事件) {
//父容器拦截我
parent.requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
(当move事件时,进入第一块代码,调用intercepted = onInterceptTouchEvent(ev),我们在onInterceptTouchEvent方法中设置不是down事件就返回true,所以intercepted为true,然后第二块代码不会执行,进入第三块代码,因为intercepted为true,所以cancelChild就为true,取消子元素事件执行,调用dispatchTransformedTouchEvent方法,cancel为true->
event.setAction(MotionEvent.ACTION_CANCEL)->handled = child.dispatchTouchEvent(event)
把mFirstTouchTarget设置为空,所以到下一个move事件来的时候,mFirstTouchTarget是为空的,在第一段代码中intercepted为true,第二段代码不执行,第三块代码走dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS),即由当前View的事件处理代码(父元素))
重写父元素的onInterceptTouchEvent方法
当为down事件时,要return false,因为在ViewGroup的dispatchTouchEvent方法中,当为down事件时,会调用resetTouchState()方法,在resetTouchState()方法里面会重置状态,把mGroupFlags也重置,这样会导致在前面的parent.requestDisallowInterceptTouchEvent(true)没有用,所以我们在onInterceptTouchEvent方法里面要设置为down事件时返回false,因为在down事件时onInterceptTouchEvent一定会执行。
public boolean onInterceptTouchEvent(MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent.ACTION_DOWN) {
super.onInterceptTouchEvent(event);
return false;
} else {
return true;
}
}
外部拦截:onInterceptTouchEvent
点击事件先经过父容器的拦截处理,如果父容器需要这件事就拦截,不需要就不拦截。
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
intercepted = false;
break;
}
case MotionEvent.ACTION_MOVE: {
if (满足父容器的拦截要求) {
intercepted = true;
} else {
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP: {
intercepted = false;
break;
}
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
来源:https://blog.csdn.net/weixin_63357306/article/details/128629042


猜你喜欢
- 前篇回顾:Spring源码解析容器初始化构造方法在上一篇文章中,我们介绍完了AnnotationConfigApplicationConte
- 前言我们平时在开发中,难免会遇到一些比较特殊的需求,就比如我们这篇文章的主题,一个关于圆弧滑动的,一般是比较少见的。其实在遇到这些东西时,不
- < application /> :应用的声明。 这个元素包含了子元素,这些子元素声明了应用的组件,元素的属性将会影响应用下的所
- 这篇文章主要介绍了Java模拟多线程实现抢票,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考
- 引言使用微信时我们会发现,首次进入微信的好友列表时,会加载好友头像,但是再次进入时,就不用重新加载了,而且其他页面都不用重新加载,说明微信的
- 格式要求:SU MO TU WE TH FR SA &nb
- 一次性全部绘制出来实现代码import java.awt.*;public class AlgoVisualizer {private st
- 最近微框架spring-boot很火,笔者也跟风学习了一下,废话不多说,现给出一个读取配置文件的例子。首先,需要在pom文件中依赖以下jar
- Java核心代码:public String getSmsInPhone() { final String SMS_URI_ALL = &q
- java金钱处理方法实例详解在支付行业中,涉及到对金钱的处理比较多。比如分转化成元、费率计算、手续费计算等等。1.分转化成元/** &nb
- 获取MCC/MNC以便控制小区广播的开启 双卡:((GeminiPhone)mPhone).getIccCardGemini(simId).
- 我们都知道Android应用软件基本上都会用到登录注册功能,那么对一个一个好的登录注册模块进行封装就势在必行了。这里给大家介绍一下我的第一个
- 一、前言本博文标题和内容参考:基于原生JS实现H5转盘游戏博主将改编成Unity版本。二、效果图三、案例制作1.界面搭建使用了9个图片作为奖
- 本文实例为大家分享了Android实现系统日历同步日程的具体代码,供大家参考,具体内容如下1、权限<uses-permission a
- 本文实例讲述了C#实现让ListBox适应最大Item宽度的方法。分享给大家供大家参考。具体实现方法如下:private void butt
- 前言镜像配置都是常规操作,必要时也可以上代理.自己搭的nexus本质也是一种镜像,可以代理maven中央仓库.各个仓库的测速,可以使用这个脚
- 在工作中要求将图片上传至本地,如下代码将介绍如何将图片上传至本地准备工作:环境:eclipse4.5-x64,jdk1.7-x64,mave
- 先看下这个问题的背景:假设有一个spring应用,开发人员希望自定义一个注解@Log,可以加到指定的方法上,实现自动记录日志(入参、出参、响
- 线程安全当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协调,这个类
- 前言在我们的日常企业应用开发当中,会碰到很多的图片素材访问的场景。比如社交类应用,您会在朋友圈中存放大量的图片,还有一些在线旅游或者直播的行