最近在看安卓开发艺术。看到滑动冲突一章,突然有感。mark一下。
先上一个demo效果图:
上面白色区域是一个listView,外面被一个scrollView包裹着。下面红色区域是一个Linearlayout占位。用来能上下滑动。
布局源码如下:
[java]
view plain
copy
print
?
- “1.0” encoding=“utf-8”?>
- “http://schemas.android.com/apk/res/android”
- android:layout_width=“match_parent”
- android:layout_height=“match_parent”>
- android:layout_width=“wrap_content”
- android:layout_height=“match_parent”
- android:orientation=“vertical”>
- android:id=“@+id/list_item”
- android:layout_width=“match_parent”
- android:layout_height=“200dp”>
- //此处用来占位,能使整个布局达到上下滑动的条件
- android:layout_width=“match_parent”
- android:layout_height=“800dp”
- android:background=“#ff0000”>
//此处用来占位,能使整个布局达到上下滑动的条件
上述布局样式:会发现,listView无法滑动,上下滑动只能滑动外部的scrollView。
问题一、为什么会滑动冲突?
首先先科普一个知识:
1)事件分发机制,是依照Activity——》ViewGroup——》View,从顶部往下分发。
2)而每个ViewGroup当disallowIntercept为false的时候,都会尝试拦截onInterceptTouchEvent()。(ps:后面我会具体谈disallowIntercept这个参数)
从简上盗一张图:点击打开链接 附上事件分发机制的链接。
知道这个知识后,思考一下当前demo,
猜想冲突原因:
分发过程中,被上层的ScrollView拦截了。没有分发到ListView。
我们直接去找ViewGroup拦截的方法onInterceptTouchEvent()。
对于当前demo的布局:activity(忽略)不考虑,直接看最外层ViewGroup类:ScrollView。
发现:ScrollView的onInterceptTouchEvent()继承自FrameLayout,而FrameLayout的onInterceptTouchEvent()原封不动的继承自ViewGroup。所以我直接看ViewGroup的onInterceptTouchEvent()方法:
ViewGroup#onInterceptTouchEvent()源码如下:
[java]
view plain
copy
print
?
- public boolean onInterceptTouchEvent(MotionEvent ev) {
- return false;
- }
public boolean onInterceptTouchEvent(MotionEvent ev) { return false; }
所以ViewGroup默认是不拦截的。而FrameLayout是没有重写这个方法的。再看ScrollView的onInterceptTouchEvent()方法:
ScrollView#onInterceptTouchEvent()源码如下:
[java]
view plain
copy
print
?
- @Override
- public boolean onInterceptTouchEvent(MotionEvent ev) {
- /*
- * This method JUST determines whether we want to intercept the motion.
- * If we return true, onMotionEvent will be called and we do the actual
- * scrolling there.
- */
- /*
- * Shortcut the most recurring case: the user is in the dragging
- * state and he is moving his finger. We want to intercept this
- * motion.//笔者注:当action为move操作时,且mIsBeingDragged为真的时候,返回true,拦截 。见注释1 */
- final int action = ev.getAction();
- if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
- return true;
- }
- /*
- * Don’t try to intercept touch if we can’t scroll anyway.//笔者注:这个ViewGroup如果不能滑动,则不允许打断。见注释2
- */
- if (getScrollY() == 0 && !canScrollVertically(1)) {
- return false;
- }
- switch (action & MotionEvent.ACTION_MASK) {
- case MotionEvent.ACTION_MOVE: {
- /*
- * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
- * whether the user has moved far enough from his original down touch.
- */
- /*
- * Locally do absolute value. mLastMotionY is set to the y value
- * of the down event.
- */
- final int activePointerId = mActivePointerId;
- if (activePointerId == INVALID_POINTER) {
- // If we don’t have a valid id, the touch down wasn’t on content.
- break;
- }
- final int pointerIndex = ev.findPointerIndex(activePointerId);
- if (pointerIndex == –1) {
- Log.e(TAG, “Invalid pointerId=” + activePointerId
- + ” in onInterceptTouchEvent”);
- break;
- }
- final int y = (int) ev.getY(pointerIndex);
- final int yDiff = Math.abs(y – mLastMotionY);
- if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
- mIsBeingDragged = true;//笔者注:见注释3
- mLastMotionY = y;
- initVelocityTrackerIfNotExists();
- mVelocityTracker.addMovement(ev);
- mNestedYOffset = 0;
- if (mScrollStrictSpan == null) {
- mScrollStrictSpan = StrictMode.enterCriticalSpan(“ScrollView-scroll”);
- }
- final ViewParent parent = getParent();
- if (parent != null) {
- parent.requestDisallowInterceptTouchEvent(true);
- }
- }
- break;
- }
- case MotionEvent.ACTION_DOWN: {
- final int y = (int) ev.getY();
- if (!inChild((int) ev.getX(), (int) y)) {
- mIsBeingDragged = false;
- recycleVelocityTracker();
- break;
- }
- /*
- * Remember location of down touch.
- * ACTION_DOWN always refers to pointer index 0.
- */
- mLastMotionY = y;
- mActivePointerId = ev.getPointerId(0);
- initOrResetVelocityTracker();
- mVelocityTracker.addMovement(ev);
- /*
- * If being flinged and user touches the screen, initiate drag;
- * otherwise don’t. mScroller.isFinished should be false when
- * being flinged.
- */
- mIsBeingDragged = !mScroller.isFinished();
- if (mIsBeingDragged && mScrollStrictSpan == null) {
- mScrollStrictSpan = StrictMode.enterCriticalSpan(“ScrollView-scroll”);
- }
- startNestedScroll(SCROLL_AXIS_VERTICAL);
- break;
- }
- case MotionEvent.ACTION_CANCEL:
- case MotionEvent.ACTION_UP:
- /* Release the drag */
- mIsBeingDragged = false;
- mActivePointerId = INVALID_POINTER;
- recycleVelocityTracker();
- if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) {
- postInvalidateOnAnimation();
- }
- stopNestedScroll();
- break;
- case MotionEvent.ACTION_POINTER_UP:
- onSecondaryPointerUp(ev);
- break;
- }
- /*
- * The only time we want to intercept motion events is if we are in the
- * drag mode.
- */
- return mIsBeingDragged;
- }
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { /* * This method JUST determines whether we want to intercept the motion. * If we return true, onMotionEvent will be called and we do the actual * scrolling there. */ /* * Shortcut the most recurring case: the user is in the dragging * state and he is moving his finger. We want to intercept this * motion.//笔者注:当action为move操作时,且mIsBeingDragged为真的时候,返回true,拦截 。见注释1 */ final int action = ev.getAction(); if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { return true; } /* * Don't try to intercept touch if we can't scroll anyway.//笔者注:这个ViewGroup如果不能滑动,则不允许打断。见注释2 */ if (getScrollY() == 0 && !canScrollVertically(1)) { return false; } switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_MOVE: { /* * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check * whether the user has moved far enough from his original down touch. */ /* * Locally do absolute value. mLastMotionY is set to the y value * of the down event. */ final int activePointerId = mActivePointerId; if (activePointerId == INVALID_POINTER) { // If we don't have a valid id, the touch down wasn't on content. break; } final int pointerIndex = ev.findPointerIndex(activePointerId); if (pointerIndex == -1) { Log.e(TAG, "Invalid pointerId=" + activePointerId + " in onInterceptTouchEvent"); break; } final int y = (int) ev.getY(pointerIndex); final int yDiff = Math.abs(y - mLastMotionY); if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) { mIsBeingDragged = true;//笔者注:见注释3 mLastMotionY = y; initVelocityTrackerIfNotExists(); mVelocityTracker.addMovement(ev); mNestedYOffset = 0; if (mScrollStrictSpan == null) { mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll"); } final ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } break; } case MotionEvent.ACTION_DOWN: { final int y = (int) ev.getY(); if (!inChild((int) ev.getX(), (int) y)) { mIsBeingDragged = false; recycleVelocityTracker(); break; } /* * Remember location of down touch. * ACTION_DOWN always refers to pointer index 0. */ mLastMotionY = y; mActivePointerId = ev.getPointerId(0); initOrResetVelocityTracker(); mVelocityTracker.addMovement(ev); /* * If being flinged and user touches the screen, initiate drag; * otherwise don't. mScroller.isFinished should be false when * being flinged. */ mIsBeingDragged = !mScroller.isFinished(); if (mIsBeingDragged && mScrollStrictSpan == null) { mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll"); } startNestedScroll(SCROLL_AXIS_VERTICAL); break; } case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: /* Release the drag */ mIsBeingDragged = false; mActivePointerId = INVALID_POINTER; recycleVelocityTracker(); if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) { postInvalidateOnAnimation(); } stopNestedScroll(); break; case MotionEvent.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; } /* * The only time we want to intercept motion events is if we are in the * drag mode. */ return mIsBeingDragged; }
注释1:mIsBeingDragged这个参数,在ScrollView中定义如下:
ScrollView#mIsBeingDragged源码如下:
[java]
view plain
copy
print
?
- /**
- * True if the user is currently dragging this ScrollView around. This is
- * not the same as ‘is being flinged’, which can be checked by
- * mScroller.isFinished() (flinging begins when the user lifts his finger).
- */
- private boolean mIsBeingDragged = false;
/** * True if the user is currently dragging this ScrollView around. This is * not the same as 'is being flinged', which can be checked by * mScroller.isFinished() (flinging begins when the user lifts his finger). */ private boolean mIsBeingDragged = false;
mIsBeingDragged默认为false,目前此处是未拦截的。
注释2:对上面的说法,先看一个效果图:
上图的红色占位区域位10dp,则scrollView无论如何都不会滑动,因此。因此并未体现滑动冲突效果onInterceptTouchEvent()返回的是false。
注释3:当滑动距离大于最小滑动距离时,onInterceptTouchEvent()返回true。我对此验证重写了ScrollView方法。
[java]
view plain
copy
print
?
- public class MyScrollView extends ScrollView{
- public MyScrollView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
- @Override
- public boolean onTouchEvent(MotionEvent ev) {
- Log.e(“MyScrollView”,super.onTouchEvent(ev)+“||onTouchEvent”);
- return super.onTouchEvent(ev);
- }
- @Override
- public boolean dispatchTouchEvent(MotionEvent ev) {
- Log.e(“MyScrollView”,super.dispatchTouchEvent(ev)+“||dispatchTouchEvent”);
- return super.dispatchTouchEvent(ev);
- }
- @Override
- public boolean onInterceptTouchEvent(MotionEvent ev) {
- Log.e(“MyScrollView”,super.onInterceptTouchEvent(ev)+“||onInterceptTouchEvent”);
- return super.onInterceptTouchEvent(ev);
- }
- }
public class MyScrollView extends ScrollView{ public MyScrollView(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onTouchEvent(MotionEvent ev) { Log.e("MyScrollView",super.onTouchEvent(ev)+"||onTouchEvent"); return super.onTouchEvent(ev); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { Log.e("MyScrollView",super.dispatchTouchEvent(ev)+"||dispatchTouchEvent"); return super.dispatchTouchEvent(ev); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { Log.e("MyScrollView",super.onInterceptTouchEvent(ev)+"||onInterceptTouchEvent"); return super.onInterceptTouchEvent(ev); } }
点击listView区域,对onInterceptTouchEvent打印Log如下:
说明:
前面多个false原因滑动距离不够。当滑动距离大于最小距离后,onInterceptTouchEvent()返回true。注:每一次滑动,都以滑动距离达到最小滑动判断是否滑动。一次滑动包含多个小的滑动。以action down 到 action up ,判断滑动距离。
总结:
滑动冲突产生原因:外部ScrollView拦截了事件,并消费了事件。
解决方案:
处理滑动冲突的方法包涵两种:内部拦截法和外部拦截法:
1)内部拦截法代码:
[java]
view plain
copy
print
?
- @Override
- public boolean dispatchTouchEvent(MotionEvent ev) {
- switch (ev.getAction()){
- case MotionEvent.ACTION_DOWN:
- getParent().requestDisallowInterceptTouchEvent(true);
- break;
- case MotionEvent.ACTION_MOVE:
- getParent().requestDisallowInterceptTouchEvent(true);
- break;
- case MotionEvent.ACTION_UP:
- getParent().requestDisallowInterceptTouchEvent(true);
- break;
- default:
- getParent().requestDisallowInterceptTouchEvent(true);
- break;
- }
- return super.dispatchTouchEvent(ev);
- }
@Override public boolean dispatchTouchEvent(MotionEvent ev) { switch (ev.getAction()){ case MotionEvent.ACTION_DOWN: getParent().requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_MOVE: getParent().requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_UP: getParent().requestDisallowInterceptTouchEvent(true); break; default: getParent().requestDisallowInterceptTouchEvent(true); break; } return super.dispatchTouchEvent(ev); }
通过以上方法,发现滑动冲突问题不存在了。实现了,滑动listView则滑动listView自身。
外部红色区域滑动,则滑动外部的ScrollView区域。
内部拦截法的原理
此处可能会有一个疑问:我们这么做的原理是什么?
既然只有一个方法requestDisallowInterceptTouchEvent():
ScrollView#requestDisallowInterceptTouchEvent源码如下:
[java]
view plain
copy
print
?
- @Override
- public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
- if (disallowIntercept) {
- recycleVelocityTracker();
- }
- super.requestDisallowInterceptTouchEvent(disallowIntercept);
- }
@Override public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { if (disallowIntercept) { recycleVelocityTracker(); } super.requestDisallowInterceptTouchEvent(disallowIntercept); }
disallowIntercept为true则,上面的代码暂时忽略,直接使用父类的的requestDisallowInterceptTouchEvent:
ViewGroup#requestDisallowInterceptTouchEvent源码如下:(代码二)
[java]
view plain
copy
print
?
- public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
- if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
- // We’re already in this state, assume our ancestors are too
- return;
- }
- if (disallowIntercept) {
- mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
- } else {
- mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
- }
- // Pass it up to our parent
- if (mParent != null) {
- mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
- }
- }
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) { // We're already in this state, assume our ancestors are too return; } if (disallowIntercept) { mGroupFlags |= FLAG_DISALLOW_INTERCEPT; } else { mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; } // Pass it up to our parent if (mParent != null) { mParent.requestDisallowInterceptTouchEvent(disallowIntercept); } }
Viewparent#requestDisallowInterceptTouchEvent
[java]
view plain
copy
print
?
- public void requestDisallowInterceptTouchEvent(boolean disallowIntercept);
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept);
首先放下位运算,先看看这个disallowIntercept这个标识。发现很眼熟。本文最开始也提到过。
对,就在这。ViewGroup#dispatchTouchEvent()源码:(代码一)
[java]
view plain
copy
print
?
- “code” class = “java” > @Override
- public boolean dispatchTouchEvent(MotionEvent ev) {
- //……..省略…..
- final boolean intercepted;
- 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; } }
- else {
- // There are no touch targets and this action is not an initial down
- // so this view group continues to intercept touches. intercepted = true; }
- //……..省略…..
- return handled;
- }
@Override public boolean dispatchTouchEvent(MotionEvent ev) { //........省略..... final boolean intercepted; 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; } } else { // There are no touch targets and this action is not an initial down // so this view group continues to intercept touches. intercepted = true; } //........省略..... return handled; }这个disallowIntercept标识也是一个位运算得到的。
disallowIntercept的位运算
好吧,这个位运算绕不开了。
1、先看mGroupFlags,mGroupFlags初始值为0。FLAG_DISALLOW_INTERCEPT初始值为
[java]
view plain
copy
?
- /**
- * When set, this ViewGroup should not intercept touch events.
- * {@hide}
- */
- protected static final int FLAG_DISALLOW_INTERCEPT = 0x80000
/** * When set, this ViewGroup should not intercept touch events. * {@hide} */ protected static final int FLAG_DISALLOW_INTERCEPT = 0x800002、mGroupFlags & FLAG_DISALLOW_INTERCEPT 计算知道值为0。
也就是:0000 0000 0000 0000 0000 & 1000 0000 0000 0000 0000 =0000 0000 0000 0000 0000;
代码一的:final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
相当于final boolean disallowIntercept = (0) != 0;
即disallowIntercept =false;
3、再看requestDisallowInterceptTouchEvent(true)时做了什么:
从代码二能看到true时对mGroupFlags重新赋值:mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
也就是mGroupFlags = mGroupFlags| FLAG_DISALLOW_INTERCEPT;
同理:mGroupFlags =0000 0000 0000 0000 0000 |1000 0000 0000 0000 0000 = 1000 0000 0000 0000 0000 ;
然后再看代码一:final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
同理:final boolean disallowIntercept = (1000 0000 0000 0000 0000 & 1000 0000 0000 0000 0000)!= 0;
因此:disallowIntercept = true;
值我们都知道了,再回到源码 代码一:
[java]
view plain
copy
?
- if (!disallowIntercept) {
- intercepted = onInterceptTouchEvent(ev);
- ev.setAction(action); // restore action in case it was changed
- } else {
- intercepted = false;
- }
if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // restore action in case it was changed } else { intercepted = false; }总结:
默认情况下:disallowIntercept = false,执行intercepted = onInterceptTouchEvent(ev); 也就是执行我们的onInterceptTouchEvent拦截方法。
当解决滑动冲突时,getParent().requestDisallowInterceptTouchEvent(true),disallowIntercept = false,则始终不执行onInterceptTouchEvent拦截方法。
2)外部拦截法代码:
(此段代码非针对该demo)
[java]
view plain
copy
?
- public boolean onInterceptTouchEvent(MotionEvent ev) {
- boolean intercepted = false;
- int x = (int) ev.getX();
- int y = (int) ev.getY();
- switch (ev.getAction()) {
- case MotionEvent.ACTION_DOWN:
- intercepted = false;
- break;
- case MotionEvent.ACTION_MOVE: {
- int deltaX = x - mLastXIntercept;
- int deltaY = y - mLastYIntercept;
- if (Math.abs(deltaX) > Math.abs(deltaY)) {
- intercepted = true;
- } else {
- intercepted = false;
- }
- break;
- }
- case MotionEvent.ACTION_UP: {
- intercepted = false;
- break;
- }
- }
- mLastXIntercept = x;
- mLastYIntercept = y;
- return intercepted;
- }
public boolean onInterceptTouchEvent(MotionEvent ev) { boolean intercepted = false; int x = (int) ev.getX(); int y = (int) ev.getY(); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: intercepted = false; break; case MotionEvent.ACTION_MOVE: { int deltaX = x - mLastXIntercept; int deltaY = y - mLastYIntercept; if (Math.abs(deltaX) > Math.abs(deltaY)) { intercepted = true; } else { intercepted = false; } break; } case MotionEvent.ACTION_UP: { intercepted = false; break; } } mLastXIntercept = x; mLastYIntercept = y; return intercepted; }总结:
从上述代码能看出外部拦截法,是重写了onInterceptTouchEvent()方法。通过滑动操作的不同,返回true(拦截) 或者 false(不拦截)。
针对不同滑动操作,来解决滑动冲突的情景,使用外部拦截法。
外部拦截法的原理:
同事件分发机制的原理,ViewGroup布局的dispatchtouchevent()方法,默认使用onInterceptTouchEvent()方法(默认值为false,不拦截)判断是否拦截,若返回值为true,则ViewGroup自身消费事件,走自身的onTouchevent事件(见上叙述的事件分发机制的图片)。当出现滑动冲突的时候可通过dispatchtouchevent()返回值,来判断是否需要拦截。