上一篇文章讲了View分发机制的源码,此次来说讲解决View滑动冲突的方式和原理。java
产生滑动冲突的场景主要有两种:android
那为何会产生滑动冲突呢,例如在父ViewGroup和子View的滑动方向一致的状况,我须要让二者均可以滑动。在上篇博客中咱们分析了事件分发机制,其中提到ViewGroup的onInterceptTouchEvent方法默认状况下是返回false,也就是ViewGroup默认状况下是不会拦截事件的。当ViewGroup接收到事件时,因为不拦截事件,会去寻找可以处理事件的子View。此时,一旦子View处理了DOWN事件,默认状况下接下来同一事件序列的其余事件都交由子View处理,此时能够看到的效果是子View能够滑动,可是父ViewGroup始终滑动不了,此时滑动冲突就出现了。ide
滑动冲突主要有两种解决方式:外部拦截法和内部拦截法源码分析
例如咱们使用ViewPager时,每每会结合Fragment,而后Fragment内部为一个ListView。这里ViewPager已经为咱们解决了滑动冲突,所以在使用时并不会冲突。试想下,若ViewPager未解决滑动冲突,默认状况下ViewPager的onInterceptTouchEvent方法返回false,因为ListView能够滚动,表明ListView能够处理事件,因此全部事件都被ListView处理了,所以咱们看到的效果会是ListView能够在竖直方向上滚动,可是ViewPager在水平方向上没法滑动。this
能够重写ViewPager,让ViewPager的onInterceptTouchEvent方法返回默认状态下的false,ViewPager内部是多个ListView。spa
public class MyViewPager extends ViewPager {
public MyViewPager(@NonNull Context context) {
super(context);
}
public MyViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
}
复制代码
运行效果如图3d
因此ViewPager是如何解决这样的滑动冲突的呢,由此引出外部拦截法。rest
所谓外部拦截法,就是当事件传递到父容器时,经过父容器去判断本身是否须要此事件,若须要则拦截事件,不须要则不拦截事件,将事件传递给子View。 上述MyViewPager和ListView显然产生了滑动冲突,咱们来分析下。咱们要的效果是在水平方向上滑动时ViewPager能够水平滚动,在竖直方向上滑动时,ListView能够滚动但ViewPager不动,所以咱们须要为ViewGroup指定事件处理的条件,因而就有了下面的伪代码。code
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
if (ViewPager须要此事件) {
return true;
}
break;
default:
break;
}
return false;
}
复制代码
如今咱们来分析下为何这段代码能够解决滑动冲突。 orm
这边首先要注意一点,外部拦截时在重写ViewGroup的onInterceptTouchEvent方法时,ViewGroup不能拦截DOWN事件和UP事件。由于一旦ViewGroup拦截了DOWN事件,也就是和mFirstTouchTarget始终为空,同一事件序列中的其余事件都不会再往下传递;若ViewGroup拦截了UP事件,则子View就不会触发单击事件,由于子View的单击事件是在UP事件时被触发的。
这边ViewPager处理事件的条件能够有多种方法,例如水平方向和竖直方向上的滑动速度、水平方向和竖直方向的滑动距离等。这边根据滑动距离判断,当水平方向的滑动距离大于竖直方向的滑动距离,则ViewPager处理事件,反之则将事件传递给ListView。
public class MyViewPager extends ViewPager {
private int mLastX;
private int mLastY;
public MyViewPager(@NonNull Context context) {
super(context);
}
public MyViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//一些ViewPager拖拽的标志位要设置,必调super,不然看不到效果
super.onInterceptTouchEvent(ev);
boolean isIntercepted = false;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
if (needEvent(ev)) {
isIntercepted = true;
}
break;
default:
}
mLastX = (int) ev.getX();
mLastY = (int) ev.getY();
LogUtils.d(" lastX = " + mLastX + " lastY = " + mLastY);
return isIntercepted;
}
private boolean needEvent(MotionEvent ev) {
//水平滚动距离大于垂直滚动距离则将事件交由ViewPager处理
return Math.abs(ev.getX() - mLastX) > Math.abs(ev.getY() - mLastY);
}
}
复制代码
运行效果:
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean isIntercept = false;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
//DOWN事件不能拦截,不然事件将没法分发到子View
isIntercept = false;
break;
case MotionEvent.ACTION_MOVE:
//根据条件判断是否拦截事件
isIntercept = needThisEvent();
break;
case MotionEvent.ACTION_UP:
//一旦父容器拦截了UP事件,子View将没法触发点击事件
isIntercept = false;
break;
default:
break;
}
return isIntercept;
}
复制代码
下面讲一种稍微复杂一点的同向滑动冲突。ScrollView内部的内部的LinearLayout存在三个子View,从上到下分别为ImageView、ListView以及TextView。
先上下效果图:
能够看到如今须要的效果是触摸ListView外部的区域,ScrollView的滑动不受限制。当触摸ListView区域时,存在多种状况。当ListView滚动到顶部时(ListView处于初始状态),此时若手指往下滑动,则ScrollView往下滑动;当ListView滚动到底部时,若此时手指往上滑动,则ScrollView往上滑动,其他状况下ListView滚动。
内部拦截法: ViewGroup默认状况下不拦截事件,由子View去控制事件的处理,若子View须要此事件,则本身处理,不然交由父容器处理。
使用内部拦截须要同时重写父ViewGroup的onInterceptTouchEvent和ViewGroup中须要解决冲突的子View的dispatchTouchEvent方法,和上面同样,先上伪代码。
子View伪代码
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
//禁止父容器拦截事件
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if (当期View不须要此事件) {
// 容许父容器拦截事件
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}
复制代码
ViewGroup 伪代码
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
return false;
default:
return true;
}
}
复制代码
这边咱们结合ScrollView和ListView这个具体实例和流程图进行分析。
首先父容器ScrollView不能拦截DOWN事件,必须将DOWN事件分发至子View,这边子View是 ListView,由于父容器一旦拦截DOWN事件,同一事件序列中的其余事件都不会传递到子View,这点在事件分发源码分析时已经分析了,这里再也不赘述。
因为内部拦截是将事件交由子View,由子View去控制事件的处理,因此事件在一开始不能被父ViewGroup直接拦截,因为DOWN事件被子View处理,此时mFristTonchTarget不为null,在默认状况下会去调用onInterceptedTouchEvent,若针对该事件该方法返回true,则事件就会被父容器拦截了,事件显然不会传递到子View,可是咱们须要将事件传递到子View,让子View去控制事件的处理。那咱们要怎么将事件传递到子View呢?从源码能够看到在调用onInterceptedTouchEvent方法前还有一个判断。
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//是否禁止拦截事件,默认为false
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;
}
} else {
intercepted = true;
}
复制代码
从源代码能够看到,会根据disallowIntercept的值判断是否要调用onInterceptTouchEvent这个方法,disallowIntercept默认为false。此时若能够将disallowIntercept的值变为true,就能够绕过onIntercepted方法,将事件传递到子View了,也就是咱们须要在MOVE事件到来以前给mGroupFlags设置FLAG_DISALLOW_INTERCEPT标志位,设置好后,若MOVE事件到来,disallowIntercept的值就会变为true,就会绕过onInterceptedTouchEvent方法的执行,将事件传递到子View了,那如何在MOVE事件到来以前给ViewGroup设置这个标志位呢?咱们能够在ViewGroup中看到这个方法。
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
复制代码
能够看到,若在调用requestDisallowInterceptTouchEvent方法时,参数为true,则mGroupFlags设置了FLAG_DISALLOW_INTERCEPT标志位,也就是disallowIntercept的值就会变为true。至于调用时机,咱们只须要在子View接收到DOWN事件时调用该方法便可,此后父ViewGroup会直接将事件传递给处理DOWN事件的子View。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
//禁止父容器拦截事件
getParent().requestDisallowInterceptTouchEvent(true);
break;
...
}
...
}
}
复制代码
若接下来的事件是子View感兴趣的,则直接处理掉,若是子View对事件不感兴趣,则将事件交还给父View,让它去处理。那么问题又来了,如何将子View不须要的事件从新交还给父View处理?此时可能有人会说,在事件分发中,子View处理不了的事件,不是自动会交给父ViewGroup处理吗?咱们说的子View处理不了的事件会传递给父ViewGroup处理,这个是针对默认的DOWN事件分发流程,可是在这不是DOWN事件且这里存在人工干预的状况,真的会是这样吗,咱们来看看源码。
先明确下当前的情景,子View处理了DOWN事件和部分MOVE事件,此时父ViewGroup的mFirstTouchEvent确定是不为null的。接下来的MOVE事件子View不须要,也就是子View不作处理,那么子View的dispatchTouchEvent方法会返回false。
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
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;
//子View 不处理事件, 子View的dispatchTouchEvent返回false,dispatchTransformedTouchEvent为false
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;
}
}
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
//直接返回false
return handled;
}
复制代码
从源代码能够看到,在这个情景下,ViewGroup的dispatchTouchEvent方法会直接返回false,不处理当前子View不感兴趣的MOVE事件,父ViewGroup的父容器也是这样直接返回false,直到传递给Activity,事件被Activity处理或者消失。而且当再一个MOVE事件来临时,MOVE仍是会传递到子View,可是子View对当前MOVE事件不感兴趣,也就是说以后的全部MOVE事件都不会被父ViewGroup处理,这样明显是存在问题的。因此子View在对事件不感兴趣时,要如何事件处理权交给父ViewGroup?咱们在子View 经过调用ViewGroup的requestDisallowInterceptTouchEvent方法,禁止父ViewGroup拦截事件,一样也能够在子View对事件不感兴趣时,调用ViewGroup的requestDisallowInterceptTouchEvent方法,容许父容器去拦截事件。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
if (当期View不须要此事件) {
// 容许父容器拦截事件
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}
复制代码
对子View来讲,对事件处理的控制逻辑已经完成了,可是对于父ViewGroup来讲并无,必需要重写ViewGroup的onInterceptedTouchEvent方法,让MOVE和UP事件返回true,表示拦截子View不感兴趣的事件,这边父ViewGroup拦截MOVE事件是能够理解的,可是为何要拦截UP事件呢,由于父ViewGroup只有拦截了UP事件才能够接收单击事件。
上述分析了原理,如今来真正解决一下ScrollView和ListView的滑动冲突。其实内部拦截的模板已经在伪代码中体现了。只要实现子View 对事件处理的判断便可。咱们须要监听ListView滚动到顶部和底部的状态,当ListView滚动到顶部时且手指触摸方向向下或者ListView滚动到底部且手机触摸方向向上,则将事件交由ScrollView处理。
public class MyListView extends ListView implements AbsListView.OnScrollListener {
private boolean isScrollToTop;
private boolean isScrollToBottom;
private int mLastX;
private int mLastY;
public MyListView(Context context) {
this(context, null);
}
public MyListView(Context context, AttributeSet attrs) {
this(context, attrs, -1);
}
public MyListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
setOnScrollListener(this);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
LogUtils.d("" + Constants.getActionName(ev.getAction()));
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
mLastX = (int) ev.getX();
mLastY = (int) ev.getY();
break;
case MotionEvent.ACTION_MOVE:
if (superDispatchMoveEvent(ev)) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
LogUtils.d("ACTION_UP");
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}
/** * 将事件交由父容器处理 * * @param ev * @return */
private boolean superDispatchMoveEvent(MotionEvent ev) {
//下滑
boolean canScrollBottom = isScrollToTop && (ev.getY() - mLastY) > 0;
boolean canScrollTop = isScrollToBottom && (ev.getY() - mLastY) < 0;
return canScrollBottom || canScrollTop;
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
isScrollToBottom = false;
isScrollToTop = false;
if (firstVisibleItem == 0) {
android.view.View firstVisibleItemView = getChildAt(0);
if (firstVisibleItemView != null && firstVisibleItemView.getTop() == 0) {
LogUtils.d("##### 滚动到顶部 ######");
isScrollToTop = true;
}
}
if ((firstVisibleItem + visibleItemCount) == totalItemCount) {
View lastVisibleItemView = getChildAt(getChildCount() - 1);
if (lastVisibleItemView != null && lastVisibleItemView.getBottom() == getHeight()) {
LogUtils.d("##### 滚动到底部 ######");
isScrollToBottom = true;
}
}
}
}
复制代码
至于ScrollView,默认在拖拽状态下会拦截MOVE事件,默认不拦截UP事件,若须要拦截UP事件,可重写ScrollView的onInterceptTouchEvent方法,但不是必须拦截UP事件,若父ViewGroup不须要触发单击事件,则能够不拦截。
public class MyScrollView extends ScrollView {
public MyScrollView(Context context) {
super(context);
}
public MyScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = super.onInterceptTouchEvent(ev);
if (ev.getAction() == MotionEvent.ACTION_UP) {
intercepted = true;
}
return intercepted;
}
}
复制代码
好了,到这里两种解决滑动冲突的方式就介绍完了,但要注意的是解决ViewPager与ListView滑动冲突并非只能用外部拦截,一样可使用内部拦截实现,第二个情景也是同样。解决方式并非绝对的,咱们要作的是选择最方便实现的方案。