首先,要谈一下android父子控件之间事件的分发,对于事件的分发有几个原则需要了解。
(1) android事件分发是从父控件向子控件逐级分发传递的。(2) 每一层控件都可能消费这个事件,消费后不再向下传递(这也是父子控件对于手势操作冲突的主要原因)。
(3) android系统中的每个ViewGroup的子类都具有下面三个和事件分发处理密切相关的方法:
1.public boolean dispatchTouchEvent(MotionEvent ev) 这个方法用来分发TouchEvent
2.public boolean onInterceptTouchEvent(MotionEvent ev) 这个方法用来拦截TouchEvent
3.public boolean onTouchEvent(MotionEvent ev) 这个方法用来处理TouchEvent
(4) android的触摸是由一个ACTION_DOWN(按下),多个ACTION_MOVE(移动),一个ACTION_UP(抬起)组成。
父子控件间的事件传递,用一张图可以更好的体现:
1
(1)、distachTouchEvent用于分发事件,true直接消费事件并不在分发,false向intercertTouchEvent分发
(2)、internceptTouchEvent用于拦截事件,true直接分发事件至自己view的onTouchEvent方法经行处理,false继续分发
到子view的dispathcTouchEvent。
(3)、onTouchEevent作为事件处理的部分,响应由多个touchevent所组成的手势。
下面我们用这些内容来处理一个具体的问题,在竖直滑动的scrollview中嵌套水平滑动的viewpager的问题。viewpager中嵌套listview也可以用类似的方法解决。
由于scrollview处于布局的外层,来自系统的触摸事件会先传递至外层的scrollview,scrolliew对于事件的消将直接影响事件继续分发至内层的viewpager。
先来看一下scrollview的distachTouchEvent源码:
@Override
361 public boolean More ...onInterceptTouchEvent(MotionEvent ev) {
362 /*
363 * This method JUST determines whether we want to intercept the motion.
364 * If we return true, onMotionEvent will be called and we do the actual
365 * scrolling there.
366 */
367
368 /*
369 * Shortcut the most recurring case: the user is in the dragging
370 * state and he is moving his finger. We want to intercept this
371 * motion.
372 */
373 final int action = ev.getAction();
374 if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
375 return true;
376 }
377
378 if (!canScroll()) {
379 mIsBeingDragged = false;
380 return false;
381 }
382
383 final float y = ev.getY();
384
385 switch (action) {
386 case MotionEvent.ACTION_MOVE:
387 /*
388 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
389 * whether the user has moved far enough from his original down touch.
390 */
391
392 /*
393 * Locally do absolute value. mLastMotionY is set to the y value
394 * of the down event.
395 */
396 final int yDiff = (int) Math.abs(y - mLastMotionY);
397 if (yDiff > mTouchSlop) {
398 mIsBeingDragged = true;
399 }
400 break;
401
402 case MotionEvent.ACTION_DOWN:
403 /* Remember location of down touch */
404 mLastMotionY = y;
405
406 /*
407 * If being flinged and user touches the screen, initiate drag;
408 * otherwise don't. mScroller.isFinished should be false when
409 * being flinged.
410 */
411 mIsBeingDragged = !mScroller.isFinished();
412 break;
413
414 case MotionEvent.ACTION_CANCEL:
415 case MotionEvent.ACTION_UP:
416 /* Release the drag */
417 mIsBeingDragged = false;
418 break;
419 }
420
421 /*
422 * The only time we want to intercept motion events is if we are in the
423 * drag mode.
424 */
425 return mIsBeingDragged;
426 }
427
397-399可以看到,在scrollview中,当其捕捉到手指上下滑动的距离大于mTouchSlop时,onInterceptTouchEvent方法返回true,拦截事件至ontouchEvent中并消费,内层的viewpager将得不到触摸事件的传递。
虽然onscroll对事件是否拦截有一定的处理,但是有时仅仅这种效果不能满足需要。这种处理策略意味着,我们水平滑动viewpager时,必须保持上下滑动的距离小于mTouchSlop,否则,触摸事件会被上层的scrollview直接消费。为了解决这种冲突,个人想法是上层的scrollview不仅要限制竖直方向的TouchSlop,同时要限制水平方向的TouchSlop,避免和viewpager的冲突。这样需要我们重写scrollview的onInterceptTouchEvent()方法。
具体的实现方法如下:
public class MScrollView extends ScrollView {
private float FistXLocation;
private float FistYlocation;
private boolean Istrigger = false;
public Animation animationUp;
public Animation animationDown;
private final int TRIGER_LENTH = 50;
private final int HORIZOTAL_LENTH = 20;
private TMHDItemsGridsAvtivity faAvtivity;
public TMHDScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// TODO Auto-generated method stub
int deltaX = 0;
int deltaY = 0;
final float x = ev.getX();
final float y = ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
deltaX = (int)(FistXLocation - x);
deltaY = (int)(FistYlocation - y);
if (Math.abs(deltaY) > TRIGER_LENTH
&& Math.abs(deltaX) < HORIZOTAL_LENTH) {
Istrigger = true;
return super.onInterceptTouchEvent(ev);
//拦截这个手势剩下的部分 ,使他不会响应viewpager的相关手势
}
return false;//没有触发拦截条件,不拦截事件,继续分发至viewpager
case MotionEvent.ACTION_DOWN:
FistXLocation = x;
FistYlocation = y;
if(getScaleY()<-400){
System.out.println(getScaleY());
}
requestDisallowInterceptTouchEvent(false);
return super.onInterceptTouchEvent(ev);
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
if (Istrigger) {
Istrigger = false;
return super.onInterceptTouchEvent(ev);
}
break;
}
return super.onInterceptTouchEvent(ev);
}
}
下面这部分就是自己定义的水平和垂直的事件拦截触发条件。
private final int TRIGER_LENTH = 50;
private final int HORIZOTAL_LENTH = 20;
重写的MScrollView,自定义了onInterceptTouchEvent()方法,从而自定义了拦截的条件。解决了父子两个控件之间相互冲突的问题。