[例证]从滑动冲突到事件分发(从源码角度分析)

来源:互联网 发布:ee是哪个国家域名 编辑:程序博客网 时间:2024/06/06 10:57

最近在看安卓开发艺术。看到滑动冲突一章,突然有感。mark一下。

先上一个demo效果图:

上面白色区域是一个listView,外面被一个scrollView包裹着。下面红色区域是一个Linearlayout占位。用来能上下滑动。

布局源码如下:

<?xml version="1.0" encoding="utf-8"?><ScrollView xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent">    <LinearLayout        android:layout_width="wrap_content"        android:layout_height="match_parent"        android:orientation="vertical">        <ListView            android:id="@+id/list_item"            android:layout_width="match_parent"            android:layout_height="200dp"></ListView>//此处用来占位,能使整个布局达到上下滑动的条件        <LinearLayout            android:layout_width="match_parent"            android:layout_height="800dp"            android:background="#ff0000">        </LinearLayout>    </LinearLayout></ScrollView>
上述布局样式:会发现,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()源码如下:

public boolean onInterceptTouchEvent(MotionEvent ev) {        return false;    }
所以ViewGroup默认是不拦截的。而FrameLayout是没有重写这个方法的。再看ScrollView的onInterceptTouchEvent()方法:

ScrollView#onInterceptTouchEvent()源码如下:

@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源码如下:

/**     * 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方法。

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)内部拦截法代码:

 @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源码如下:

@Override    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {        if (disallowIntercept) {            recycleVelocityTracker();        }        super.requestDisallowInterceptTouchEvent(disallowIntercept);    }
disallowIntercept为true则,上面的代码暂时忽略,直接使用父类的的requestDisallowInterceptTouchEvent:

ViewGroup#requestDisallowInterceptTouchEvent源码如下:(代码二)

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

public void requestDisallowInterceptTouchEvent(boolean disallowIntercept);

首先放下位运算,先看看这个disallowIntercept这个标识。发现很眼熟。本文最开始也提到过。

对,就在这。ViewGroup#dispatchTouchEvent()源码:(代码一)

<pre name="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;    }


这个disallowIntercept标识也是一个位运算得到的。

disallowIntercept的位运算

好吧,这个位运算绕不开了。

1、先看mGroupFlags,mGroupFlags初始值为0。FLAG_DISALLOW_INTERCEPT初始值为

/**     * When set, this ViewGroup should not intercept touch events.     * {@hide}     */    protected static final int FLAG_DISALLOW_INTERCEPT = 0x80000
2、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;

值我们都知道了,再回到源码 代码一:

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)

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()返回值,来判断是否需要拦截。

















0 0