(此文章是以发表日期的两年前所写,但至今来看仍不过时,所以再在此发表)
这两天在做这个美女图片软件时,为了实现一个需求,遇到了由于事件分发传递机制引起的种种异常、难题和BUG,对事件分发传递有了进一步的理解,悟出一种重写事件分发的最佳实践(个人认为的最佳方法)。。
需求
如图,主界面是由三个ListView和一个标题栏组成的,三个ListView都可以自由上下滑动,现有一个需求:
当手指处于中间ListView的上半部分滑动时,旁边两个ListView也要向相同方向跟随中间的ListView滑动。
看似简单的需求,却可以引发种种BUG,这里就记录下这些问题产生的原因和解决的方法。。
首先,我们要明白Android事件传递的流程
dispatchTouchEvent(MotionEvent ev) → onInterceptTouchEvent(MotionEvent ev) → onTouchEvent(MotionEvent ev)
由父控件向子控件一层一层向下传递,经过 (事件分发 → 事件拦截 → 事件处理)流程 ,这里不对此作详细介绍
解决方案
这里简称左侧ListView为lv1,中间ListVIew为lv2,右边ListView为lv3
最容易想到的解决方案就是重写 dispatchTouchEvent()方法,手动为三个ListVIew分发事件
在Activirt中重写 dispatchTouchEvent()方法, 代码如下:
public
boolean
dispatchTouchEvent(
MotionEvent
ev) {
// 判断触摸点
坐
标
x值
如
果
小
于
lv1
的
宽
度
,
则
手
指
触
摸
的
是
lv1
,
这
个
事
件
应
分
发
给
lv1
进
行
处
理
if (
ev.
getRawX()
<
lv1.
getWidth()) {
lv1.
dispatchTouchEvent(
ev);
// 判断触摸点
坐
标
x
如
果
小
于
lv1
的
宽
度
* 2
,
则
手
指
触
摸
的
是
lv3
,
这
个
事
件
应
分
发
给
lv3
进
行
处
理
}
else
if (
ev.
getRawX()
>
lv1.
getWidth()
*
2) {
lv3.
dispatchTouchEvent(
ev);
}
else {
// 否则触摸的就是lv2,事件将被分发给lv2处理
lv2.
dispatchTouchEvent(
ev);
// 如果此时触摸点y坐标小于lv的高度的一半,则说明触摸点处于lv2的上半部分
// 事件需要再分发给lv1和lv3
if (
ev.
getRawY()
<
lv1.
getHeight() /
2) {
lv1.
dispatchTouchEvent(
ev);
lv3.
dispatchTouchEvent(
ev);
}
}
// 返回true,消耗此事件
return
true;
}
lv2和lv3无法接收点击事件
预期效果:点击listview中的美女条目,跳转该美女的图片集
lv1正常,达到预期;lv2和lv3可以正常滑动,但是无法响应点击。
问题产生原因分析
重写 dispatchTouchEvent()方法,给子ListView传递事件时,事件对象ev被原封不动的传递了下去。
比如用户点击lv2的一个条目,此时假设每个lv的宽度为100,点击点ev的x坐标为150,经过上面判断,150既不小于100,也不大于200,则进入else代码块,执行lv2.dispatchTouchEvent(ev);,ev被原封不动的传递给lv2
会造成什么样的后果呢?
lv2在判断点击点位于哪个条目的时候,ev的x坐标为150,而lv2的宽度一共只有100,这个x坐标超出了lv2的宽度,也就是说点到了lv2的外面,那么这个点击当然就无法正确触发条目点击事件了。lv3也是同理。
问题解决方案
当事件需要分发给lv2时,需要将点击点的x坐标减去lv的宽度,重新设置给ev,再传递下去。
当事件需要分发给lv3时,需要将点击点的 x坐标减去lv的宽度 * 2,重新设置给ev,再传递下去。
上方标题栏可以接收触摸事件
在上方标题栏中触摸上下拖动和点击,也会造成ListView滑动和触发lv1的点击事件
问题产生原因分析
要处理三个ListView的事件分发,应该重写它们的父布局LinearLayout的 dispatchTouchEvent()方法,而不是重写 Activity的事件分发方法,这样会影响到Activirt上的所有控件。
问题解决方案
创建自定义View继承 LinearLayout,重写 dispatchTouchEvent()方法
BUG解决
追求优雅的代码
创建自定义View继承 LinearLayout,重写 dispatchTouchEvent()方法
BUG解决
public
boolean dispatchTouchEvent(MotionEvent ev) {
// 获取触摸点的x坐标
int x
= (
int) ev.getRawX();
// 获取触摸点的y坐标
int y
= (
int) ev.getRawY();
// 获取lv的宽度,三个lv是等宽的,所以随便取谁的都一样
int width
= lv1.getWidth();
// 判断触摸点x坐标如果小于lv1的宽度,则手指触摸的是lv1,这个事件应分发给lv1进行处理
if (x
< width) {
lv1.dispatchTouchEvent(ev);
// 判断触摸点x坐标如果小于lv1的宽度 * 2,则手指触摸的是lv3,这个事件应分发给lv3进行处理
}
else
if (x
> width
* 2) {
// 此时事件应该被分发给lv3,需要重新设置x坐标,减去两个lv的宽度
ev.setLocation(x
- (width
* 2), y);
lv3.dispatchTouchEvent(ev);
}
else {
// 此时事件应该被分发给lv2,需要重新设置x坐标,减去lv的宽度
ev.setLocation(x
- width, y);
// 否则触摸的就是lv2,事件将被分发给lv2处理
lv2.dispatchTouchEvent(ev);
// 如果此时触摸点y坐标小于lv的高度的一半,则说明触摸点处于lv2的上半部分
// 事件需要再分发给lv1和lv3
if (y
< lv1.getHeight() / 2) {
lv1.dispatchTouchEvent(ev);
lv3.dispatchTouchEvent(ev);
}
}
// 返回true,消耗此事件
return true;
}
追求优雅的代码
上述代码中,去掉了系统的super.dispatchTouchEvent(ev)方法,造成很多东西需要自己处理,判断了三种情况,管理了三个ListView的事件分发
完全可以优化,其实实现需求只需要判断一种情况:当滑动点位于lv2的上半部分的时候,将事件同时也分发给另外两个ListVIew
优化后, LinearLayout的 dispatchTouchEvent()方法
public
boolean
dispatchTouchEvent(
MotionEvent
ev) {
// 第一个子条目,也就是ListView1
View
view
=
getChildAt(
0);
if (
MotionEvent.
ACTION_DOWN
==
ev.
getAction()) {
downY
= (
int)
ev.
getY();
}
int
x
= (
int)
ev.
getX();
int
width
=
view.
getWidth();
// 如果x处于中间的那个listview,并且y处于listview的上半部分
if (
x
>
width
&&
x
<
width
*
2
&&
downY
<
view.
getHeight() /
2) {
//将事件分发给另外两个ListView
view.
dispatchTouchEvent(
ev);
getChildAt(
2).
dispatchTouchEvent(
ev);
}
return
super.
dispatchTouchEvent(
ev);
}
不但没有各种BUG,代码长度也减少了这么多。。
经过这次事件,总结出如下几点经验:
1、没事千万不要删掉return super.dispatchTouchEvent(ev),系统为我们做了N多事情,包括事件该如何传递,传递下去该如何控制xy坐标,等等等等...
2、事件的传递,尽量不要重写Activity的,它的处理逻辑和ViewGroup的略有不同,应该重写目标控件的父控件。
3、如必须手动向下级子View传递事件,则需要计算并重新设置好x、y坐标,再传递
4、重写dispatchTouchEvent、onTouchEvent、方法时,尽量不要直接return true;或者 return false,除非系统对事件的处理与我们的预期有冲突。