安卓开发过程中滑动冲突的情形主要有三类:
① 父view与子view的滑动方向不同,如:父view左右滑动,子view上下滑动或相反;
这种情形是比较简单的,只需要根据不同的滑动动作进行相应的拦截与处理即可。
② 父view与子view的滑动方向相同,即,父view左右,子view也左右,父view上下,子view也是上下;
这种情形需要根据具体情况来进行拦截处理,比如父View在出现子View滑动到边缘的情况才进行拦截处理或者其他情况。
③ 以上两种情形,多个View的嵌套。
这种情形虽比较复杂,但根据上面两种情形的处理法则分开进行处理即可。
滑动冲突的解决策略的理论基础为安卓的事件分发机制,下面先带大家补补这方面的知识。
Android 编程下 Touch 事件的分发和消费机制
Android 中与 Touch 事件相关的方法包括:dispatchTouchEvent(MotionEvent ev)、onInterceptTouchEvent(MotionEvent ev)、onTouchEvent(MotionEvent ev);能够响应这些方法的控件包括:ViewGroup 及其子类、Activity。方法与控件的对应关系如下表所示:
Touch 事件相关方法 | 方法功能 | ViewGroup | Activity |
public boolean dispatchTouchEvent(MotionEvent ev) | 事件分发 | Yes | Yes |
public boolean onInterceptTouchEvent(MotionEvent ev) | 事件拦截 | Yes | No |
public boolean onTouchEvent(MotionEvent ev) | 事件响应 | Yes | Yes |
从这张表中我们可以看到 ViewGroup 及其子类对与 Touch 事件相关的三个方法均能响应,而 Activity 对 onInterceptTouchEvent(MotionEvent ev) 也就是事件拦截不进行响应。另外需要注意的是 View 对 dispatchTouchEvent(MotionEvent ev) 和 onInterceptTouchEvent(MotionEvent ev) 的响应的前提是可以向该 View 中添加子 View,如果当前的 View 已经是一个最小的单元 View(比如 TextView),那么就无法向这个最小 View 中添加子 View,也就无法向子 View 进行事件的分发和拦截,所以它没有 dispatchTouchEvent(MotionEvent ev) 和 onInterceptTouchEvent(MotionEvent ev),只有 onTouchEvent(MotionEvent ev)。
一、Touch 事件分析
▐ 事件分发:public boolean dispatchTouchEvent(MotionEvent ev)
Touch 事件发生时 Activity 的 dispatchTouchEvent(MotionEvent ev) 方法会以隧道方式(从根元素依次往下传递直到最内层子元素或在中间某一元素中由于某一条件停止传递)将事件传递给最外层 View 的 dispatchTouchEvent(MotionEvent ev) 方法,并由该 View 的 dispatchTouchEvent(MotionEvent ev) 方法对事件进行分发。dispatchTouchEvent 的事件分发逻辑如下:
- 如果 return true,事件会分发给当前 View 并由 dispatchTouchEvent 方法进行消费,同时事件会停止向下传递;
- 如果 return false,事件分发分为两种情况:
- 如果当前 View 获取的事件直接来自 Activity,则会将事件返回给 Activity 的 onTouchEvent 进行消费;
- 如果当前 View 获取的事件来自外层父控件,则会将事件返回给父 View 的 onTouchEvent 进行消费。
- 如果返回系统默认的 super.dispatchTouchEvent(ev),事件会自动的分发给当前 View 的 onInterceptTouchEvent 方法。
▐ 事件拦截:public boolean onInterceptTouchEvent(MotionEvent ev)
在外层 View 的 dispatchTouchEvent(MotionEvent ev) 方法返回系统默认的 super.dispatchTouchEvent(ev) 情况下,事件会自动的分发给当前 View 的 onInterceptTouchEvent 方法。onInterceptTouchEvent 的事件拦截逻辑如下:
- 如果 onInterceptTouchEvent 返回 true,则表示将事件进行拦截,并将拦截到的事件交由当前 View 的 onTouchEvent 进行处理;
- 如果 onInterceptTouchEvent 返回 false,则表示将事件放行,当前 View 上的事件会被传递到子 View 上,再由子 View 的 dispatchTouchEvent 来开始这个事件的分发;
- 如果 onInterceptTouchEvent 返回 super.onInterceptTouchEvent(ev),事件默认会被拦截,并将拦截到的事件交由当前 View 的 onTouchEvent 进行处理。
▐ 事件响应:public boolean onTouchEvent(MotionEvent ev)
在 dispatchTouchEvent 返回 super.dispatchTouchEvent(ev) 并且 onInterceptTouchEvent 返回 true 或返回 super.onInterceptTouchEvent(ev) 的情况下 onTouchEvent 会被调用。onTouchEvent 的事件响应逻辑如下:
- 如果事件传递到当前 View 的 onTouchEvent 方法,而该方法返回了 false,那么这个事件会从当前 View 向上传递,并且都是由上层 View 的 onTouchEvent 来接收,如果传递到上面的 onTouchEvent 也返回 false,这个事件就会“消失”,而且接收不到下一次事件。
- 如果返回了 true 则会接收并消费该事件。
- 如果返回 super.onTouchEvent(ev) 默认处理事件的逻辑和返回 false 时相同。
针对滑动冲突的解决策略有以下两种:
一种是外部拦截法:即是当事件满足滑动条件,通过父View的onInterceptTouchEvent方法对其进行
拦截,拦截之后将直接进入父View的onTouchEvent进行事件消费,不会再传入下级view;
二种是内部拦截法:即是通过子View的dispatchTouchEvent方法接收到down事件,然后获取父View的requestDisallowInterceptTouchEvent方法禁止其onInterceptTouchEvent拦截,当满足父View滑动条件的
时候才允许。
第二种方法需要父View不拦截down事件,一般情况下也是不拦截down事件的,因为拦截了down事件,所有子元素点击事件都会失效。
在实践过程中,具体采取哪种方法,需要要据实际情况来处理。
针对第一种方法:
重写onInterceptTouchEvent进行条件判断@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mPrevX = MotionEvent.obtain(event).getX();
break;
case MotionEvent.ACTION_MOVE:
final float eventX = event.getX();
float xDiff = Math.abs(eventX - mPrevX);
if (xDiff > mTouchSlop) {
return false;
}
if(!isFirst){
return false;
}
}
return super.onInterceptTouchEvent(event);
}
为了精准判断滑动的方式,我们在自定义viewgroup里面重写onInterceptTouchEvent时,需要对滑动的xy轴距离判断大小,若竖直方向距离大则认为是竖直滑动,否则认为是水平滑动。
而第二种方法一般代码如下:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if("满足条件")
getParent().requestDisallowInterceptTouchEvent(false);
break;
}
return super.dispatchTouchEvent(ev);
}
public class NewRoleListRecyclerView extends RecyclerView {
private int mLastMotionX;
private int mLastMotionY;
ViewPager mViewPager = null;
FixRequestDisallowTouchEventPtrFrameLayout mPtrFrameLayout = null;
public NewRoleListRecyclerView(Context context) {
super(context);
}
public NewRoleListRecyclerView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public NewRoleListRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if(mViewPager == null) {
ViewParent viewParent = this;
while (viewParent != null) {
viewParent = viewParent.getParent();
if (viewParent instanceof ViewPager) {
mViewPager = (ViewPager) viewParent;
break;
}
}
}
if(mPtrFrameLayout == null) {
ViewParent viewParent = this;
while (viewParent != null) {
viewParent = viewParent.getParent();
if (viewParent instanceof FixRequestDisallowTouchEventPtrFrameLayout) {
mPtrFrameLayout = (FixRequestDisallowTouchEventPtrFrameLayout)viewParent;
break;
}
}
}
int x = (int) ev.getRawX();
int y = (int) ev.getRawY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
if(mViewPager != null) {
mViewPager.requestDisallowInterceptTouchEvent(true);
}
if(mPtrFrameLayout != null) {
mPtrFrameLayout.disallowInterceptTouchEvent(true);
}
// 发生down事件时,记录x,y坐标
mLastMotionX = x;
mLastMotionY = y;
break;
case MotionEvent.ACTION_MOVE:
int deltaY = y - mLastMotionY;
int deltaX = x - mLastMotionX;
if (Math.abs(deltaX) < Math.abs(deltaY)) {
if(mPtrFrameLayout != null) {
mPtrFrameLayout.disallowInterceptTouchEvent(false);
}
}
break;
case MotionEvent.ACTION_UP:
if(mViewPager != null) {
mViewPager.requestDisallowInterceptTouchEvent(false);
}
if(mPtrFrameLayout != null) {
mPtrFrameLayout.disallowInterceptTouchEvent(false);
}
break;
}
return super.dispatchTouchEvent(ev);
}
}
/**
* Created by xxx on 17/1/18.
*/
/**
* 解决PtrFrameLayout源码中在实现dispatchTouchEvent方法时,没有考虑到
* FLAG_DISALLOW_INTERCEPT的因素,导致requestDisallowInterceptTouchEvent方法不起作用
*/
public class FixRequestDisallowTouchEventPtrFrameLayout extends PtrFrameLayout {
private boolean mDisallowInterceptTouchEvent = false;
public FixRequestDisallowTouchEventPtrFrameLayout(Context context) {
super(context);
}
public FixRequestDisallowTouchEventPtrFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FixRequestDisallowTouchEventPtrFrameLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public void disallowInterceptTouchEvent(boolean disallowIntercept) {
mDisallowInterceptTouchEvent = disallowIntercept;
super.requestDisallowInterceptTouchEvent(disallowIntercept);
}
@Override
public boolean dispatchTouchEvent(MotionEvent e) {
if (mDisallowInterceptTouchEvent) {
return dispatchTouchEventSupper(e);
}
return super.dispatchTouchEvent(e);
}
}