事件分发机制原理分析(案例和源码)-View篇

这篇文章用于记录我在学习Android事件分发的一些小感悟,在我自己学习的过程当中,我发现android事件分发这一块是一个非常关键的点,因为在自定义控件里面经常需要使用到,但是,现在普遍的博文,要么写得太晦涩,太难懂,要么就是好像是自己能用越高大上的词语表述就越显得自己牛,或者通俗到对于一个新手而言,根本看不懂的地步,总之就是看了等于白看,浪费时间。因此,我自己觉得应该写一篇,让初来乍到的人,看得懂的博文,告诉后来的人,为什么会这样子。也算为开源社区,尽一点绵薄之力了。

对于事件分发机制的整体理解

什么是事件分发呢,从词义上去理解,就是一个一个的事件分别发送出去,在我们这篇博文里面,主要介绍的对象就是MotionEvent(点击事件)一系列操作,引起的分发过程。整体总是很抽象,是吧。不要紧,往下看。

业务场景需求还原

考虑下面一个场景,有时候在开发过程中,比如在一个自定义的Item里面放了一个checkBox,这个时候,我们需要点击Item的时候,顺便选中checkBox,然后对Item条目上的文字进行更新。很简单,为item设置一个OnclickListener,并把checkbox的ischeck方法作为返回值传给Item的ischeck,然后更改item上的条目,就行了呗。一切都是那么自然,但是很快业务就出现问题了,那就是当我们点击checkBox的时候,checkbox被选中,当时,item上的文字却没有更新。一图胜千言。

《事件分发机制原理分析(案例和源码)-View篇》

很常见的业务需求,但是为什么当我们点击checkBox的时候,虽然checkbox切换成为了ischeck的状态,但是文字没有更新呢,这就涉及到了,今天要分析的内容,就是因为点击事件最终传递到了, Item的子view,checkBox里面,而更新item的文字的业务逻辑代码是写在Item里面的,所以当然没法更新了。这里只是简单提一下,什么业务下会经常处理这个问题。因为要讲清楚这个问题,需要重新开一篇博文了,还是比较有价值的,有时间我会整理一篇出来的。今天就先分析View的事件分发。

案例分析

在这里需要提一下,我对代码世界的整体理解,那就是代码世界本质上也是人生活世界的一种投射,所以很多设计本质上都是遵循人在日常生活中的一些基本规律,只是站在了一个更高的角度上去用一门语言去描述罢了。所以开始我们的代码吧。

新建一个自定义的button

package site.yanhui.viewdispatchevent.view;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
/**
 * Created by Archer on 2017/10/2.
 * <p>
 * 功能描述: 自定义一个button,用来理解view的事件分发机制
 *
 */
public class MyButton extends android.support.v7.widget.AppCompatButton {
    private static final String TAG = "MyButton";
    public MyButton(Context context) {
        super(context);
    }
    public MyButton(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public MyButton(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    @Override
    public boolean dispatchTouchEvent(MotionEvent event)
    {
        int action = event.getAction();
        switch (action)
        {
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG, "dispatchTouchEvent ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG, "dispatchTouchEvent ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.e(TAG, "dispatchTouchEvent ACTION_UP");
                break;
            default:
                break;
        }
        return super.dispatchTouchEvent(event);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event)
    {
        int action = event.getAction();
        switch (action)
        {
            case MotionEvent.ACTION_DOWN:
                Log.e(TAG, "onTouchEvent ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.e(TAG, "onTouchEvent ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.e(TAG, "onTouchEvent ACTION_UP");
                break;
            default:
                break;
        }
        return super.onTouchEvent(event);
    }
}

在xml中引用

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="site.yanhui.viewdispatchevent.MainActivity">
<site.yanhui.viewdispatchevent.view.MyButton
    android:id="@+id/btn_my_button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="MyButton"/>
</LinearLayout>

最后看一下MainActivity

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    @BindView(R.id.btn_my_button)
    MyButton btnMyButton;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        btnMyButton.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                int action = event.getAction();
                switch (action)
                {
                    case MotionEvent.ACTION_DOWN:
                        Log.e(TAG, "onTouch ACTION_DOWN");
                        break;
                    case MotionEvent.ACTION_MOVE:
                        Log.e(TAG, "onTouch ACTION_MOVE");
                        break;
                    case MotionEvent.ACTION_UP:
                        Log.e(TAG, "onTouch ACTION_UP");
                        break;
                    default:
                        break;
                }
                return false;
            }
        });
    }
    @OnClick(R.id.btn_my_button)
    public void MyButtonClicked() {
//        Toast.makeText(this, "Mybutton is clicked", Toast.LENGTH_SHORT).show();
        Log.d(TAG, "MyButtonClicked: MyButton");
    }
}

很简单的布局,照着默认的来就行,然后给button设置一个touch和onclick的监听事件就可以了。接下来我们就可以开始分析了。

//down
10-03 01:17:24.581 16564-16564/site.yanhui.viewdispatchevent E/MyButton: dispatchTouchEvent ACTION_DOWN
10-03 01:17:24.581 16564-16564/site.yanhui.viewdispatchevent E/MainActivity: onTouch ACTION_DOWN
10-03 01:17:24.581 16564-16564/site.yanhui.viewdispatchevent E/MyButton: onTouchEvent ACTION_DOWN
//move
10-03 01:17:24.644 16564-16564/site.yanhui.viewdispatchevent E/MyButton: dispatchTouchEvent ACTION_MOVE
10-03 01:17:24.646 16564-16564/site.yanhui.viewdispatchevent E/MainActivity: onTouch ACTION_MOVE
10-03 01:17:24.646 16564-16564/site.yanhui.viewdispatchevent E/MyButton: onTouchEvent ACTION_MOVE
//move
10-03 01:17:24.664 16564-16564/site.yanhui.viewdispatchevent E/MyButton: dispatchTouchEvent ACTION_MOVE
10-03 01:17:24.664 16564-16564/site.yanhui.viewdispatchevent E/MainActivity: onTouch ACTION_MOVE
10-03 01:17:24.664 16564-16564/site.yanhui.viewdispatchevent E/MyButton: onTouchEvent ACTION_MOVE
//move
10-03 01:17:24.721 16564-16564/site.yanhui.viewdispatchevent E/MyButton: dispatchTouchEvent ACTION_MOVE
10-03 01:17:24.722 16564-16564/site.yanhui.viewdispatchevent E/MainActivity: onTouch ACTION_MOVE
10-03 01:17:24.722 16564-16564/site.yanhui.viewdispatchevent E/MyButton: onTouchEvent ACTION_MOVE
//up
10-03 01:17:24.723 16564-16564/site.yanhui.viewdispatchevent E/MyButton: dispatchTouchEvent ACTION_UP
10-03 01:17:24.723 16564-16564/site.yanhui.viewdispatchevent E/MainActivity: onTouch ACTION_UP
10-03 01:17:24.724 16564-16564/site.yanhui.viewdispatchevent E/MyButton: onTouchEvent ACTION_UP
//onclick
10-03 01:17:24.728 16564-16564/site.yanhui.viewdispatchevent D/MainActivity: MyButtonClicked: MyButton

注意看输出,event.getAction()的值有三个,012分别对应了,按下,移动,抬起三种状态。仔细看上面的代码,当我们给Button设置了touch的监听以后,onclick方法是最后才执行的。也就是说是紧跟up这个动作之后,从现实规律的角度来考虑,我们应该分解一下点击事件,分别是按下(down),抬起(up),中间可能手抖一下,就出现了移动(move),然后才会完成点击的这次事件。因此我们的log才会打印出数据。所以touch方法的优先顺序肯定是高于click方法的,因为click方法本质上是一系列touch方法结束以后的回调。

接下来我们看一下,我们针对上面的代码看一下具体的执行顺序。

首先执行的dispatchTouchEvent >onTouch > onTouchEvent 。

然后我们把touch方法的返回值设置为true看看有什么效果。

  btnMyButton.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            int action = event.getAction();
            switch (action)
            {
                case MotionEvent.ACTION_DOWN:
                    Log.e(TAG, "onTouch ACTION_DOWN");
                    break;
                case MotionEvent.ACTION_MOVE:
                    Log.e(TAG, "onTouch ACTION_MOVE");
                    break;
                case MotionEvent.ACTION_UP:
                    Log.e(TAG, "onTouch ACTION_UP");
                    break;
                default:
                    break;
            }
            return true;//设置为true,之前为false,运行一下程序
        }
    });
}

结果:

//down
10-03 02:23:20.417 12088-12088/site.yanhui.viewdispatchevent E/MyButton: dispatchTouchEvent ACTION_DOWN
10-03 02:23:20.418 12088-12088/site.yanhui.viewdispatchevent E/MainActivity: onTouch ACTION_DOWN
//move
10-03 02:23:20.423 12088-12088/site.yanhui.viewdispatchevent E/MyButton: dispatchTouchEvent ACTION_MOVE
10-03 02:23:20.423 12088-12088/site.yanhui.viewdispatchevent E/MainActivity: onTouch ACTION_MOVE
10-03 02:23:20.501 12088-12088/site.yanhui.viewdispatchevent E/MyButton: dispatchTouchEvent ACTION_MOVE
10-03 02:23:20.501 12088-12088/site.yanhui.viewdispatchevent E/MainActivity: onTouch ACTION_MOVE
10-03 02:23:20.517 12088-12088/site.yanhui.viewdispatchevent E/MyButton: dispatchTouchEvent ACTION_MOVE
10-03 02:23:20.517 12088-12088/site.yanhui.viewdispatchevent E/MainActivity: onTouch ACTION_MOVE
10-03 02:23:20.535 12088-12088/site.yanhui.viewdispatchevent E/MyButton: dispatchTouchEvent ACTION_MOVE
10-03 02:23:20.535 12088-12088/site.yanhui.viewdispatchevent E/MainActivity: onTouch ACTION_MOVE
10-03 02:23:20.552 12088-12088/site.yanhui.viewdispatchevent E/MyButton: dispatchTouchEvent ACTION_MOVE
10-03 02:23:20.552 12088-12088/site.yanhui.viewdispatchevent E/MainActivity: onTouch ACTION_MOVE
10-03 02:23:20.568 12088-12088/site.yanhui.viewdispatchevent E/MyButton: dispatchTouchEvent ACTION_MOVE
10-03 02:23:20.568 12088-12088/site.yanhui.viewdispatchevent E/MainActivity: onTouch ACTION_MOVE
//up
10-03 02:23:20.568 12088-12088/site.yanhui.viewdispatchevent E/MyButton: dispatchTouchEvent ACTION_UP
10-03 02:23:20.568 12088-12088/site.yanhui.viewdispatchevent E/MainActivity: onTouch ACTION_UP

看一下,发现,OnTouchEvent方法就没有执行了,仔细看看,onclick方法也没有执行了,也就是说,当我们把Touch方法的返回值设置为true的时候,首先是onTouchEvent方法失效,然后导致了onclick方法失效了,也就是OnTouchEvent方法里面肯定存在一个onclick回调函数,让我们在mainActivity里面实现,当我们ontouch置为true以后,导致了事件分发的中断。(事件被消费了)

首先你需要知道一点,只要你触摸到了任何一个控件,就一定会调用该控件的dispatchTouchEvent方法。那当我们去点击按钮的时候,就会去调用Button类里的dispatchTouchEvent方法,可是你会发现Button类里并没有这个方法,那么就到它的父类TextView里去找一找,你会发现TextView里也没有这个方法,那没办法了,只好继续在TextView的父类View里找一找,这个时候你终于在View里找到了这个方法。这是什么呢,这就是继承,在源码设计里面的体现,大大的减少了代码的冗余性

《事件分发机制原理分析(案例和源码)-View篇》

也就是说问题的根源就在这个最先执行的dispatchTouchEvent里面。图来自

源码分析

View的dispatchTouchEvent方法

//行号在9988的地方
/**
 * 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);
    }
  //第一个注意的点,初始值为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 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;
}

可以看到,相比于5.0的源码,我使用的8.0的源码作为分析,可以看到家里很多部分,顺着下来仔细看看,什么一致性确认,安全性,手势什么的等的单词,说明之后的android的安全性的确做了很多的优化,看不懂没关系,这些不是我们关心的。

行号20

boolean result = false;

result的初始值是false。

行号64

return result;

看到这个方法最终返回的值是一个result,也就是说只要result一旦被赋值就会被立马返回出去。

行号37-44

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

我们看到了一个if语句(40到42行),里面有四个条件的判断。

  • li != nul
  • li.mOnTouchListener != null
  • (mViewFlags & ENABLED_MASK) == ENABLED
  • li.mOnTouchListener.onTouch(this, event)

当这四个值的返回值都为真的时候,result置为true

第一个条件ListenerInfo li = mListenerInfo;决定了第一行条件为true。

第二个条件,我们看一下,在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;
}

setOnTouchListener,是不是有点眼熟啊,对啊,这不是我们在mainActivity里面设置的嘛,所以肯定不为空啊

第三个条件(mViewFlags & ENABLED_MASK) == ENABLED是判断当前点击的控件是否是enable的,按钮默认都是enable的,因此这个条件恒定为true。

第四个条件li.mOnTouchListener.onTouch(this, event),看它调用的是onTouch方法,当然要跟进去看看咯。

public interface OnTouchListener {
    /**
     * Called when a touch event is dispatched to a view. This allows listeners to
     * get a chance to respond before the target view.
     *
     * @param v The view the touch event has been dispatched to.
     * @param event The MotionEvent object containing full information about
     *        the event.
     * @return True if the listener has consumed the event, false otherwise.
     */
    boolean onTouch(View v, MotionEvent event);
}

找到了吧,我们在给button设置 OnTouchListener的时候,当然要实现这个onTouch方法了,读读注释,当返回true的时候,这个事件就被消费了,所以就满足了四个条件为真,后面的代码就不会再执行了。

小结一下

分析到这里,先小结一下。如果你想要让一个可以点击的控件禁用其点击事件,那么需要把dispatchTouchEvent的返回值置为true就行了,那么到底是要怎么把它置为true,就是把OnTouchListener的回调OnTouch方法的返回值置为true。因为一旦OnTouch方法返回true,四个条件全部满足,result=true,直接返回出去了,OnTouchEvent方法就不会再执行,而Onclick方法的回调是在OntouchEvent方法中得到的回调。所以不执行就没有Onclick方法。如果你能看懂我现在得到的结论,说明你已经很好的理解了,我谈及的内容,如果不理解,回头多读几遍,接下来我们当然不希望控件的Ontouch方法返回true了,因此还是置为false,这样我们才可以继续分析源码的调用过程。

行号47-49

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

看到这里是不是很明了了,首先需要明白一点,代码能够走到这里,说明了,Ontouch方法一定是返回的false,因为一旦Ontouch方法返回的是true那么直接弹出去了,因此说明,result还是false,if的两个条件第一个肯定为真了,就看第二个的onTouchEvent的返回值。

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

行号24-33

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

注释写的很清楚了,即便是一disabled的view,只要被点击了仍然会消费点击事件。

行号34-38

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

如果有代理就交给代理去做

行号40-148

这个部分就相当于,剩下的代码部分,就是我们需要重点看的部分了。

if (((viewFlags & CLICKABLE) == CLICKABLE ||
           (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
           (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE)

先判断这个view是不是可以点击,可以的话就进入到if循环里面,后面陆续做一些获取焦点的判断和方法,但这并不是我们的重点,我们继续往前更近。

行号67-76

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

首先一路看着下来,我们发现focusTaken为false,置反以后呢,恒为true,也就是说,当一个view有焦点并且点击的时候,首先会去看看这个mPerformClick的情况,如果为为空就闯建一个,然后post过去,最终会调用到一个performClick()方法,更进去看一眼。

performClick()

/**
    * Call this view's OnClickListener, if it is defined.  Performs all normal
    * actions associated with clicking: reporting accessibility event, playing
    * a sound, etc.
    *
    * @return True there was an assigned OnClickListener that was called, false
    *         otherwise is returned.
    */
   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;
   }

看了上面这个方法,是不是觉得有点眼熟啊,尤其是if里面的几个条件,当全部满足的时候,执行的是li.mOnClickListener.onClick(this);,当然得跟进去看看了。

public interface OnClickListener {
       /**
        * Called when a view has been clicked.
        *
        * @param v The view that was clicked.
        */
       void onClick(View v);
   }

现在是不是很明白了,这就是说,当我们给一个控件绑定OnClickListener的时候会调用的到的onClick方法,聊到这里,需要特别注意的是,这个方法,是在up(手抬起来)以后触发的,这也就解释了,为什么onclic方法为什么会在更在后面,逻辑很清晰。

/**
    * Register a callback to be invoked when this view is clicked. If this view is not
    * clickable, it becomes clickable.
    *
    * @param l The callback that will run
    *
    * @see #setClickable(boolean)
    */
   public void setOnClickListener(@Nullable OnClickListener l) {
       if (!isClickable()) {
           setClickable(true);
       }
       getListenerInfo().mOnClickListener = l;
   }

这就是给一个view绑定setOnClickListener的过程,这也就保证了li.mOnClickListener!=null的条件,因为我们在MainActivity里面绑定的,也实现了onClick方法。

行号150

这里还需要注意一点,就是在onTouchEvent方法里面,只要能够进行action的判断,那么最终的返回值是true,而且onTouchEvent的返回值也决定了dispatchTouchEvent的返回值,也就是说决定是事件是否被消费了。在这里的demo的控件的的传递中,当然是被这个button消费了。所以调用了onTouchEvent方法。分析到这里,相信你也明白,这个事件分发是什么情况了。

参考文献和相关推荐

郭霖的相关分享

鸿洋的相关分享

zxy的相关分享

工匠若水的相关分享

Carson_Ho的相关分享

源码

事件分发View篇

    原文作者:Android源码分析
    原文地址: https://juejin.im/entry/59db37de6fb9a00a4f1b634a
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞