自定义View系列教程06--详解View的Touch事件处理
Android多分辨率适配框架(1)— 核心基础
Android多分辨率适配框架(2)— 原理剖析
Android多分辨率适配框架(3)— 使用指南
自定义View系列教程00–推翻自己和过往,重学自定义View
自定义View系列教程01–常用工具介绍
自定义View系列教程02–onMeasure源码详尽分析
自定义View系列教程03–onLayout源码详尽分析
自定义View系列教程04–Draw源码分析及其实践
自定义View系列教程05–示例分析
自定义View系列教程06–详解View的Touch事件处理
自定义View系列教程07–详解ViewGroup分发Touch事件
自定义View系列教程08–滑动冲突的产生及其处理
PS:如果觉得文章太长,那就直接看视频吧
在之前的几篇文章中结合Andorid源码还有示例分析完了自定义View的三个阶段:measure,layout,draw。 在自定义View的过程中我们还经常需要处理View的Touch事件,这就涉及到了大伙常说的Touch事件的分发。其实,这一部分还是有些复杂的,而且有的地方不是很好理解,尤其是对于刚上路的新司机来说经常理不清楚,欲求不满,欲罢不能——想搞懂却又觉得难,想放弃又觉得舍不得。
好吧,我也经历过这些痛楚,感同身受。
所以,我们就从相对而言比较简单的View的Touch事件处理入手开始这部分知识的学习和总结。
滴滴,开车了,车门即将关闭。上车请刷卡,没卡的乘客请投币。
如果一个View(比如Button)接收到Touch,那么该Touch事件首先会传入到它的dispatchTouchEvent( )方法,所以我们从这里开始学习View对Touch事件的处理。
/**
* Pass the touch screen motion event down to the target view, or this
* view if it is the target.
*
* @param event The motion event to be dispatched.
* @return True if the event was handled by the view, false otherwise.
*/
public boolean dispatchTouchEvent(MotionEvent event) {
if (event.isTargetAccessibilityFocus()) {
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
event.setTargetAccessibilityFocus(false);
}
boolean result = false;
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
stopNestedScroll();
}
if (onFilterTouchEventForSecurity(event)) {
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);
}
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
嗯哼,这段源码不长,除了注释就剩下不到100行了。该方法的输入参数为event它表示Touch事件,这个很好理解;那么它的返回值有是什么含义呢?该boolean值表示的是Touch事件是否被消费。
在此,对该部分源码的核心部分和主要逻辑做一个梳理
第一步:
调用TouchListener中的onTouch()处理Touch事件,请参见代码第31-32行
该if判断中一共包含了4个条件,必须同时满足时才表示Touch事件被消费
- li != null
ListenerInfo是View中的一个静态类,包含了几个Listener,比如TouchListener,FocusChangeListener,LayoutChangeListeners,ScrollChangeListener等等。一般情况下它均不为null,所以我们不用过多关注它。 - li.mOnTouchListener != null
mOnTouchListener是由View设置的,比如mButton.setOnTouchListener()。所以如果View设置了Touch监听那么,那么mOnTouchListener不空;反之,mOnTouchListener为null - (mViewFlags & ENABLED_MASK) == ENABLED
当前View可用(ENABLED)。通常可调用view.setEnabled( )设置View是否可用 - li.mOnTouchListener.onTouch(this, event)
这一点其实是在li.mOnTouchListener != null的基础上继续判断。判断TouchListener的onTouch( )方法是否消耗了Touch事件。返回值为true表示消费掉该事件,false表示未消费。
在这四个条件中,我们通常最关心的就是最后一个:TouchListener的onTouch()方法。假如这四个条件中的任意一个不满足,那么result仍为false;则进入下一步
第二步:
调用View自身的onTouchEvent()处理Touch事件,请参见代码第36-38行
if (!result && onTouchEvent(event)) {
result = true;
}
嗯哼,看到了吧:如果在上一步中Touch事件被消费result为true,就不会执行这三行代码了。该处调用了onTouchEvent()若该方法返回值false那么dispatchTouchEvent()的返回值也为false;反之,若该方法返回值为true,那么dispatchTouchEvent()的返回值亦为true。
既然onTouchEvent()这么重要,我们就接着看该方法的源码
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
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:
mHasPerformedLongPress = false;
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);
}
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);
if (!pointInView(x, y, mTouchSlop)) {
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
removeLongPressCallback();
setPressed(false);
}
}
break;
}
return true;
}
return false;
}
这段代码稍微复杂一些,在此分析几个核心点。
当View为disable时对于Touch的处理,请参见代码第7-16行。
若一个View是disable的,如果它是CLICKABLE或者LONG_CLICKABLE或CONTEXT_CLICKABLE的就返回true,表示消耗掉了Touch事件。
但是请注意,该view所对应的ClickListener.onClick( )不会有任何的响应。即官方文档的描述:A disabled view that is clickable still consumes the touch events, it just doesn’t respond to them.
若View虽然是disable的,但只要满足这三个条件中的一个,它就会消费掉Touch事件但不再回调view的onClick( )方法
处理ACTION_DOWN,ACTION_MOVE,ACTION_UP事件等,请参见代码第24-116行。
在此请注意在对于ACTION_UP的处理时调用了performClick(),请参见代码第50行。public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
} sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}在该方法中调用了view的mOnClickListener.onClick( ),请参见代码第6行。
嗯哼,看到了吧:我们平常见得很多的Click事件是在View的onTouchEvent( )中处理ACTION_UP时调用的。返回onTouchEvent()方法的输出结果,请参见代码第118-121行。
在该处请尤其注意:
如果View是enable的,只要该View满足CLICKABLE和LONG_CLICKABLE以及CONTEXT_CLICKABLE这三者的任意一个(请参见代码第24-26行)不论当前的action是什么,该onTouchEvent()返回的均是true(请参见代码第118行);而且会在ACTION_UP时处理click事件。
同理,如果这三个条件都不满足,该onTouchEvent()返回的是false。
也请注意一个细节:
View的clickable属性视不同的子View有所差异
比如:Button的clickable默认为true,但是TextView的clickable属性默认为false。
View的longClickable属性默认为false。
当然,我们可以通过代码修改这些默认的属性。
比如:setClickable()和setLongClickListener()可以改变View的CLICKABLE和LONG_CLICKABLE属性。
除此以外,通过设置监听器也可改变某些属性。
比如:setOnClickListener()会将View的CLICKABLE设置为true;setOnLongClickListener()会将View的LONG_CLICKABLE设置为true。
第三步:
返回Touch事件是否被消费,请参见代码第52行
以上就为View对于Touch事件的主要步骤。
在此我画了一个简单的流程图,现结合该图和刚才的源码分析对View的Touch事件处理流程做一个总结。
- View处理Touch事件的总体流程
dispatchTouchEvent()—>onTouch()—>onTouchEvent()—>onClick()
Touch事件最先传入dispatchTouchEvent()中;如果该View存在TouchListener那么会调用该监听器中的onTouch()。在此之后如果Touch事件未被消费,则会执行到View的onTouchEvent()方法,在该方法中处理ACTION_UP事件时若该View存在ClickListener则会调用该监听器中的onClick() - onTouch()与onTouchEvent()以及click三者的区别和联系
2.1 onTouch()与onTouchEvent()都是处理触摸事件的API
2.2 onTouch()属于TouchListener接口中的方法,是View暴露给用户的接口便于处理触摸事件,而onTouchEvent()是Android系统自身对于Touch处理的实现
2.3 先调用onTouch()后调用onTouchEvent()。而且只有当onTouch()未消费Touch事件才有可能调用到onTouchEvent()。即onTouch()的优先级比onTouchEvent()的优先级更高。
2.4 在onTouchEvent()中处理ACTION_UP时会利用ClickListener执行Click事件。所以Touch的处理是优先于Click的
2.5 简单地说三者执行顺序为:onTouch()–>onTouchEvent()–>onClick() - View没有事件的拦截(onInterceptTouchEvent( )),ViewGroup才有,请勿混淆
关于View对Touch事件的处理就分析到此。
滴滴,到站了,下车的乘客们请往后门走。
PS:若觉得文章太长,那就直接看视频吧。
who is the next one? ——> 详解ViewGroup的Touch事件分发
自定义View系列教程06--详解View的Touch事件处理的更多相关文章
- [js高手之路] html5 canvas系列教程 - 状态详解(save与restore)
本文内容与路径([js高手之路] html5 canvas系列教程 - 开始路径beginPath与关闭路径closePath详解)是canvas中比较重要的概念.掌握理解他们是做出复杂canvas动 ...
- [js高手之路] es6系列教程 - Set详解与抽奖程序应用实战
我们还是从一些现有的需求和问题出发,为什么会有set,他的存在是为了解决什么问题? 我们看一个这样的例子,为一个对象添加键值对 var obj = Object.create( null ); obj ...
- [js高手之路] es6系列教程 - Map详解以及常用api
ECMAScript 6中的Map类型是一种存储着许多键值对的有序列表.键值对支持所有的数据类型. 键 0 和 ‘0’会被当做两个不同的键,不会发生强制类型转换. 如何使用Map? let map = ...
- 自定义View系列教程07--详解ViewGroup分发Touch事件
深入探讨Android异步精髓Handler 站在源码的肩膀上全解Scroller工作机制 Android多分辨率适配框架(1)- 核心基础 Android多分辨率适配框架(2)- 原理剖析 Andr ...
- 自定义View系列教程01--常用工具介绍
站在源码的肩膀上全解Scroller工作机制 Android多分辨率适配框架(1)- 核心基础 Android多分辨率适配框架(2)- 原理剖析 Android多分辨率适配框架(3)- 使用指南 自定 ...
- 自定义View系列教程08--滑动冲突的产生及其处理
深入探讨Android异步精髓Handler 站在源码的肩膀上全解Scroller工作机制 Android多分辨率适配框架(1)- 核心基础 Android多分辨率适配框架(2)- 原理剖析 Andr ...
- 自定义View系列教程05--示例分析
站在源码的肩膀上全解Scroller工作机制 Android多分辨率适配框架(1)- 核心基础 Android多分辨率适配框架(2)- 原理剖析 Android多分辨率适配框架(3)- 使用指南 自定 ...
- 自定义View系列教程04--Draw源码分析及其实践
深入探讨Android异步精髓Handler 站在源码的肩膀上全解Scroller工作机制 Android多分辨率适配框架(1)- 核心基础 Android多分辨率适配框架(2)- 原理剖析 Andr ...
- 自定义View系列教程03--onLayout源码详尽分析
深入探讨Android异步精髓Handler 站在源码的肩膀上全解Scroller工作机制 Android多分辨率适配框架(1)- 核心基础 Android多分辨率适配框架(2)- 原理剖析 Andr ...
随机推荐
- Yii 网站上线不需手动配置
参考: http://www.cnblogs.com/x3d/p/php_auto_prepend_file.html
- Web App开发注意事项
1.首先我们来看看webkit内核中的一些私有的meta标签,这些meta标签在开发webapp时起到非常重要的作用 <meta content=”width=device-width, ini ...
- IoT: 物联网安全测试经验总结
前言 今年早些时候,我参与了许多关于物联网解决方案的安全测试.主要目标是找出体系结构和解决方案中的漏洞.在这篇文章中,我将讨论一些与物联网解决方案的问题和挑战. 什么是物联网? 在你学习有关IPv6的 ...
- [jnhs]全套CRC校验 算法
摘自 https://blog.csdn.net/cp1300/article/details/51443350 uint8_t crc4_itu(uint8_t *data, uint_len le ...
- 【react】react-bookManager
作者可能是本意想要做一个图书管理系统,不过添加书籍的时候报错,所以简单的页面我们简单的看看 先上github地址:https://github.com/hesisi/react-bookManager ...
- JavaScript模式:字面量和构造函数
本篇主要讨论了通过字面量以构造对象的方法,比如对象.数组以及正则表达式等字面量的构造方法,同时还讨论了与类似Object()和Array()等内置构造函数相比,为什么基于字面量表示法是更为可取. 对象 ...
- js生成二维码以及插入图片
先根据qrcode官网demo,不同属性值的变化,二维码的变化效果:https://larsjung.de/jquery-qrcode/latest/demo/ 进入demo中,审查元素查看里面引用的 ...
- (转)Cookie存中文乱码的问题
有个奇怪的问题:登录页面中使用Cookie存值,Cookie中要存中文汉字.代码在本地调试,一切OK,汉字也能顺利存到Cookie和从Cookie中读出,但是放到服务器上不管用了,好好的汉字成了乱码, ...
- 通过工具SQLyog进行导入数据
可以通过工具SQLyog进行图形化导入数据. 1.准备好Excel表格 2.将excel表格数据导入到mysql数据库 (1)打开准备好的excel表,选择格式 另存为csv. (2)如果准备的exc ...
- Codeforces 375A
这是一道数学题,真是很考验数学思维,之前也遇到过相似的问题,但是依然是想不到点子上,就这提而言,最重要的就是 能否发现由 1, 6, 8,9这四个数字组成的排列对7取模是可以得到0, 1, 2, 3, ...