码农翻身

浅谈Android事件分发机制

- by MRyan, 2020-03-07


浅谈Android事件分发机制

这是本人第一次写博文,主要是想记录自己的学习过程(毕竟菜鸟一个还需非常努力),仅是个人观点,如果有问题错误,请各位大佬指出,立刻改正绝不会一错再错
前因
当重写Button的ontouch()事件时返回true不执行onclick返回false则执行,原理是什么?为什么要这么做?这些小疑惑阻挡了我一点点进步,于是查资料学习了Android的事件分发机制,写下了自己的理解。

正文:

进入正题

1.什么是点击事件。

2.什么是事件分发。

3.事件如何进行传递和分发顺序。

4.事件分发的方法。

5.源码分析。

6.实例分析
依此解决上面的小疑惑吧。

  • 1.什么是点击事件

        简单的来说就是当我们点击屏幕的时候,就会产生点击事件也就是Touch,事件的类型有四种:
        

    |MotionEvent.ACTION_DOWN|手指触摸按下 |

MotionEvent.ACTION_UP手指抬起
MotionEvent.ACTION_MOVE手指移动
MotionEvent.ACTION_CANCEL非人为原因的结束事件

很好理解吧。
举个生活中的例子:
按手印 就是一系列事件组成。这里纸就相当于View,
手指按下(MotionEvent.ACTION_DOWN)——手指抬起( MotionEvent.ACTION_UP )一个手印出现了。
用手指写血书 也是一系列事件组成。这里书就相当于View,手指按下(MotionEvent.ACTION_DOWN)——手指移动(MotionEvent.ACTION_MOVE)——手指抬起( MotionEvent.ACTION_UP ) 一个大气的字出现了。

由这个很生活的例子可以发现,一系列的事件都必须是以(DOWN)事件开始,(UP)事件结束,中间可以有(MOVE)事件也可以没有
产生事件了以后通过参数 MotionEvent传递给需要的VIEW进行处理。

  • 2.什么是事件的分发

                通俗的说上面举的例子中 手印的出现,血字的出现过程就是点击事件传递的过程。
  • 3.事件如何进行传递和传递顺序

        这里我们需要了解,Android的ui界面是由**Activity**,**View**,**ViewGroup**所组成     ViewGroup是View的子类,但是ViewGroup能包含View,举个例子,LinearLayout是ViewGroup,而Button是View。

    而事件的分发顺序是Activity——ViewGroup——View

  • 4.事件分发的方法

       这里有dispatchTouchEvent() (事件分发)、onInterceptTouchEvent()(事件拦截)和onTouchEvent()(事件处理)三个方法

    这里需要注意(onInterceptTouchEvent()是VIewGroup独有的)

也就是说当产生点击事件,dispatchTouchEvent() 就会调用 如果是ViewGroup 就会调用onInterceptTouchEvent() 判断是否拦截事件 然后在dispatchTouchEvent() 内部onTouchEvent()会被调用。

  • 5.源码分析

        接下来我们来进行源码分析吧,首先附上View中dispatchTouchEvent(MotionEvent event)源码 进行重要部分分析
   /**
     * Pass the touch screen motion event down to the target view, or this
     * view if it is the target.
     *
     * @param event The motion event to be dispatched.
     * @return True if the event was handled by the view, false otherwise.
     */
    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)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatemen
  /******************************请注意这***************************************/
            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;
    }

上面这一大堆就是View的dispatchTouchEvent(MotionEvent event)源码(真的好多好多)请看我标出来的那部分源码

  /******************************请注意这***************************************/
            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 (li != null && li.mOnTouchListener != null

                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event))`

也就是说当着4个参数都为真的时候,执行下面的 result = true; 语句,而这四个参数中有一个不为真则都返回false,如果返回false则满足` if (!result && onTouchEvent(event)) {

            result = true;
        }`就会执行onTouchEvent(event)方法。而 result 和dispatchTouchEvent(MotionEvent event)返回值相同。

继续对条件判断进行分析首先第个参数li != null li肯定不为null,为再来看第个参数li.mOnTouchListener != null这也就是说当我们调用了setOnTouchListener的方法它会给mOnTouchListener 赋值,所以既然我们用了这个方法第二个参数也为

 /**
     * 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;/*第二个参数*/
    }

我们再来看第个参数 (mViewFlags & ENABLED_MASK) == ENABLED这句代码是通过位运算来判断控件的属性(激活还是未激活)当我们创建一个控件的时候默认会激活(ENABLED)所以第三个参数也为

现在就差第个参数(也是最后一个关键的参数)li.mOnTouchListener.onTouch(this, event)在上面咱们说到,如果四个参数都为真,则执行 result = true; 如果有一个不为真则执行onTouchEvent(event),上面咱们判断了其余三个参数都为真,所以第四个参数就是决定性的参数,也就是说li.mOnTouchListener.onTouch(this, event)为真执行 result = true;(从而使得View.dispatchTouchEvent()直接返回true,事件分发结束) li.mOnTouchListener.onTouch(this, event)为假就执行onTouchEvent(event)。磨磨唧唧说了这么多废话,那到底li.mOnTouchListener.onTouch(this, event)返回什么呢,这就看我们回调重写onTouch()中返回什么它就是什么。

假设我们让onTouch()返回false 则执行onTouchEvent(event)
我们来看onTouchEvent(event)源码

/**
     * 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();

        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return clickable;
        }
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    if ((viewFlags & TOOLTIP) == TOOLTIP) {
                        handleTooltipUp();
                    }
                    if (!clickable) {
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        break;
                    }
                    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)) {
                                    performClickInternal();
                                }
                            }
                        }

                        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:
                    if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                        mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                    }
                    mHasPerformedLongPress = false;

                    if (!clickable) {
                        checkForLongClick(0, x, y);
                        break;
                    }

                    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, x, y);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    if (clickable) {
                        setPressed(false);
                    }
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    break;

                case MotionEvent.ACTION_MOVE:
                    if (clickable) {
                        drawableHotspotChanged(x, y);
                    }

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

            return true;
        }

        return false;
    }

这也太*多了,难受香菇,不用担心,我们来挑重要代码进行分析找到case MotionEvent.ACTION_DOWN:/触摸按下事件*

我们来看这
`if (isInScrollingContainer) {

                    mPrivateFlags |= PFLAG_PREPRESSED;
                    if (mPendingCheckForTap == null) {
                        mPendingCheckForTap = new CheckForTap();
                    }
                    mPendingCheckForTap.x = event.getX();
                    mPendingCheckForTap.y = event.getY();
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                } `

简单的讲就是通过post向UI线程中发送消息,然后会执行 performClick()方法,这个方法是执行点击事件。看到这里会不会哎呦忽然知道了什么?,没错,也就是onclick()是在ontouch()处理的时候后发生的,也就是ontouch()优先于onclick()
我们再来分析performClick()源码

public boolean performClick() {
        // We still need to call this method to handle the cases where performClick() was called
        // externally, instead of through performClickInternal()
        notifyAutofillManagerOnClick();

        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);

        notifyEnterOrExitForAutoFillIfNeeded(true);

        return result;
    }

其中`if (li != null && li.mOnClickListener != null) {

        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }`

又是这个判断,虽然不一样但是类似,我们看li!=null是,第二个参数li.mOnClickListener != null当我们调用控件的setOnclicklistener()事件是,mOnClickListener 会被赋值,所以第个参数也为 所以控件点击事件被处理。

以上说了这么多会不会有点懵,没关系,我们通过实例来证明以上的观点

  • 6.实例分析
    创建一个布局,添加一个button当作测试控件
package a_text.com.tasdasdasd;

import android.app.Activity;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;

public class MainActivity extends Activity {
    private Button button;
    private static final String tast="日志分析";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button=findViewById(R.id.button);
        button.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.i(tast,"执行了onTouch()+ 动作是:" + event.getAction());
                return false;
            }
        });
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.i(tast,"执行了onClick()");
            }
        });
    }
}

运行程序,点击button 查看日志
测试1
会发现没毛病onTouch()优先级确实比onClick()高,也证实了我们以上的说法。

接下来我们将onTouch()返回值改成true那会怎么样呢,按照我们之前的说法将不会执行onClick()方法,也就是日志中不会在有执行了onClick(),究竟是不是这样呢。请看日志:
测试2
我们发现叮咚,我们对了,证实了之上的观点。

总结:onTouch();返回true将会阻碍事件继续向下传递,
所以onClick()方法不会被执行,反之执行onClick()方法。

现在我们回到我最开始提出的问题,为什么我在重写onTouch()方法中返回true,按钮不执行onclick方法,而返回值改成false则正确,通过上面的浅谈你应该会有想法了。
白话总结:
View.dispatchTouchEvent派发事件是传递的,如果返回值为true将停止下次事件派发,如果返回false将继续下次派发。例如,当前派发ACTION_DOWN事件,如果返回false则继续派发ACTION_UP,如果返回true派发完ACTION_DOWN就停止了,所以接受不到ACTION_UP、ACTION_MOVE。
结束语:
好了,到这里就结束了,作为一个菜鸟,我还是有梦想的,通过不断学习中进步。因为本人水平有限,所以本文只是浅谈,有更多的细节没有介绍到,如果有问题,希望大佬们指出,写这个文章是希望记录自己的学习过程,温故而知新。

作者:MRyan


本文采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。
转载时请注明本文出处及文章链接。本文链接:https://www.wormholestack.com/archives/158/
2024 © MRyan 47 ms