android5.0推出了RecyclerView,在开发中ListView的使用频率就很少了,这里讲ScrollView和ListView滑动冲突主要目的是ViewGroup的事件处理、事件分发及事件拦截。
ScrollView嵌套ListView,当然这里的ListView给的是固定高度,如果不是给的固定高度,给的是match_parent或者wrap_content的话ListView只会显示一个条目的高度;给ListView固定高度后,ScrollView和ListView不做任何处理,会发现ScrollView可以滑动,ListView不可以滑动。
原因:
ScrollView接到滑动事件后直接拦截掉了,并没有给到ListView。
解决:
让ScrollView不拦截掉ListView的滑动事件
解决的方式有很多种,这里就说下两种比较简单的方式;
方式一:
自定义ScrollView,重写dispatchTouchEvent方法:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// 不要拦截 true为不要拦截 false为拦截
requestDisallowInterceptTouchEvent(true);
return super.dispatchTouchEvent(ev);
}
方式二:
自定义ScrollView,重写onInterceptTouchEvent方法:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//返回false为不要拦截 返回true为拦截
return false;
}
这样子就解决掉ScrollView和ListView滑动冲突了
ViewGroup的事件处理、事件拦截、事件分发主要涉及到这几方法:
dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent、onTouch等方法,首先触发的是dispatchTouchEvent方法;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
//如果是一个新的ACTION_DOWN手势,就会将之前的ACTION_DOWN手势清除掉
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// Check for interception.开始检测事件拦截
final boolean intercepted;
//第一次走这里的时候mFirstTouchTarget是null
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
// 将改变后的动作进行保存
ev.setAction(action);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
...
// Check for cancelation.检测是否是放弃的
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
//如果事件被拦截掉了就不会走这里,也就是子view不会响应事件
if (!canceled && !intercepted) {
//canceled为false且事件没有被拦截
...
resetCancelNextUpFlag(child);
//dispatchTransformedTouchEvent 子孩子的事件处理
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
...
break;
}
...
}
...
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
//dispatchTransformedTouchEvent 子孩子的事件处理
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
//dispatchTransformedTouchEvent 子孩子的事件处理
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
...
//默认返回false
return handled;
}
所以dispatchTouchEvent方法默认返回的是false;在检测事件拦截的时候会调用到onInterceptTouchEvent方法;
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}
onInterceptTouchEvent默认返回的也是false,代表不拦截事件;
但是onInterceptTouchEvent调不调用取决于下面这句代码:
//是否拒绝检测事件拦截
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
如果disallowIntercept为true,代表拒绝检测事件拦截;disallowIntercept为false,代表需要检测事件拦截,所以不想父view拦截子view的事件的话,可以重写父view的dispatchTouchEvent方法并在return super.dispatchTouchEvent(ev);之前调用requestDisallowInterceptTouchEvent(true);将disallowIntercept值改true,这样就不会去检测事件拦截了。
还有就是将onInterceptTouchEvent返回值改为false,这样父view也不会去拦截子view的事件;
如果父view没有拦截子view的事件,就会去调用dispatchTransformedTouchEvent方法,
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
//如果没有子view或者子view为null就会去调用父类也就是view的dispatchTouchEvent方法
handled = super.dispatchTouchEvent(event);
} else {
//否则就调用子view的dispatchTouchEvent方法
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
...
return handled;
}
上面这个大致就是ViewGroup的事件处理,但是会发现ScrollView和RecyclerView并没有滑动的冲突,其实RecyclerView extends ViewGroup,并重写了onInterceptTouchEvent等方法已经做了处理,所以才不会冲突。
关联播客:
View的事件分发和处理
ViewGroup的事件拦截、事件分发、事件处理