前言
先上图。
请注意看开头部分
这里引用的是一张我之前写的一个组件的动图,很明显大家可以在我开头看到一个很流畅的下拉上拉的操作,没有任何阻碍,这就是我所说的完美解决嵌套滑动冲突方案。
接下来的话请注意:
阅读本文需要对Android的事件分发机制有一定的了解,如果不了解我建议先去了解一下Android的事件分发机制!
言归正传:
这里我打算通过一步步的模拟我们要遇到问题的情景,再一步步的解决这些问题,最终呈现出一个完美的解决方案。
1.解决嵌套滑动冲突第一步,拦截事件
情景1: 首先我们在自己写的容器中(可以滑动的容器都可以)添加一个 RecyclerView(只要是可以滑动的组件就可以,此处我以 RecyclerView为例),当遇到这种场景,我们就会发现无论我们怎么滑动 RecyclerView,父容器都不会滑动。
这时候了解过事件分发机制的朋友就很明显的可以发现,事件一直被RecyclerView消费了,父容器并没有消费到事件。
所以这里我已下拉为例,来提供第一个解决方案:
列表在顶部,且下拉时,容器将事件拦截:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
mInitialDownY = (int)ev.getY();
break;
case MotionEvent.ACTION_MOVE:
int nowY = (int)ev.getY();
int offsetY = mInitialDownY - nowY;
mInitialDownY = nowY;
if(!rvView.canScrollVertically(-1)) {
if (offsetY < 0) {//判断子view是否滑动到顶部并且当前是下拉
return true;//是的就拦截事件
}
}
break;
}
return false;
}
如果你这样写了并运行后,很容易的就发现会出现一个问题:
情景2: 列表自身已经在滑动,滑动到最顶部时,继续下拉,事件没有被容器拦截,需要重新松手,按下再下拉,容器才会拦截事件。
很明显是因为列表还在持有事件。
那这时候的思路也很明确,在列表滚动时,到达最顶部或者底部,将事件还给父容器。
2.让子 View “交还”事件
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
mDispatchDownY = (int)ev.getY();
break;
case MotionEvent.ACTION_MOVE:
int nowY = (int)ev.getY();
int offsetY = mDispatchDownY-nowY;
mDispatchDownY = nowY;
if((!rvView.canScrollVertically(-1)&&offsetY<0)||(!rvView.canScrollVertically(1)&&offsetY>0)){
//子 View 到达顶部或者底部,且滑动方向符合逻辑时,将事件还给父容器
//此处应该再添加相关逻辑避免父容器消费事件时频繁调用此方法
requestDisallowInterceptTouchEvent(false);
}
break;
}
return super.dispatchTouchEvent(ev);
}
上面代码中出现了这个方法
requestDisallowInterceptTouchEvent(false);
这个方法用途是告诉容器可以拦截事件,如果参数是 true 的话就是不要拦截事件。
顺便来看下源码好了:
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
//此处判断当前mGroupFlags是不是FLAG_DISALLOW_INTERCEPT,如果是则说明当前这个ViewGroup已经是FLAG_DISALLOW_INTERCEPT状态了,后面的代码没必要再执行了
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;//如果disallowIntercept为true,则将ViewGroup状态置为FLAG_DISALLOW_INTERCEPT
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;//反之就取消掉这个标记
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);//从这里看出来 这个方法是递归的
}
}
其实requestDisallowInterceptTouchEvent
方法的源码很简单,看注释就知道它是怎么实现的了,有意思的是这个方法是递归的,所以一旦调用了requestDisallowInterceptTouchEvent(true)
后,就会将当前ViewGroup以及所有包含了它的ViewGroup都置为FLAG_DISALLOW_INTERCEPT
这个标记,这个标记看名字就知道是立了一个不拦截事件的flag。
而我们通过调用requestDisallowInterceptTouchEvent(false)
来告诉容器可以拦截事件,达到一个事件"交还"给父容器的效果。
至于为什么调用这个方法就能解决这个问题,其实和下一步的知识点有很大的关联,在后面一起做一个分析。
3.父容器持有事件时将事件转交给子View
这一步就是完美解决嵌套滑动冲突的最后一步,让我们来模拟一下发生这种情况的场景:
情景3: 当我们父容器再滑动时,滑动到某一个位置,或者说这个时候 按照我们写好的判断是否该拦截这个事件的方法 来判断出我们应该将事件交给子 View,让子 View 可以滑动,我们就发现了,此时子 View 并不会滑动,还是父容器在滑动。
这里依旧可以很明显的看出,父容器并没有把事件发给子 View。
所以我们追寻一下之前写的代码,发现在父容器里目前只在onInterceptTouchEvent
方法中处理了相关逻辑,那秉着遇事不解看源码的原则,我们去看下onInterceptTouchEvent
是在什么时候被执行的,不难发现,onInterceptTouchEvent
是在ViewGroup的dispatchTouchEvent
方法中被执行的,这里我贴出相关的源码:
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();//4
}
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {//1
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;//2
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);//3
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;//5
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
先来看注释1, 这里是个判断,判断条件就是当前事件是ACTION_DOWN
且mFirstTouchTarget
不为null的情况,执行下一步,这里ACTION_DOWN
就是第一个手指接触屏幕的时候产生的事件,mFirstTouchTarget
则是可以接受事件的View。
再来看注释2, 这个地方是不是很熟悉,我们又看到了FLAG_DISALLOW_INTERCEPT
,没错,这就是我们调用requestDisallowInterceptTouchEvent
方法时相关的一个FLAG(其实这个状态只有调用requestDisallowInterceptTouchEvent(true)
时会被设置),所以这里的逻辑就是 当前状态不为FLAG_DISALLOW_INTERCEPT
时,disallowIntercept
为 false,反之为 true。
插入注释4: 此处会关闭FLAG_DISALLOW_INTERCEPT
状态,所以每次ACTION_DOWN
时都会重置这个状态。
最后看注释3, 这里会执行我们需要的onInterceptTouchEvent
方法,所以到现在我们得出结论,onInterceptTouchEvent
方法,只有当前状态为ACTION_DOWN
且mFirstTouchTarget
不为null的情况,当前ViewGroup状态不为FLAG_DISALLOW_INTERCEPT
时,才会被调用,而且在情景3发生时,我们已经拦截过事件(不然事件不会由父容器消费),说明当前没有可以接收事件的子View。
为什么调用requestDisallowInterceptTouchEvent(false)
可以“交还”事件,看到这里我们先把上面抛出的这个问题解决了,当子 View 在滑动时,像RecyclerView这些可滑动的组件,消费事件时内部一般都会调用getParent().requestDisallowInterceptTouchEvent(true)
方法,将其所有的父容器的状态都标记为FLAG_DISALLOW_INTERCEPT
,实现一个长期持有事件,只有触发ACTION_DOWN
或者调用getParent().requestDisallowInterceptTouchEvent(false)
时会重置这个状态,此时父容器mFirstTouchTarget
不为空,所以不需要ACTION_DOWN
也可以有机会执行onInterceptTouchEvent
方法,容器调用requestDisallowInterceptTouchEvent(false)
,关闭状态FLAG_DISALLOW_INTERCEPT
,此时注释2中disallowIntercept
为 false,此时可以执行onInterceptTouchEvent
方法,父容器经过判断,拦截事件。
解决了上面的问题我们再来得出一个结论: 父容器滑动时,不会执行 onInterceptTouchEvent
方法把事件分发给子View。
解决方案也很直接: 模拟一次ACTION_DOWN
事件,触发onInterceptTouchEvent
方法,分发事件给子 View。
先上代码
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
mDispatchDownY = (int)ev.getY();
break;
case MotionEvent.ACTION_MOVE:
mLastMoveMotionEvent = ev;//缓存最后的事件
//计算偏移量
int nowY = (int)ev.getY();
int offsetY = mDispatchDownY-nowY;
mDispatchDownY = nowY;
if(...){//判断条件,在合适的时候模拟`ACTION_DOWN`事件
sendDownEvent(mLastMoveMotionEvent);
}
break;
}
return super.dispatchTouchEvent(ev);
}
/**
* @param ev
* 模拟down事件
*/
private void sendDownEvent(MotionEvent ev){
MotionEvent e = MotionEvent.obtain(ev.getDownTime(),ev.getEventTime(), MotionEvent.ACTION_DOWN,ev.getX(),ev.getY(),ev.getMetaState());
super.dispatchTouchEvent(e);
}
调用MotionEvent.obtain
,模拟一个 down 事件,容器重新调用拦截方法,分发事件给子 View ,此时无阻碍的嵌套滑动实现。
总结
我说的完美解决嵌套滑动冲突就是上面这三步,但是实现这三步需要我们对Android事件分发机制要有一个清晰的了解,我们不应该局限于基本的事件分发教程中告诉你的这里返回 true 我们就拦截了事件,那里返回 false 我们就...
秉着遇事不解看源码的原则
而要更深的去了解Android事件分发,去解决遇到的相关问题,从上文的源码分析中,我们可以通过一小段代码分析出这么多的问题所在,所以解决问题还是要回归源码,从源码分析,找出问题,解决问题。
最后我放一张解决方案的流程图做一个总结:
- 首先需要在
onInterceptTouchEvent
方法中判断是否拦截 - 当子 View消费事件时,判断不需要事件的时候调用
requestDisallowInterceptTouchEvent(false)
让父容器可以重新拦截事件 - 当父容器消费事件时,判断不需要事件的时候模拟
ACTION_DOWN
事件重新执行onInterceptTouchEvent
方法,将事件发给子 View