android事件分发

android事件分发

示例代码地址https://github.com/kinglong123/androiddistribution

基础知识

事件主要有 down (MotionEvent.ACTION_DOWN),move(MotionEvent.ACTION_MOVE),up(MotionEvent.ACTION_UP)。
基本上的手势均由 down 事件为起点,up 事件为终点,中间可能会有一定数量的 move 事件。这三种事件是大部分手势动作的基础。

先来分析View的分发

结合下面的布局

<RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        android:paddingBottom="@dimen/activity_vertical_margin"
        tools:context="touch.touchdemo.MainActivity">
    <Button
            android:text="Hello World!"
            android:id="@+id/bt"
            android:textAllCaps="false"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

    <ImageView
            android:text="Hello World!"
            android:id="@+id/iv"
            android:layout_below="@+id/bt"
            android:background="@mipmap/ic_launcher"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

</RelativeLayout>

首先为button设置点击事件和OnTouch事件并返回false。

        btn = (Button) findViewById(R.id.bt);
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.v("TAG","onClick execute!");
            }
        });
        btn.setOnTouchListener(new View.OnTouchListener(){
            @Override
            public boolean onTouch(View view, MotionEvent event) {
                // TODO Auto-generated method stub
                Log.v("TAG","onTouch execute,"+"action is "+ ViewTool.actionToString(event.getAction()));
                return false;
            }
        });

这时点击button的打印信息

09-16 12:09:55.194 1720-1720/touch.touchdemo V/TAG: onTouch execute,action is ACTION_DOWN
09-16 12:09:55.302 1720-1720/touch.touchdemo V/TAG: onTouch execute,action is ACTION_UP
09-16 12:09:55.302 1720-1720/touch.touchdemo V/TAG: onClick execute!

再把button的OnTouch事件的返回值设为true。

        btn = (Button) findViewById(R.id.bt);
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.v("TAG","onClick execute!");
            }
        });
        btn.setOnTouchListener(new View.OnTouchListener(){
            @Override
            public boolean onTouch(View view, MotionEvent event) {
                // TODO Auto-generated method stub
                Log.v("TAG","onTouch execute,"+"action is "+ ViewTool.actionToString(event.getAction()));
                return true;
            }
        });

这时点击button的打印信息

09-16 12:09:55.194 1720-1720/touch.touchdemo V/TAG: onTouch execute,action is ACTION_DOWN
09-16 12:09:55.302 1720-1720/touch.touchdemo V/TAG: onTouch execute,action is ACTION_UP

发现点击事件没有执行

onClick()方法没有被执行,这里我们把这种现象叫做点击事件被onTouch()消费掉了,事件不会在继续向onClick()方法传递了

onTouch中返回了true时底层到底发生了什么?为什么在onTouch中返回了true,事件便不会继续向下传递了?onTouch和onTouchEvent的区别到底在哪里?为了解决我们心中的疑惑,我们必须去深入分析相关的源代码了。

补充知识点:Android中所有的事件都必须经过disPatchTouchEvent(MotionEvent ev)这个方法的分发。<br />
然后决定是自身消费当前事件还是继续往下分发给子控件处理。<br />
那么我们看看这个view里面的disPatchTouchEvent(MotionEvent ev)方法<br />

    public boolean dispatchTouchEvent(MotionEvent event) {
        // If the event should be handled by accessibility focus first.
        if (event.isTargetAccessibilityFocus()) {
            // We don't have focus or no virtual descendant has it, do not handle the event.
            if (!isAccessibilityFocusedViewOrHost()) {
                return false;
            }
            // We have focus and got the event, then use normal event dispatch.
            event.setTargetAccessibilityFocus(false);
        }

        boolean result = false;

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }

        if (onFilterTouchEventForSecurity(event)) {
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

        if (!result && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }

        // Clean up after nested scrolls if this is the end of a gesture;
        // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
        // of the gesture.
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }

        return result;
    }

代码有点多,我们一步步来看:

    public boolean dispatchTouchEvent(MotionEvent event) {
        // If the event should be handled by accessibility focus first.
        if (event.isTargetAccessibilityFocus()) {
            // We don't have focus or no virtual descendant has it, do not handle the event.
            if (!isAccessibilityFocusedViewOrHost()) {
                return false;
            }
            // We have focus and got the event, then use normal event dispatch.
            event.setTargetAccessibilityFocus(false);
        }

最前面这一段就是判断当前事件是否能获得焦点,如果不能获得焦点或者不存在一个View那我们就直接返回False跳出循环,接下来:

 if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }

设置一些标记和处理input与手势等传递,不用管,到这里:

  if (onFilterTouchEventForSecurity(event)) {
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

这里if (onFilterTouchEventForSecurity(event))是用来判断View是否被遮住等,ListenerInfo是View的静态内部类,专门用来定义一些XXXListener等方法的,到了重点:

 if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }

设置一些标记和处理input与手势等传递,不用管,到这里:

            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }

很长的一个判断,一个个来解释:第一个li肯定不为空,因为在这个If判断语句之前就new了一个li,第二个条件li.mOnTouchListener != null,怎么确定这个mOnTouchListener不为空呢?我们在View类里面发现了如下方法:

 /**
     * Register a callback to be invoked when a touch event is sent to this view.
     * @param l the touch listener to attach to this view
     */
    public void setOnTouchListener(OnTouchListener l) {
        getListenerInfo().mOnTouchListener = l;
    }

意味着只要给控件注册了onTouch事件这个mOnTouchListener就一定会被赋值,接下来(mViewFlags & ENABLED_MASK) == ENABLED是通过位与运算来判断这个View是否是ENABLED的,我们默认控件都是ENABLED的,所以这一条也成立;最后一条li.mOnTouchListener.onTouch(this, event)是判断onTouch()的返回值是否为True,我们后面把默认为False的返回值改成了True,所以这一整系列的判断都是True,那么这个disPatchTouchEvent(MotionEvent ev)方法直接就返回了True,那么接下来的代码都不会被执行。<br />
这就解释了上面为什么setOnTouchListener的毁掉onTouch返回true时,onClick不执行了。<br />

结合上面的代码可以得到结论:<br />
<br />
1 . OnTouchListener的优先级比onTouchEvent要高,联想到刚才的小Demo也可以得出OnTouchListener 中的onTouch方法优先于onClick()方法执行(onClick()是在onTouchEvent(event)方法中被执行的这个待会会说到)
<br />
2 . 如果控件(View)的onTouch返回False或者mOnTouchListener为null(控件没有设置setOnTouchListener方法)或者控件不是ENABLE的情况下会调用onTouchEvent方法,此时dispatchTouchEvent方法的返回值与onTouchEvent的返回值一样。

继续分析dispatchTouchEvent方法里面onTouchEvent的实现

 /**
     * Implement this method to handle touch screen motion events.
     * <p>
     * If this method is used to detect click actions, it is recommended that
     * the actions be performed by implementing and calling
     * {@link #performClick()}. This will ensure consistent system behavior,
     * including:
     * <ul>
     * <li>obeying click sound preferences
     * <li>dispatching OnClickListener calls
     * <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
     * accessibility features are enabled
     * </ul>
     *
     * @param event The motion event.
     * @return True if the event was handled, false otherwise.
     */
    public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();

        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return (((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
        }

        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            setPressed(true, x, y);
                       }

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }

                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }

                        if (prepressed) {
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }

                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_DOWN:
                    mHasPerformedLongPress = false;

                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

                    // Walk up the hierarchy to determine if we're inside a scrolling container.
                    boolean isInScrollingContainer = isInScrollingContainer();

                    // For views inside a scrolling container, delay the pressed feedback for
                    // a short period in case this is a scroll.
                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        setPressed(true, x, y);
                        checkForLongClick(0);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    setPressed(false);
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_MOVE:
                    drawableHotspotChanged(x, y);

                    // Be lenient about moving outside of buttons
                    if (!pointInView(x, y, mTouchSlop)) {
                        // Outside button
                        removeTapCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            // Remove any future long press/tap checks
                            removeLongPressCallback();

                            setPressed(false);
                        }
                    }
                    break;
            }

            return true;
        }

        return false;
     }

代码还是很多,我们依然一段一段来分析,最前面的一段代码:

  if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return (((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
        }

根据前面的分析我们知道这一段代码是对当前View处于不可用状态的情况下的分析,通过注释我们知道即使是一个不可用状态下的View依然会消耗点击事件,只是不会对这个点击事件作出响应罢了,另外通过观察这个return返回值,只要这个View的CLICKABLE和LONG_CLICKABLE或者CONTEXT_CLICKABLE有一个为True,那么返回值就是True,onTouchEvent方法会消耗当前事件。<br />

看下一段代码:

if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

这段代码的意思是如果View设置有代理,那么还会执行TouchDelegate的onTouchEvent(event)方法,这个onTouchEvent(event)的工作机制看起来和OnTouchListener类似,这里不深入研究.<br />

下面看一下onTouchEvent中对点击事件的具体处理流程:

 if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            setPressed(true, x, y);
                       }

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }

                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }

                        if (prepressed) {
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }

                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_DOWN:
                    mHasPerformedLongPress = false;

                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

                    // Walk up the hierarchy to determine if we're inside a scrolling container.
                    boolean isInScrollingContainer = isInScrollingContainer();

                    // For views inside a scrolling container, delay the pressed feedback for
                    // a short period in case this is a scroll.
                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        setPressed(true, x, y);
                        checkForLongClick(0);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    setPressed(false);
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_MOVE:
                    drawableHotspotChanged(x, y);

                    // Be lenient about moving outside of buttons
                    if (!pointInView(x, y, mTouchSlop)) {
                        // Outside button
                        removeTapCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            // Remove any future long press/tap checks
                            removeLongPressCallback();

                            setPressed(false);
                        }
                    }
                    break;
            }

            return true;
        }

        return false;
    }

我们还是一行行来分解:

  if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                     case MotionEvent.ACTION_UP:
                      ....
                      performClick();
                      ....
            //省略
            }
             return true;
    }
   return false;

这边主要关注两点

  1. 可点击的view返回true,否则返回false
  2. 在 MotionEvent.ACTION_UP:中会进行点击事件判断

performClick()源码:

    public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        return result;
    }

那就是当ACTION_UP事件发生时,会触发performClick()方法,如果这个View设置了OnClickListener那么最终会执行到OnClickListener的回调方法onClick(),这也就验证了刚才所说的:onClick()方法是在onTouchEvent内部被调用的。

继续:我们为demo中的imageView设置touch事件

        imageView  = (ImageView) findViewById(R.id.iv);
        imageView.setOnTouchListener(new View.OnTouchListener(){
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                // TODO Auto-generated method stub
                Log.v("TAG","onTouch execute,"+"action is "+ViewTool.actionToString(event.getAction()));
                return false;
            }
        });

这时点击imageView打印信息为:

09-16 12:09:55.194 1720-1720/touch.touchdemo V/TAG: onTouch execute,action is ACTION_DOWN

再为imageView增加点击事件

 imageView  = (ImageView) findViewById(R.id.iv);
        imageView.setOnTouchListener(new View.OnTouchListener(){
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                // TODO Auto-generated method stub
                Log.v("TAG","onTouch execute,"+"action is "+ViewTool.actionToString(event.getAction()));
                return false;
            }
        });
        imageView.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View arg0) {
                // TODO Auto-generated method stub
                Log.v("TAG","onClick execute!");
            }
        });

这时的单元信息为

09-16 13:03:14.682 1720-1720/touch.touchdemo V/TAG: onTouch execute,action is ACTION_DOWN
09-16 13:03:14.782 1720-1720/touch.touchdemo V/TAG: onTouch execute,action is ACTION_UP
09-16 13:03:14.782 1720-1720/touch.touchdemo V/TAG: onClick execute!

为什么只设置setOnTouchListener时只相应了 ACTION_DOWN,增加设置了setOnClickListener时ACTION_DOWN、ACTION_UP事件都得到相应呢?

这边补充一个android分发的重要知识点:<br />
关于dispatchTouchEvent的返回<br />

  1. 当我们给某个控件设置了Touch事件,当点击该控件时,会触发一系列的事件,如ACTION_DOWN,ACTION_MOVE,ACTION_UP。

  2. dispatchTouchEvent在进行事件分发时,如果某个ACTION返回了false,那么后面的ACTION都将得不到执行。也就是说,只有前一个ACTION返回true,后一个的ACTION才会得到执行。

当imageView只设置setOnTouchListener事件时:<br />
Imageview—不可点击setOnTouchListener<br />
-- onTouchEvent返回false(上面有分析过,不可点击onTouchEvent返回false)<br />
-- dispatchTouchEvent返回false<br />
在ACTION_DOWN时dispatchTouchEvent返回了false。后续的ACTION得不到执行。<br />

为什么设置了setOnClickListener后续的ACTION可以得到执行呢?<br />

setOnClickListener的源码:

    public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }

setOnClickListener方法,它先会去判断当前控件是否是Clickable的,如果不是Clickable的,则将当前控件设置为Clickable的。当我们调用了ImageView对象的setOnClickListener方法后,ImageView对象就已经变成了Clickable的,所以其表现和Button一致也是自然的。

View总结

  1. onTouch和onTouchEvent都是在dispatchTouchEvent方法中被调用的方法。onTouch会优先于onTouchEvent被执行。

  2. 如果onTouch通过返回true将事件消费掉,事件便不会传递到onTouchEvent中。特别要强调的一点是,只有当mOnTouchListener不为null并且控件是enabled,onTouch方法才会得到执行。

  3. dispatchTouchEvent在进行事件分发时,如果某个ACTION返回了false,那么后面的ACTION都将得不到执行。

  4. setOnClickListener方法会设置view为可点击。

接下来我们看 ViewGroup的事件分发:

结合下面的布局:

<touch.touchdemo.widget.CustomLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:orientation="vertical"
        android:id="@+id/customLayout"
        tools:context="touch.touchdemo.MainActivity">




    <Button
            android:id="@+id/btn1"
            android:textAllCaps="false"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Button1"
    />

    <Button
            android:id="@+id/btn2"
            android:textAllCaps="false"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Button2"
    />
</touch.touchdemo.widget.CustomLayout>

CustomLayout 继承LinearLayout:

public class CustomLayout extends LinearLayout {

    public CustomLayout(Context context) {
        super(context);
    }

    public CustomLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    public CustomLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public CustomLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev){
        return false;
    }

}

为button1、button2设置点击事件为 customLayout设置setOnTouchListener。

        customLayout.setOnTouchListener(new View.OnTouchListener(){

            @Override
            public boolean onTouch(View arg0, MotionEvent arg1) {
                Log.v("TAG","customLayout onTouch:"+ ViewTool.actionToString(arg1.getAction()));
                return false;
            }

        });
        btn1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.v("TAG","onClick execute!");
            }
        });

        btn2.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View arg0) {
                // TODO Auto-generated method stub
             

点击buttion1打印信息:

09-16 13:35:08.242 24349-24349/touch.touchdemo V/TAG: onClick execute!

点击buttion2打印信息:

09-16 13:35:27.438 24349-24349/touch.touchdemo V/TAG: onClick execute!

点击空白地方打印信息:

09-16 13:35:53.670 24349-24349/touch.touchdemo V/TAG: customLayout onTouch:ACTION_DOWN

修改CustomLayout中onInterceptTouchEvent的返回值为true:

public class CustomLayout extends LinearLayout {

    public CustomLayout(Context context) {
        super(context);
    }

    public CustomLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    public CustomLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public CustomLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev){
        return true;
    }

}

这时点击button1

09-16 13:38:34.694 27489-27489/touch.touchdemo V/TAG: customLayout onTouch:ACTION_DOWN

这时点击button1

09-16 13:38:34.694 27489-27489/touch.touchdemo V/TAG: customLayout onTouch:ACTION_DOWN

这时点击空白地方

09-16 13:38:34.694 27489-27489/touch.touchdemo V/TAG: customLayout onTouch:ACTION_DOWN

这是为什么呢?点击事件得不到执行了,只有ACTION_DOWN得到相应。<br />

这需要分析下ViewGroup的dispatchTouchEvent。源码比较长这里就不贴出来了,有兴趣的可以自己去看看。<br />
我们可以用一段伪代码来说明ViewGroup的dispatchTouchEvent主要作用

public boolean dispatchTouchEvent(MotionEvent e) {
    boolean consumed = false;
    if (onInterceptTouchEvent(e)) {
        consumed = onTouchEvent(e);
    } else {
        for (View view: childs) {
            consumed = view.dispatchTouchEvent(e);
            if (consumed) {
                break;
            }
        }
        if (!consumed) {
            consumed = onTouchEvent(e);
        }
    }    
    return consumed;
}
  1. 首先判断ViewGroup的onInterceptTouchEvent是否拦截,如果拦截执行自身的onTouchEvent
  2. 不拦截向下分发给自view去执行。
  3. 如果子view中有相应的处理(dispatchTouchEvent返回true),ViewGroup的dispatchTouchEvent返回true。
  4. 如果子view中没有相应的处理(dispatchTouchEvent返回flase),ViewGroup会再执行自身的onTouchEvent。

我们可以用一张流程图来说明这个过程:<br />

Paste_Image.png

结合这张图有兴趣的同学可以跑下demo中的流程打印验证下。

结合上面说的。我们来分析下ViewPager是怎么处理滑动冲突的:

Viewpager套Viewpager时的事件处理<br />

Paste_Image.png

demo中我们简单的写一个示例Viewpager套Viewpager如上图<br />
可以看到滑动点在里面的viewpager时,里面的viewpager滑动,滑动点在外面的viewpager只外面的viewpager滑动。<br />

我们看下Viewpager中的onInterceptTouchEvent实现:<br />
代码很多,关键是在ACTION_MOVE时,他是如果判断拦截与不拦截(拦截返回true和不拦截false)<br />
找到关键代码:

                if (dx != 0 && !isGutterDrag(mLastMotionX, dx) &&
                        canScroll(this, false, (int) dx, (int) x, (int) y)) {
                    // Nested view has scrollable area under this point. Let it be handled there.
                    mLastMotionX = x;
                    mLastMotionY = y;
                    mIsUnableToDrag = true;
                    return false;
                }

可以看出在viewpager的onInterceptTouchEvent的MotionEvent.ACTION_MOVE:<br />
会去判断当前显示的页面是否可以滑动,如果可以滑动,则将该事件丢给当前显示的页面处理。<br />

这种拦截法叫做:外部拦截法

所谓外部拦截法是指所有的触摸事件都会先经过经过父容器的传递,从而父容器在需要此触摸事件的时候就可以拦截此触摸事件,否者就传递给子View。这样就可以解决滑动冲突的问题,这种方法比较符合触摸事件的传递、处理机制。外部拦截法需要重写父容器的onInterceptTouchEvent方法,在该方法中根据滑动冲突处理规则做相应的拦截即可。<br />
可用下面的伪代码来表示:

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
 boolean intercepted = false;
 int x = (int) event.getX();
 int y = (int) event.getY();

 switch (event.getAction()) {
 case MotionEvent.ACTION_DOWN: {
     intercepted = false;
     break;
 }
 case MotionEvent.ACTION_MOVE: {
     if (父容器需要当前触摸事件) {
         intercepted = true;
     } else {
         intercepted = false;
     }
     break;
 }
 case MotionEvent.ACTION_UP: {
     intercepted = false;
     break;
 }
 default:
     break;
 }
 mLastXIntercept = x;
 mLastYIntercept = y;
 return intercepted;
}

我们继续:
现在的代码我们不兼容4.0以前的版本了,所有viewpager嵌套viewpager的实现简单了很多<br />

在 API13及前面的版本Viewpager 套Viewpager 直接写存在兼容问题。<br />

我可以通过源码来看为什么存在兼容:

                if (dx != 0 && !isGutterDrag(mLastMotionX, dx) &&
                        canScroll(this, false, (int) dx, (int) x, (int) y)) {
                    // Nested view has scrollable area under this point. Let it be handled there.
                    mLastMotionX = x;
                    mLastMotionY = y;
                    mIsUnableToDrag = true;
                    return false;
                }

canScroll的实现

    protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
        if (v instanceof ViewGroup) {
            final ViewGroup group = (ViewGroup) v;
            final int scrollX = v.getScrollX();
            final int scrollY = v.getScrollY();
            final int count = group.getChildCount();
            // Count backwards - let topmost views consume scroll distance first.
            for (int i = count - 1; i >= 0; i--) {
                // TODO: Add versioned support here for transformed views.
                // This will not work for transformed views in Honeycomb+
                final View child = group.getChildAt(i);
                if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
                        y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
                        canScroll(child, true, dx, x + scrollX - child.getLeft(),
                                y + scrollY - child.getTop())) {
                    return true;
                }
            }
        }

        return checkV && ViewCompat.canScrollHorizontally(v, -dx);
    }

ViewCompat.canScrollHorizontally的调用

    public static boolean canScrollHorizontally(View v, int direction) {
        return IMPL.canScrollHorizontally(v, direction);
    }

static final ViewCompatImpl IMPL;的实现

    static final ViewCompatImpl IMPL;
    static {
        final int version = android.os.Build.VERSION.SDK_INT;
        if (version >= 23) {
            IMPL = new MarshmallowViewCompatImpl();
        } else if (version >= 21) {
            IMPL = new LollipopViewCompatImpl();
        } else if (version >= 19) {
            IMPL = new KitKatViewCompatImpl();
        } else if (version >= 17) {
            IMPL = new JbMr1ViewCompatImpl();
        } else if (version >= 16) {
            IMPL = new JBViewCompatImpl();
        } else if (version >= 15) {
            IMPL = new ICSMr1ViewCompatImpl();
        } else if (version >= 14) {
            IMPL = new ICSViewCompatImpl();
        } else if (version >= 11) {
            IMPL = new HCViewCompatImpl();
        } else if (version >= 9) {
            IMPL = new GBViewCompatImpl();
        } else if (version >= 7) {
            IMPL = new EclairMr1ViewCompatImpl();
        } else {
            IMPL = new BaseViewCompatImpl();
        }
    }

可以找到api13以以下的canScrollHorizontally的实现:

        public boolean canScrollHorizontally(View v, int direction) {
            return (v instanceof ScrollingView) &&
                canScrollingViewScrollHorizontally((ScrollingView) v, direction);
        }

这边(v instanceof ScrollingView),因为v为viewpager,(v instanceof ScrollingView)为fasle,所有api13以以前的canScrollHorizontally反false,即没有实现滑动判断,永远都是flase。<br />
可以找到api14以以上的canScrollHorizontally的实现:

        public boolean canScrollHorizontally(View v, int direction) {
            return ViewCompatICS.canScrollHorizontally(v, direction);
        }

最终调用的是view.java中的

    public boolean canScrollHorizontally(int direction) {
        final int offset = computeHorizontalScrollOffset();
        final int range = computeHorizontalScrollRange() - computeHorizontalScrollExtent();
        if (range == 0) return false;
        if (direction < 0) {
            return offset > 0;
        } else {
            return offset < range - 1;
        }
    }

这里帮你做了是否可滑动判断。<br />
到这里我们就从源码层面分析了Viewpager 套Viewpager 兼容问题<br />

我们来兼容下:<br />

先介绍另一种滑动冲突的解决方法<br />
内部拦截法:<br />

  1. 内部拦截法是指父容器不拦截任何触摸事件,所有的触摸事件都传递给子元素,如果子元素需要此触摸事件就直接消耗掉,否者就交由父容器进行处理,(通过内部子元素来进行是否进行拦截)这种方法和Android中的事件传递、处理机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,使用起来较外部拦截法稍显复杂。这种方法需要重写子元素的dispatchTouchEvent方法。
  2. 子 View 可以使用 requestDisallowInterceptTouchEvent 影响去父 View 的分发,可以决定父 View 是否要调用 onInterceptTouchEvent 。比如,requestDisallowInterceptTouchEvent(true),父 View 就不用调用 onInterceptTouchEvent 来判断拦截,而就是不拦截。
    用伪代码表示为:

子元素的dispatchTouchEvent方法中<br />

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
 int x = (int) event.getX();
 int y = (int) event.getY();

 switch (event.getAction()) {
 case MotionEvent.ACTION_DOWN: {
     getParent().requestDisallowInterceptTouchEvent(true);
     break;
 }
 case MotionEvent.ACTION_MOVE: {
     int deltaX = x - mLastX;
     int deltaY = y - mLastY;
     if (父容器需要当前触摸事件) {
         getParent().requestDisallowInterceptTouchEvent(false);
     }
     break;
 }
 case MotionEvent.ACTION_UP: {
     break;
 }
 default:
     break;
 }

 mLastX = x;
 mLastY = y;
 return super.dispatchTouchEvent(event);
}

在demo代码中的的实现为:

public class ViewPagerCompat2 extends ViewPager {

    /** 触摸时按下的点 **/
    PointF downP = new PointF();
    /** 触摸时当前的点 **/
    PointF curP = new PointF();
    private int first = 1;
    private float mLastMotionX;
    private float mLastMotionY;
    public ViewPagerCompat2(Context context) {
        super(context);
    }

    public ViewPagerCompat2(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        final float x = ev.getX();
        final float y = ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);//告诉父view不拦截
                first = 1;
                mLastMotionX = x;
                mLastMotionY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                if (first == 1) {
                    if (Math.abs(x - mLastMotionX) < Math.abs(y - mLastMotionY)) {
                        first = 0;//y轴滑动拦截
                        getParent().requestDisallowInterceptTouchEvent(false);

                    } else {
                        //x轴滑动不拦截
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }

                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                getParent().requestDisallowInterceptTouchEvent(false);
                break;
        }
        return super.dispatchTouchEvent(ev);
    }
}

上面这种内部拦截法。当然兼容也可以使用外部拦截法:<br />

既然ViewPager在API14以上可以正常滑动重写了canScrollHorizontally(int)方法,查看ViewPager的canScrollHorizontally(int)方法源码发现此方法不存在版本兼容问题,在API13及其以下版本上也可直接调用。于是乎解决办法就是继承ViewPager重写canScroll(View, boolean, int, int, int)方法,直接调用canScrollHorizontally(int)即可,如下:

public class ViewPagerCompat extends ViewPager {
    public ViewPagerCompat(Context context) {
        super(context);
    }

    public ViewPagerCompat(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
        if(v instanceof ViewGroup){
            final ViewGroup group = (ViewGroup) v;
            final int scrollX = v.getScrollX();
            final int scrollY = v.getScrollY();
            final int count = group.getChildCount();
            // Count backwards - let topmost views consume scroll distance first.
            for (int i = count - 1; i >= 0; i--) {
                // TODO: Add versioned support here for transformed views.
                // This will not work for transformed views in Honeycomb+
                final View child = group.getChildAt(i);
                if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
                        y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
                        canScroll(child, true, dx, x + scrollX - child.getLeft(),
                                y + scrollY - child.getTop())) {
                    return true;
                }
            }
        }

        if(checkV){
            // Direct call ViewPager.canScrollHorizontally(int)
            if(v instanceof ViewPager){
                return ((ViewPager) v).canScrollHorizontally(-dx);
            }else{
                return ViewCompat.canScrollHorizontally(v, -dx);
            }
        }else{
            return false;
        }
    }
}

<br />
<br />
<br />
<br />

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容