美文网首页
android事件分发

android事件分发

作者: king龙123 | 来源:发表于2017-01-23 14:47 被阅读0次

    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 />

    相关文章

      网友评论

          本文标题:android事件分发

          本文链接:https://www.haomeiwen.com/subject/mcyabttx.html