在 Android 开发当中,View 的事件分发机制是一块很重要的知识。不仅在开发当中经常需要用到,面试的时候也经常被问到。
如果你在面试的时候,能把这块讲清楚,对于校招生或者实习生来说,算是一块不错的加分项。对于工作几年的我们来说,这是必须掌握的,讲不明白,那你回去等通知吧,哈哈。
目录大概如下:
对于屏幕的点击,滑动,抬起等一系的动作,其实都是由一个一个MotionEvent对象组成的。根据不同动作,主要有以下三种事件类型:
1.ACTION_DOWN:手指刚接触屏幕,按下去的那一瞬间产生该事件 2.ACTION_MOVE:手指在屏幕上移动时候产生该事件 3.ACTION_UP:手指从屏幕上松开的瞬间产生该事件 4.ACTION_CANCEL 当前 View 的手势被打断,后续不会再收到任何事件
从 ACTION_DOWN 开始到 ACTION_UP/ACTION_CANCEL 结束我们称为一个事件序列
正常情况下,无论你手指在屏幕上有多么骚的操作,最终呈现在 MotionEvent 上来讲无外乎下面 3 种 case。
ACTION_CANCEL 一般是指 ChildView 原先拥有事件处理权,后面由于某些原因,该处理权需要交回给上层去处理,ChildView便会收到ACTION_CANCEL事件。对于一些复位或者重置操作,我们应该在 ACTION_UP 和 ACTION_CANCEL 里面同时进行处理。
代码逻辑上是:上层判断之前交给ChildView的事件处理权需要收回来了,便会做事件的拦截处理,拦截时给ChildView发一个ACTION_CANCEL事件
我们知道,View 的事件分发机制主要涉及到以下几个方法- dispatchTouchEvent ,这个方法主要是用来分发事件的
我们先来看一张图。
以下内容参考图解 Android 事件分发机制这一篇博客 在这里插入图片描述
仔细看的话,图分为3层,从上往下依次是Activity、ViewGroup、View
事件从左上角那个白色箭头开始,由 Activity 的 dispatchTouchEvent 进行分发
箭头的上面字代表方法返回值,(return true、return false、return super.xxxxx(),super 的意思是调用父类实现。)
dispatchTouchEvent和 onTouchEvent的框里有个【true---->消费】的字,表示的意思是如果方法返回true,那么代表事件就此消费,不会继续往别的地方传了,事件终止。
目前所有的图的事件是针对ACTION_DOWN的,对于ACTION_MOVE和ACTION_UP我们最后做分析。
当触摸事件发生时,首先 Activity 将 TouchEvent 传递给最顶层的 View,TouchEvent最先到达最顶层 view 的 dispatchTouchEvent ,然后由 dispatchTouchEvent 方法进行分发,
如果dispatchTouchEvent返回true 消费事件,事件终结。
如果dispatchTouchEvent返回 false ,则回传给父View的onTouchEvent事件处理;
如果dispatchTouchEvent返回super的话,默认会调用自己的onInterceptTouchEvent方法。
关于更多详细分析,请查看原博客图解 Android 事件分发机制,真心推荐,写得很好。
在开发当中,View 的滑动冲突时经常遇到的,比如 ViewPager 嵌套 ViewPager,ScrollView 嵌套 ViewPager。下面让我们一起来看看怎么解决。
第一种情况,滑动方向不同 第二种情况,滑动方向相同 第三种情况,上述两种情况的嵌套 在这里插入图片描述
看了上面三种情况,我们知道他们的共同特点是父View 和子View都想争着响应我们的触摸事件,但遗憾的是我们的触摸事件 同一时刻只能被某一个View或者ViewGroup拦截消费,所以就产生了滑动冲突。
那既然同一时刻只能由某一个 View 或者 ViewGroup 消费拦截,那我们就只需要 决定在某个时刻由这个 View 或者 ViewGroup 拦截事件,另外的 某个时刻由 另外一个 View 或者 ViewGroup 拦截事件,不就 OK了吗?
综上,正如 在 《Android开发艺术》 一书提出的,总共 有两种解决方案
以下解决思路来自于 《Android开发艺术》 书籍下面的两种方法针对第一种情况(滑动方向不同),父View是上下滑动,子View是左右滑动的情况。
从父View着手,重写onInterceptTouchEvent方法,在父View需要拦截的时候拦截,不要的时候返回false,为代码大概 如下
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
final float x = ev.getX();
final float y = ev.getY();
final int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
mDownPosX = x;
mDownPosY = y;
break;
case MotionEvent.ACTION_MOVE:
final float deltaX = Math.abs(x - mDownPosX);
final float deltaY = Math.abs(y - mDownPosY);
// 这里是够拦截的判断依据是左右滑动,读者可根据自己的逻辑进行是否拦截
if (deltaX > deltaY) {
return false;
}
}
return super.onInterceptTouchEvent(ev);
}
从子View着手,父View先不要拦截任何事件,所有的事件传递给 子View,如果子View需要此事件就消费掉,不需要此事件的话就交给 父View处理。
实现思路 如下,重写子 View的dispatchTouchEvent方法,在Action_down 动作中通过方法 requestDisallowInterceptTouchEvent(true) 先请求 父 View不要拦截事件,这样保证子 View 能够接受到 Action_move 事件,再在 Action_move 动作中根据自己的逻辑是否要拦截事件,不需要拦截事件的话再交给 父 View 处理。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int x = (int) ev.getRawX();
int y = (int) ev.getRawY();
int dealtX = 0;
int dealtY = 0;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
dealtX = 0;
dealtY = 0;
// 保证子View能够接收到Action_move事件
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
dealtX += Math.abs(x - lastX);
dealtY += Math.abs(y - lastY);
Log.i(TAG, "dealtX:=" + dealtX);
Log.i(TAG, "dealtY:=" + dealtY);
// 这里是够拦截的判断依据是左右滑动,读者可根据自己的逻辑进行是否拦截
if (dealtX >= dealtY) {
getParent().requestDisallowInterceptTouchEvent(true);
} else {
getParent().requestDisallowInterceptTouchEvent(false);
}
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_CANCEL:
break;
case MotionEvent.ACTION_UP:
break;
}
return super.dispatchTouchEvent(ev);
}
更多细节,可以查看我的这篇文章,里面有详细介绍哦 ViewPager,ScrollView 嵌套ViewPager滑动冲突解决
实现之前,我们首先来阐述一下思路,怎样实现双击事件,正所谓,授人以鱼不如授人以渔。
单击:用户点击一次之后,一段时间之内不再点击
双击;用户点击一次之后,一段时间之内再次点击
实现思路
open class MyDoubleTouchListener(private val myClickCallBack: MyClickCallBack) : OnTouchListener {
private var clickCount = 0 //记录连续点击次数
private val handler: Handler = Handler()
interface MyClickCallBack {
fun oneClick() //点击一次的回调
fun doubleClick() //连续点击两次的回调
}
override fun onTouch(v: View, event: MotionEvent): Boolean {
if (event.action == MotionEvent.ACTION_DOWN) {
clickCount++
handler.postDelayed({
if (clickCount == 1) {
myClickCallBack.oneClick()
} else if (clickCount == 2) {
myClickCallBack.doubleClick()
}
handler.removeCallbacksAndMessages(null)
//清空handler延时,并防内存泄漏
clickCount = 0 //计数清零
}, timeout.toLong()) //延时timeout后执行run方法中的代码
}
return false //让点击事件继续传播,方便再给View添加其他事件监听
}
companion object {
private const val TAG = "MyClickListener"
private val timeout = ViewConfiguration.getDoubleTapTimeout() //双击间四百毫秒延时
init {
Log.i(TAG, "timeout is $timeout ")
}
}
}
三级事件呢,其实也很简单,我们直接判断在指定时间间隔内点击的次数即可
open class MyMultiTouchListener(private val myClickCallBack: MyClickCallBack) : OnTouchListener {
private var clickCount = 0 //记录连续点击次数
private val handler: Handler = Handler()
interface MyClickCallBack {
fun oneClick() //点击一次的回调
fun doubleClick() //连续点击两次的回调
fun threeClick() // 连续点击三次的回调
}
override fun onTouch(v: View, event: MotionEvent): Boolean {
if (event.action == MotionEvent.ACTION_DOWN) {
clickCount++
handler.postDelayed({
if (clickCount == 1) {
myClickCallBack.oneClick()
} else if (clickCount == 2) {
myClickCallBack.doubleClick()
} else if (clickCount == 3) {
myClickCallBack.threeClick()
}
handler.removeCallbacksAndMessages(null)
//清空handler延时,并防内存泄漏
clickCount = 0 //计数清零
}, timeout.toLong()) //延时timeout后执行run方法中的代码
}
return false //让点击事件继续传播,方便再给View添加其他事件监听
}
companion object {
private const val TAG = "MyClickListener"
private val timeout = 600 //双击间四百毫秒延时
init {
Log.i(TAG, "timeout is $timeout ")
}
}
}
在 Android 开发当中,几乎所有的事件都会与用户进行交互,而我们用得的最多的就是手势了。
我们知道当我们触摸屏幕的时候,会产生很多事件,比如 down,move,up, fling 事件等等。一些简单的处理,我们可以直接重写 View 的 onTouchEvent 方法,根据 View 的 MotionEvent 事件进行处理。
而 Google 为了方便开发者方便接入,提供了几个默认处理类,那就是 GestureDetector 和 ScaleGestureDetector。
GestureDetector这个类对外提供了两个接口和一个外部类 。
接口:OnGestureListener,OnDoubleTapListener
内部类:SimpleOnGestureListener,同时实现了 OnGestureListener,OnDoubleTapListener 接口,如果只想使用接口里面的某个方法,可以直接使用它,方便快捷。
讲解之前,我们向来看一下怎么使用GestureDetector(Context context, GestureDetector.OnGestureListener listener)
第一步,初始化 GestureDetector
对象
mDetector = GestureDetectorCompat(this, MyGestureListener())
可以看到有两个参数,第一个参数 context,第二个参数 OnGestureListener,我们可以直接实现 OnGestureListener 接口,也可以直接使用 GestureDetector.SimpleOnGestureListener
private class MyGestureListener : GestureDetector.OnGestureListener {
private val TAG = "GestureDemoActivity"
override fun onShowPress(e: MotionEvent?) {
Log.d(TAG, "onShowPress: e is $e")
}
override fun onSingleTapUp(e: MotionEvent?): Boolean {
Log.d(TAG, "onSingleTapUp: e is $e")
return false
}
override fun onDown(event: MotionEvent): Boolean {
Log.d(TAG, "onDown: $event")
return true
}
override fun onFling(
event1: MotionEvent,
event2: MotionEvent,
velocityX: Float,
velocityY: Float
): Boolean {
Log.d(TAG, "onFling: $event1 $event2")
return false
}
override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, distanceX: Float, distanceY: Float): Boolean {
Log.d(TAG, "onScroll: distanceX is $distanceX,distanceY is $distanceY ")
return false
}
override fun onLongPress(e: MotionEvent?) {
Log.d(TAG, "onLongPress: e is $e")
}
}
第二步:设置双击监听
// 设置双击监听
mDetector.setOnDoubleTapListener(object : GestureDetector.OnDoubleTapListener {
override fun onDoubleTap(e: MotionEvent?): Boolean {
Log.d(TAG, "onDoubleTap: e is e")
return false
}
override fun onDoubleTapEvent(e: MotionEvent?): Boolean {
Log.d(TAG, "onDoubleTapEvent: e is e")
return false
}
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean {
Log.d(TAG, "onSingleTapConfirmed: e is e")
return false
}
})
最后,重写 Activity 或者 View 的 onTouchEvent ,将事件交给 mDetector 处理。
通常会有两种写法,第一种是如果手势处理器处理了,直接返回 true,进行消费。否则,进行默认处理
override fun onTouchEvent(event: MotionEvent): Boolean {
return if (mDetector.onTouchEvent(event)) {
true
} else {
super.onTouchEvent(event)
}
}
第二种写法是直接在 onTouchEvent 方法中,直接调用 mDetector.onTouchEvent(event)
方法
override fun onTouchEvent(event: MotionEvent): Boolean {
mDetector.onTouchEvent(event)
return super.onTouchEvent(event)
}
第二种写法,一般不会影响当前 View 或者 Activity 事件的传递,在开发当中,有时候为了减少一些触摸事件的冲突,经常这样写。
ScaleGestureDetector 这里暂时不展开描述了,写着写着,**发现好多呀,一个周末就这样过去,贼快。
这篇文章,其实不难。主要是将 View 的事件分发机制,滑动冲突,以及开发当中经常用到的一些知识点,总结一下。
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/fcDJgLOWekeEkpdEC_c4UA
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。