这个问题的确我也遇到过,copy别人自定义View的demo,发现导入eclipse或者android stdio不能正常工作,然后根据网上一些解决经验,并不在意原来这都一种套路来的,都是遇到问题,一上来就百度,解决了就不了了之,下次遇到了,再百度,并不了解其原理。书中作者提供了通用的解决办法和解决的思路,受益匪浅!!
View的滑动冲突有3种场景:
(1)父容器和子View滑动方式不相同的冲突。如类似ViewPager水平滑动的View,子View的Layout有ListView,ViewPager是水平滑动个,而ListView是竖直滑动的,滑动的方式不相同。当然ViewPager已经解决冲突问题。
(2)父容器和子View滑动方式相同的冲突。如ScrollView包含ListView,两者都是监听竖直滑动,方向相同。
(3)嵌套以上2种的冲突。这种情况最复杂。
第(1)种场景的滑动冲突是最容易解决的,作者提供两种解决办法的思路:外部拦截法和内部拦截法。
外部拦截法就是在父容器onInterceptTouchEvent方法内判断是否符合自己的滑动方式,再决定拦不拦截。但是父容器拦截ACTION_DOWN事件时必须返回false,因为如果拦截ACTION_DOWN的话,接下来的事件都会交给父容器处理,子View则没有机会收到事件,更谈不上处理事件。在拦截ACTION_UP事件也必须返回false,因为父容器拦截ACTION_UP事件没有意义,一旦返回true(拦截),子View就不能响应ACTION_UP事件,进一步导致子View的Click事件不能响应。其实这种方法就是在ACTION_MOVE事件做判断,是否拦截ACTION_MOVE的事件,毕竟是滑动事件,最有用也就是ACTION_MOVE事件了。具体利用外部拦截法解决场景(1)代码如下。
// 自定义父容器
private int lastX;
private int lastY;
private int moveX;
private Scroller scroller;
// 初始化
private void inti() {
scroller = new Scroller(getContext());
}
// 重写父容器的onInterceptTouchEvent方法
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean isIntercept = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch(event.getAction) {
case MotionEvent.ACTION_DOWN :
isIntercept = false;
// 如果动画没有完成,继续响应事件,并停止当前的动画
if (!scroller.isFinished()) {
scroller.abortAnimation();
isIntercept = true;
}
break;
case MotionEvent.ACTION_MOVE :
// 例如父容器需要水平滑动
int deltaX = x - lastX;
int deltaY = y - lastY;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
isIntercept = true;
} else {
isIntercept = false;
}
break;
case MotionEvent.ACTION_UP :
isIntercept = false;
break;
}
lastX = x;
lastY = y;
return isIntercept;
}
// 重写父容器的onTouchEvent,如果onInterceptTouchEvent返回true,就会执行这个方法
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
switch(event.getAction) {
case MotionEvent.ACTION_DOWN :
// 我觉得作者写这几行重复的代码多余了,因为上面已经执行了,不知道对不对??
if (!scroller.isFinished()) {
scroller.abortAnimation();
}
moveX = 0;
break;
case MotionEvent.ACTION_MOVE :
int deltaX = x - lastX;
moveX += deltaX;
scrollBy(-deltaX, 0);
break;
case MotionEvent.ACTION_UP :
/**
* 如果moveX是正数,证明手指向右滑动,如果超过screenWidth的一半,scroller将要向右移动(screenWidth-x)的距离,显示前一个页面,否则scroller将要向左移动x的距离,让原来的页面回到原点。
**/
int slideX = -x;
if (moveX > screenWidth / 2) {
slideX = screenWidth - x;
}
scroller.startScroll(getScrollX(), 0, slideX, 0, 500);
invalidate();
break;
}
}
// 重写computeScroll方法配合Scroller实现滑动
public void computeScroll() {
if(scroller.computeScrollOffset()) {
scrollTo(scroll.getCurrentX(), scroll.getCurrentY());
postInvalidate();
}
}
内部拦截稍微复杂点,就是父容器不做拦截,直接传递给子View处理事件。如果符合子View的滑动方式,就消耗这个事件,否则交回给父容器处理。主要利用了子View设置父容器的一个标志位FLAG_DISALLOW_INTERCEPT,是否让父容器拦截事件。子View拦截ACTION_DOWN事件时,设置让父容器不能拦截事件。在ACTION_MOVE判断是否符合自己的滑动规则,如果不符合,允许父容器拦截事件。大概意思就是这样。具体思想如下代码。
// 重写子View的dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch(event.getAction) {
case MotionEvent.ACTION_DOWN :
parent.requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE :
// 例如父容器需要水平滑动
int deltaX = x - lastX;
int deltaY = y - lastY;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
parent.requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP :
break;
}
lastX = x;
lastY = y;
return super.dispatchTouchEvent(event);
}
// 还要重写父容器的onInterceptTouchEvent方法,除了ACTION_DOWN不要拦截,其他都拦截
public boolean onInterceptTouchEvent(MotionEvent event) {
if (event.getAction == MotionEvent.ACTION_DOWN) {
// 这样子View就可以拦截ACTION_DOWN事件
return false;
} else {
// true表示拦截,因为子View在拦截ACTION_DOWN已经设置父容器不能拦截事件,所以拦截了没有效果,只有当子View设置父容器可以拦截,这个true再次起作用
return true;
}
}
以上只是第(1)种场景,博客写代码,超级不方便的说!!
不过经过第(1)中场景,其实想法已经出来的了。解决其他两种也是这两种思想,只不过判断的依据不一样罢了。第一种根据的是手指滑动的距离,而其他2种就得看具体业务而定。写累了,其他两种有兴趣的看书。
代码有什么不对的地方,烦请指出,因为是纯手打的。