欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

嵌套View的滑动,及拦截冲突问题.

程序员文章站 2024-02-14 23:03:34
...

分析嵌套view滑动时为什么会有冲突,怎样解冲突

这里的一个场景是:父View是一个可以左右滑动的界面(可以自定义ViewPage,模拟出冲突的情况,因为ViewPager已经处理了滑动冲突,所以如果不重写,模拟不出这里的场景),其子View是一个可以上下话的界面,比如是一个listView.

 

 

抛开ims侧的事件处理逻辑,直接说应用侧.

应用侧事件分发的起点从Activity开始.

public boolean dispatchTouchEvent(MotionEvent ev) #Activity.java{
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

这里的getWindow返回的是phonewindow. 如果phonewindow,view树没有处理事件,最终事件交给Activity的onTouchEvent(ev)消费.

    public boolean superDispatchTouchEvent(MotionEvent event) #PhoneWindow.java{
        return mDecor.superDispatchTouchEvent(event);
    }

这里的mDecor是DecorView,也即是view树的根,滑动冲突主要是在view树的嵌套处理中.接下来的事件处理就是ViewGroup,View的分发处理.

仅关注重点代码.

首先分析Down事件:

public boolean dispatchTouchEvent(MotionEvent ev) #ViewGroup.java{

	//第一次执行down事件,先考虑canceled,intercepted都为false,
//先说canceled通常是这个事件被它///的父view拦截时触发,然后intercepted通常是由ViewGroup中
//重写的onInterceptTouchEvent(ev)返回值决定.
	if (!canceled && !intercepted) {
//第一次down事件,newTouchTarget这个接受事件的view是null,同时view个数不为零.
		if (newTouchTarget == null && childrenCount != 0) {
//把一个ViewGroup中的所有子View按Z轴排序,父View的index小于子View的index,
//然后开始循环从子View也即是index最大的view开始,判断当前的事件是否在其坐标范围内.
			final ArrayList<View> preorderedList = buildTouchDispatchChildList();
			 if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                	ev.setTargetAccessibilityFocus(false);
                                	continue;
//这里newTouchTarget依然为null,因为 getTouchTarget方法中mFirstTouchTarget,导致其中的
//循环没有执行.
		 	newTouchTarget = getTouchTarget(child);

//分发事件给具体的view去处理,这个方法中会去调用child的  //child.dispatchTouchEvent(transformedEvent);的方法.
			if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
			//这个child的dispatchTouchEvent方法,返回值表示了事件是否被其消费,
//消费的情况有两种,一个是执行了其中的onTouch方法,一个是通过onTouchEvent消费了事件.
//如果事件被消费了,事件分发就结束了.同时会给newTouchTarget = mFirstTouchTarget= addTouchTarget(child, idBitsToAssign);赋值为当前消费了事件的view.
//如果这个child没有消费这个事件,通过循环就会把事件出给child的父View,去消费.
			}
                            }
		}

	}
}

正常的down事件,上面就处理完了.

 

下面看 如果down事件被拦截了,会怎么样?

这里的拦截是通过在具体的ViewGroup中重写boolean onInterceptTouchEvent(MotionEvent event)方法,让其返回true来实现.

比如在自定义的ViewPager中,重写这个 onInterceptTouchEvent方法,返回true.现象是整个界面可以左右滑动,但是listview不能上下滑动.

通过源码看下为什么?

public boolean dispatchTouchEvent(MotionEvent ev) #ViewGroup.java{
            final boolean intercepted;
//还是分析的down事件,
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
// mGroupFlags默认不包含 FLAG_DISALLOW_INTERCEPT,根据重写的 onInterceptTouchEvent,这里的 intercepted将会为true.
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } 

//如果intercepted为true,下面的条件里面的代码不会执行了,根据前面的了解,这里面的代码是去处理ViewGroup的子View的事件分发,所以这种情况,其子View就没有消费事件的机会了.
	 if (!canceled && !intercepted) {}

//接着是通过下面的调用,根据前面的分析, mFirstTouchTarget 这个情况下会为null, 所以由自定义viewpager的ontouchEvent来消费事件,那么他的子View ,即listview就没机会消费事件了.
	      // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } 
}

类似的道理,如果在自定义的ViewPager中,重写这个 onInterceptTouchEvent方法,返回false.现象是整个界面不可以左右滑动,但是listview可以上下滑动,因为这种情况下viewpager没有消费事件,所以传给了listview消费了.

 

前面都是分析的down事件,下面看move事件.为什么自定义的ViewPager中,重写这个 onInterceptTouchEvent方法,返回false.他就不能左右滑动了?

 

还是看 dispatchTouchEvent方法,只是现在的事件是Move.

public boolean dispatchTouchEvent(MotionEvent ev) #ViewGroup.java{
//down事件后, mFirstTouchTarget不为null了,所以这个条件依然成立,通过 onInterceptTouchEvent判读后, intercepted就是false了.
	            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            }

//这个条件依然是成立的.
	if (!canceled && !intercepted) {
//但是这里不会进入,这个跟down有区别的地方.
		             if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {}

//所以,直接走到这里,但是走的是else
 		   if (mFirstTouchTarget == null) {}
		  else {
//这里就一直会是listview去消息事件,
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
		} 
	
	}
}

最后,我们看下该怎么解决这种嵌套view的冲突问题.

 

先看一个方法,请求不要拦截touch事件.

public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) #ViewGroup.java{
	        if (disallowIntercept) {
//主要做的事情,就是给 mGroupFlags设置标记为.
             mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }
}

仍然以上面的场景,这里自定义的viewpager中, onInterceptTouchEvent返回true,

public boolean onInterceptTouchEvent(MotionEvent event) {
//这里之所以要对down区别处理,是因为viewgroup的 dispatchTouchEvent中有段代码,在down时会resetTouchState.
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            super.onInterceptTouchEvent(event);
            return false;
        }
	return true;
}

自定义的listView中,重写 dispatchTouchEvent方法,down的时候,请求parentView不要拦截,move的时候,当左右滑动时,恢复了这个flag.

public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();

switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN: {
        getParent().requestDisallowInterceptTouchEvent(true);
        break;
    }
    case MotionEvent.ACTION_MOVE: {
        int deltaX = x - mLastX;
        int deltaY = y - mLastY;
        if (Math.abs(deltaX) > Math.abs(deltaY)) {
            getParent().requestDisallowInterceptTouchEvent(false);
        }
        break;
    }
    }
}

在down的时候,我们再看ViewGroup中的处理,

public boolean dispatchTouchEvent(MotionEvent ev) #ViewGroup.java{
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
//这里因为 mGroupFlags先 | 然后在 &  FLAG_DISALLOW_INTERCEPT,结果不等于0,
//所以 disallowIntercept为true了,所以 intercepted就置为false. 
//这种情况,实际上自定义的viewpager的中 onInterceptTouchEvent方法就没用了.
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            }

}

重点看下move时,是怎么处理的.

自定义listview的move事件,如果是上下滑动,跟down事件是一致的,会有listview消费.

当左右滑动时,因为设置了getParent().requestDisallowInterceptTouchEvent(false),最终将有viewpager来消费左右滑动事件.

public boolean dispatchTouchEvent(MotionEvent ev) #ViewGroup.java{
//当前是move事件,但是 mFirstTouchTarget不为null,因为 getParent().requestDisallowInterceptTouchEvent(false),所以(mGroupFlags & FLAG_DISALLOW_INTERCEPT) 是0, 那么disallowIntercept就为false,结果就会调用 onInterceptTouchEvent,会把 intercepted置为true;
	            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } 

//intercepted为true,下面的条件代码不会执行
	if (!canceled && !intercepted) {}
//重点代码是else部分,
	 if (mFirstTouchTarget == null) {  }
	else {

         TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
// intercepted为true,会把 cancelChild置为true.
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
//因为 cancelChild为true,这个函数的执行中,会执行event.setAction(MotionEvent.ACTION_CANCEL);然后因为target.child不为null,所以实际的结果将是cancel了listview的执行.
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
//cancelChild为true,会把 mFirstTouchTarget = next;因为前面的处理next为null,所以 mFirstTouchTarget也被置为null了.
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
	} 
}

记住上面的条件变量,并且记住当前拥有事件的是listview,因为move事件是会多次执行的,那么在接着执行move事件时,同样走 dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent ev) #ViewGroup.java{
//结合前面一次move的变量值,这里 mFirstTouchTarget为null,
	if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {}
}

跟进入dispatchTransformedTouchEvent,因为child为null,将会执行viewpager的 dispatchTouchEvent方法,这样就把move事件从listview转到了viewpager.

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) #ViewGroup.java{
	        if (newPointerIdBits == oldPointerIdBits) {
            if (child == null || child.hasIdentityMatrix()) {
                if (child == null) {
                    handled = super.dispatchTouchEvent(event);
	}
}