目录
- 正文
- 剖析事件分发的过程
- ACTION_DOWN
- ACTION_MOVE
- ACTION_UP
- ACTION_CANCEL
- 完成案例代码
- ACTION_DOWN
- ACTION_MOVE
- ACTION_UP
- ACTION_CANCEL
- 截断ACTION_DOWN
- 结束
正文
经过事件分发之View事件处理和ViewGroup事件分发和处理源码分析这两篇的的理论知识分析,我们已经大致的了解了事件的分发处理机制,但是这并不代表你就一定能写好事件处理的代码。
既然我们有了基本功,那么本文就通过一个案例来逐步分析事件处理的代码如何写,事件冲突如何解决。
剖析事件分发的过程
为了模拟实际情况,我特意搞了一幅画View各种嵌套的图
图中有一个MyViewGroup,它可以左右滑动,本文就用它来讲解事件处理的代码如何写。
后面的分析需要大家有前面两篇文章的基础,请务必理解清楚,否则你可能会觉得我在讲天书。
ACTION_DOWN
由于我们操作的目标是MyViewGroup,因此我会把手指在MyViewGroup内容区域内按下,至于按在哪里,其实无所谓,甚至在TextView上也行。此时系统会把ACTION_DOWN事件经过Activity传递给ViewGroup0,那么问题来了ViewGroup0会不会截断事件呢?
如果ViewGroup0截断了ACTION_DOWN事件,那么它的所有子View在这个事件序列结束前,将无法接收到任何事件,包括ACTION_DOWN事件。MyViewGroup就是ViewGroup0的子View,很显然我们并不希望这样的事情发生。如果真的发生从一开始就截断ACTION_DOWN这样的事情,那父View控件的代码写的绝壁有问题。
事件序列是由ACTION_DOWN开始,由ACTION_UP或者ACTION_CANCEL结束,并且中间有0个或者多个ACTION_MOVE组成。
那么有没有截断ACTION_DOWN事件的情况呢?当然有,ViewGroup必须处于一个合理的状态,并且有理由截断ACTION_DOWN事件。例如ViewPager,当手指在屏幕快速划过后,页面还处于滑动状态,此时如果手指再次按下,ViewPager把这个ACTION_DOWN事件当做是停止滑动当前滑动并且重新开始滑动的指示,因此它有理由截断这个ACTION_DOWN事件。
那么,ViewGroup0在没有任何合理状态,并且还没有任何合理理由的情况下,是绝不会截断ACTION_DOWN事件的,因此它会把这个事件传递给MyViewGroup。
MyViewGroup很高兴接收到了第一个事件ACTION_DOWN,按照刚才讲的规则,常规状态下,是不截断ACTION_DOWN事件的,但是如果MyViewGroup在滑动状态中,并且手指已经离开屏幕,当再次按下手指的时候,我希望MyViewGroup截断ACTION_DOWN事件的,因此onInterceptTouchEvent()方法的事件处理的框架代码应该这样写
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
//. 如果处于无状态,默认不截断
//. 如果处于滑动状态,截断事件
break;
}
return super.onInterceptTouchEvent(ev);
}
现在讨论的是事件处理的框架代码如何写,因此没有具体的代码。
你肯定以为ACTION_DOWN事件就这样处理完了是吧,机智的我早已看穿一切
MyViewGroup是需要实现滑动特性的,那么它就必须要能接收到ACTION_MOVE事件。那么ACTION_DOWN事件要如何处理,才能确保这个事情呢?必须满足下面的一个条件
- MyViewGroup有一个子View处理了ACTION_DOWN事件。
- MyViewGroup自己处理ACTION_DOWN事件。
第一个条件呢,是最理想的情况,因为MyViewGroup在这种情况下,不用处理ACTION_DOWN事件就可以接收到ACTION_MOVE事件。
然而第一个条件,是不可控的,因此我们要做好最坏的打算,那就是MyViewGroup自己处理ACTION_DOWN。因此,在onTouchEvent()中处理ACTION_DOWN事件要返回true。
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 自己处理ACTIOND_DOWN,必须返回true
return true;
}
return super.onTouchEvent(event);
}
ACTION_MOVE
前面处理ACTION_DOWN已经确保了ACTION_MOVE可以顺利接收,根据前面列出的2个保证条件,那么接收ACTION_MOVE的情况如下
- MyViewGroup有一个子View处理了ACTION_DOWN,那么ACTION_MOVE将会在onInterceptTouchEvent()中被接收。
- MyViewGroup自己处理了ACTION_DOWN,那么ACTION_MOVE将会在onTouchEvent()中接收到。
对于第一种情况,其实有个限制条件,那就是子View必须允许MyViewGroup截断事件,否则MyViewGroup将收不到ACTION_MOVE事件。如果出现这种情况,那你得检查子控件的代码了是否写的合理了。
首先讨论第二种情况,如果ACTION_MOVE在onTouchEvent()中接收到,那就代表MyViewGroup要自己处理事件来滑动,因此返回true
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_MOVE:
// 自己处理ACTION_MOVE,返回true
return true;
}
return super.onTouchEvent(event);
}
现在来继续看第一种情况,ACTION_MOVE在发送给处理了ACTION_DOWN的子View前,需要通过MyViewGroup的onInterceptTouchEvent()方法,那么MyViewGroup要不要截断ACTION_MOVE事件呢?其实有很多种情况,我们来逐一分析可行性。
有人说,既然onInterceptTouchEvent()会一直接收ACTION_MOVE事件,那可以不截断就直接执行滑动。表面上看MyViewGroup实现了滑动,但是在实际中可能遇到问题。假如子View也是一个滑动的控件,那么在MyViewGroup滑动的时候,由于没有截断事件,因此子View同时也会根据自己的意愿去滑动,这岂不是瞎搞吗?又或者说子View在接收ACTION_MOVE事件后,请求父View不允许截断后续的事件,那么MyViewGroup后续就处理不了ACTION_MOVE事件了。
经过上面的分析,有人可能会说,一不做二不休,那就直接截断得了。我只能说,这位施主你太冲动!
如果直接粗暴的截断,万一遇上了不是完全垂直滑动的手势,MyViewGroup却在水平滑动,那岂不是尴尬了。
这时候,肯定有人忍不了了,截断也不是,不截断也不是,你想闹哪样!我们可以变通下嘛,我们要有条件的截断,避免刚才的尴尬情况嘛,举两个常用的条件
- 达到滑动的临界点
- 判断手势是水平滑动还是垂直滑动
那么,在onInterceptTouchEvent()方法中关于是否截断ACTION_MOVE的框架代码可以这样写
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_MOVE:
// 达到滑动标准就截断,否则不截断
// 滑动标准如下
//. 达到滑动的临界距离
//. 判断手势是水平滑动
break;
}
return super.onInterceptTouchEvent(ev);
}
ACTION_UP
我们先来讨论下,ACTION_UP会在哪里接收到
- MyViewGroup处理了ACTION_DOWN,ACTION_UP将会在onTouchEvent()中接收到。
- MyViewGroup在截断ACTION_MOVE之前,ACTION_UP将会在onInterceptTouchEvent()中接收到。
- MyViewGroup截断ACTION_MOVE后,ACTION_UP将会在onTouchEvent()中接收到。
第一种情况,返回true吧,因为毕竟是MyViewGroup自己处理了ACTION_UP事件。
第二种情况,返回false吧,因为此时MyViewGroup还没有处理滑动事件呢。
第三种情况,返回true吧,因为毕竟是MyViewGroup自己处理了ACTION_UP事件。
从源码角度看,对于ACTION_UP事件的处理的返回值,好像并不太重要。 但是返回true还是false其实是向父View表明一个种态度,那就是我到底是不是处理了ACTION_UP事件。
ACTION_CANCEL
从前面文章分析可知,ACTION_CANCEL是在MyViewGroup的父View截断了MyViewGroup的ACTION_MOVE事件后收到的,ACTION_CANCEL接收的地方其实和ACTION_UP是一样,至于是处理还是不处理,根据实际中有没有做实质的动作来相应的返回true或者false。
完成案例代码
前面我们已经对每个事件到底处不处理进行了分析,并且写出了事件处理的框架,那么接下来,我们就可以在这个框架之下,很放心地完成MyViewGroup滑动特性的代码了。
ACTION_DOWN
在处理ACTION_DOWN的时候要做啥呢?当然是记录手指按下时的坐标。由于ACTION_DOWN一定会经过onInterceptTouchEvent(),所以在这里记录按下坐标
public boolean onInterceptTouchEvent(MotionEvent ev) {
float x = ev.getX();
float y = ev.getY();
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
// 记录手指按下的坐标
mLastX = mStartX = x;
mLastY = mStartY = y;
//. 如果处于无状态,默认不截断
//. 如果处于滑动状态,截断事件
break;
}
return super.onInterceptTouchEvent(ev);
}
mStartX和mStartY表示手指按下的坐标,mLastX和mLastY表示最近一次事件的坐标。
ACTION_MOVE
根据前面的分析,处理ACTION_MOVE有情况有如下几种
如果MyViewGroup存在一个子View处理了ACTION_DOWN,
MyViewGroup截断ACTION_MOVE之前,ACTION_MOVE将会在onInterceptTouchEvent()中接收。
MyViewGroup截断ACTION_MOVE之后,ACTION_MOVE将会在onTouchEvent()中接收。
如果MyViewGroup处理了ACTION_DOWN,那么ACTION_MOVE将会在onTouchEvent()中接收。
第一种情况,根据前面的分析,我们将在onInterceptTouchEvent()根据条件来截断ACTION_MOVE事件。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
float x = ev.getX();
float y = ev.getY();
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_MOVE:
// 计算从手指按下时滑动的距离
float distanceX = Math.abs(x - mStartX);
float distanceY = Math.abs(y - mStartY);
if (distanceX > mScaledTouchSlop && distanceX > * distanceY) {
// 设置拖拽状态
setState(SCROLLING_STATE_DRAGGING);
// 不允许父View截断后续事件
requestDisallowIntercept(true);
// 执行一次拖拽的滑动
performDrag(x);
// 更新最新事件坐标
mLastX = x;
mLastY = y;
// 截断后续的事件
return true;
}
break;
}
return super.onInterceptTouchEvent(ev);
}
根据我们的分析,要达到截断ACTION_MOVE的标准才截断后续的ACTION_MOVE事件,从代码中可以看出这个标准有两条
- 水平滑动的距离要大于一个临界值。
- 水平滑动的距离要大于两倍的垂直滑动距离,这样就排除了一些不标准的手势。
当我们认为这是一次有效的滑动的时候,就要截断后续的ACTION_MOVE事件,这就是代码中看到的return true的原因。
然而事情还没有完,我们还做了一些优化动作
第一步,设置拖拽状态。这是因为在截断后续的ACTION_MOVE后,后续的ACTION_MOVE事件就会分发给MyViewGroup的onTouchEvent(),而onTouchEvent()也要处理其他情况的拖拽,因此需要这个状态判断值。
第二步,请求父View不允许截断后续ACTION_MOVE事件。因为MyViewGroup马上要执行以系列的滑动动作,如果父View此时截断了事件那肯定是不合适的,因此要通知父View不要搞事情。
第三步,执行一次滑动。可能很多人不理解为何要在onInterceptTouchEvent()中执行滑动动作,这个方法名义上只是用来判断是否截断事件的。
其实这里是有原因的,由于要截断后续的ACTION_MOVE事件,那么这次的ACTION_MOVE事件是不会发送到MyViewGroup的onTouchEvent()中的,而是把这个ACTION_MOVE事件变为ACTION_CANCEL事件发给处理了ACTION_DOWN事件的子View。因此当前的ACTION_MOVE如果不在onInterceptTouchEvent()处理,那么就会丢失这一次滑动处理。
截断后续的ACTION_MOVE后,MyViewGroup的onTouchEvent()会接收后续的ACTION_MOVE,那么在这里要继续执行滑动
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getActionMasked()) {
case MotionEvent.ACTION_MOVE:
if (mState == SCROLLING_STATE_DRAGGING) {
// 处于滑动状态就继续执行滑动
performDrag(x);
mLastX = x;
}
return true;
}
return super.onTouchEvent(event);
}
至此,处理ACTION_MOVE的第一种情况已经处理完毕,我们现在来看下第二种情况,那就是MyViewGroup处理了ACTION_DOWN,所有的ACTION_MOVE事件都将交给MyViewGroup的onTouchEvent()处理。那么此时MyViewGroup还没有滑动,因此需要再次判断是否达到滑动标准
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getActionMasked()) {
case MotionEvent.ACTION_MOVE:
if (mState == SCROLLING_STATE_DRAGGING) {
// 处于滑动状态就继续执行滑动
performDrag(x);
// 更新最新坐标点
mLastX = x;
} else {
// 不处于滑动状态,就再次检测是否达滑动标准
float distanceX = Math.abs(x - mLastX);
float distanceY = Math.abs(y - mLastY);
if (distanceX > mScaledTouchSlop && distanceX > * distanceY) {
setState(SCROLLING_STATE_DRAGGING);
requestDisallowIntercept(true);
performDrag(x);
mLastX = x;
}
}
return true;
}
return super.onTouchEvent(event);
}
ACTION_UP
对于ACTION_UP事件,我们先来预想下发生的情况
- 没有截断ACTION_MOVE事件之前,ACTION_UP事件会先由onInterceptTouchEvent()处理。
- 截断ACTION_MOVE事件之后,ACTION_UP事件会由onTouchEvent()处理。
- MyViewGroup处理了ACTION_DOWN事件,ACTION_UP事件全部会由onTouchEvent()处理。
第一种情况,由于MyViewGroup还没有产生滑动,因此不需要处理此种情况下手指抬起事件。
第二种情况,MyViewGroup已经产生滑动,如果MyViewGroup是一个像ViewPager一样的页面式的滑动,那么当手指抬起时,它需要进行一些页面定位操作,也就是决定滑动到哪个页面。
第三种情况,其实就是第一种情况和第二种情况的综合版而已。
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getActionMasked()) {
case MotionEvent.ACTION_UP:
if (mState == SCROLLING_STATE_DRAGGING) {
setState(SCROLLING_STATE_SETTING);
// 使用Scroller进行定位操作
int contentWidth = getWidth() - getHorizontalPadding();
int scrollX = getScrollX();
int targetIndex = (scrollX + contentWidth /) / contentWidth;
mScroller.startScroll(scrollX,, targetIndex * contentWidth - scrollX, 0);
invalidate();
}
return true;
}
return super.onTouchEvent(event);
}
ACTION_CANCEL
ACTION_CANCEL这个事件比较特殊,按照正常流程看,是由于父View截断了MyViewGroup的ACTION_MOVE事件后,把ACTION_MOVE变为了ACTION_CANCEL,然后发送给MyViewGroup。
如果MyViewGroup在进行滑动之前,会先请求父View不允许截断它的事件,也就是说之后父View不可能截断ACTION_MOVE事件,也就是不可能发送ACTION_CANCEL事件。
如果MyViewGroup还没开始滑动,那么MyViewGroup就可能会收到ACTION_CANCEL事件,然而此时不用做任何处理动作,因为MyViewGroup还没有滑动产生状态呢。
这是一种正常情况下的纯理论分析,不排除异常情况。
截断ACTION_DOWN
现在,我们回过头来处理MyViewGroup截断ACTION_DOWN的情况,前面我们说过,如果手指抬起,MyViewGroup还是处于滑动状态,在我们这个例子中叫做定位状态,那么当手指按下时,就需要截断事件,因为MyViewGroup认为这个时候的按下动作是为了停止当前滑动,并用手指控制滑动
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
float x = ev.getX();
float y = ev.getY();
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
// 重置状态
setState(SCROLLING_STATE_IDLE);
// 记录手指按下的坐标
mLastX = mStartX = x;
mLastY = mStartY = y;
//. 如果处于无状态,默认不截断
//. 如果处于滑动状态,截断事件
if (!mScroller.isFinished()) {
// 停止定位动作
mScroller.abortAnimation();
// 设置拖拽状态
setState(SCROLLING_STATE_DRAGGING);
// 不允许父View截断后续事件
requestDisallowIntercept(true);
return true;
}
break;
}
return super.onInterceptTouchEvent(ev);
}
当MyViewGroup截断ACTION_DOWN事件后,那么后续的的ACTION_MOVE事件就由onTouchEvent()来进行滑动处理,这个过程在前面已经实现。
结束
本文先从理论上搭建了事件处理的框架,然后用一个简单的例子实现了这个框架。如果大家在看本文的时候有任何疑问,请先参考前面两篇文章的分析,如果还是有疑问,欢迎在评论里留言讨论。
详细源码请参考github,实现的效果如下
为了测试,我在第一个页面放置了一个Button,然后点击Button开始滑动,可以看到Button并没有相应点击事件。然后在第二个页面返回第一个页面时,只有滑动超过了一半的宽度,才会自动滑动到第一页面。