我们在使用ViewPager来承载自定义控件时,可能会遇到这样一种情况:我们自定义的控件被触摸后,被滑动的居然是ViewPager,而不是我们的自定义控件被操作。
1 分析
(1)多次操作后,你会发现:
当你触摸自定义控件后,如果先竖向滑动后再正常操作,自定义控件交互也完全正常;
当你触摸自定义控件后,如果先横向滑动,那么你的自定义控件将不会再动作,随之替代的是ViewPager的跟手滑动。
(2)在自定义控件的onTouchEvent()里加上Log来打印MotionEvent的action,你会发现:
自定义控件在接受了ACTION_DOWN和数个ACTION_MOVE后,会接受到一个ACTION_CANCEL事件。随后你的自定义控件将不再受控,ViewPager再次拦截了事件,开始左右滑动起来。就算你在onTouchEvent()里死命地返回true去通知系统你要消化此事件,亦无济于事。
一个View正常处理的事件流程为:
ACTION_DOWN -> ACTION_MOVE -> ... -> ACTION_MOVE -> ACTION_UP
而现在的事件流程变成了这样:
ACTION_DOWN -> ACTION_MOVE -> ... -> ACTION_MOVE -> ACTION_CANCEL
ACTION_CANCEL 表示当前这个触摸手势由于某种原因被宣告无效,你在这个事件里获取不到任何有效的触摸点信息。你需要把它当作ACTION_UP来处理,但不要像正常流程一样去触发任何的控件动作,只要把它当作这次触摸手势已经取消并结束就好。
那么是谁来取消了这次触摸手势呢?触摸手势被取消后,ViewPager又动了起来,说明触摸手势还存在呀?~
别装了,傻子也能看出来是ViewPager劫取了这次触摸手势,并给它的子控件的发送了ACTION_CANCEL事件。
(3) 其他控件会受影响吗?
在ViewPager里添加一个Android自带的SeekBar。这个控件的交互也是左右滑动,刚好和ViewPager的交互方式一致,使用这个控件来测试可以很好的证明ViewPager是否会影响子控件的交互。
可我们会惊奇地发现,SeekBar一切正常,ViewPager也一切正常。那么SeekBar里一定有解决问题的方法!
2 解决方案
查阅了SeekBar的源码,最后发现在SeekBar的onTouchEvent()里有一个奇怪的方法调用:
attemptClaimDrag() // 尝试拒绝拖拽操作
这个方法尝试获取控件的父容器,并且通知父容器不要截断触摸事件的发送。如果你在ACTION_DOWN时返回了true去告诉父容器你需要处理本次事件流程后,就得在接下来的每次事件中告知父容器,本次事件流程你还需要处理,不要用手势检测器去探测并截断事件流程。
处理代码如下:
private void attemptClaimDrag() {
ViewParent parent = getParent();
if (parent != null) {
// 如果控件有父控件,那么请求父控件不要劫取事件
// 以便此控件正常处理所有触摸事件
// 而不是被父控件传入ACTION_CANCEL去截断事件
parent.requestDisallowInterceptTouchEvent(true);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean isNeedToEatEvent = false;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
{
// TODO 处理ACTION_DOWN事件
isNeedToEatEvent = true;
attemptClaimDrag();
break;
}
case MotionEvent.ACTION_MOVE:
{
// TODO 处理ACTION_MOVE事件
isNeedToEatEvent = true;
attemptClaimDrag();
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
{
// TODO 处理ACTION_UP事件
isNeedToEatEvent = true;
break;
}
}
return isNeedToEatEvent;
}
3 结论
如此,你的自定义控件将安静地躺在ViewPager里处理自己的事件,而不会再被ViewPager打断。