Android事件分发机制详解(一)

点击/触摸事件

在Android开发中,我们不免碰到点击响应和触摸事件,最常见的栗子就是定义一个按钮,然后设置它的点击/触摸监听器:

...
Button btn = (Button) findViewById(R.id.btn);
// click
btn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Log.i("Event Dispatch", "button click");
    }
});

// touch
btn.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        Log.i("Event Dispatch", "button touch");
        return false;
    }
});
...

既设置了点击监听,又设置了触摸监听,那么谁会先触发呢,运行程序看log输出:

12-21 14:49:44.350 12994-12994/com.sye.eventdispatchdemo I/Event Dispatch: button touch
12-21 14:49:44.411 12994-12994/com.sye.eventdispatchdemo I/Event Dispatch: button touch
12-21 14:49:44.437 12994-12994/com.sye.eventdispatchdemo I/Event Dispatch: button click

很明显,先执行的是onTouch方法,然后再执行的是onClick方法,下面从这里为入口来分析View的事件分发机制。

View的事件分发机制

相信熟悉Android的童鞋都知道,在我们触摸屏幕时,之前会触发Activity::dispatchTouchEvent方法,然后通过Activity对触摸事件的分发到相应父布局的dispatchTouchEvent方法,当然,是指Activity所属的顶层View没有拦截掉触摸事件,这个后面再分析。父布局指的是LinearLayoutFrameLayout等一些常用布局,如果看了源码的童鞋就会知道这些类没有dispatchTouchEvent方法的,但是它们都是继承于ViewGroup,所以在Activity将事件分发到ViewGroup,最后再由ViewGroup将事件分发到对应的target View中处理,流程大致是这样,但是我们先不分析ViewGroup的事件分发,我们先从源码的角度来看一下更加简单的View的事件分发。

我们还是用上面的例子来介绍View的事件分发,当我们点击Button时,会触发ButtondispatchTouchEvent方法,而在Button类中并没有dispatchTouchEvent,那么就从它的父类TextView中找,但是在TextView中也没有,再往上在View中找到该方法的实现:

/**
* 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) {
...
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;
}
...
}

因为代码比较长,只贴关键的代码了。ListenerInfo类是View类中的一个静态类,主要作用是保存View的一些监听,例如OnLongClickListenerOnClickListener等等,这些可以通过阅读源码来了解。然后这里是将mListenerInfo赋值给li对象,在第一个if条件中有一个判断li.mOnTouchListener.onTouch(this, event),调用的是li对象的mOnTouchListener::onTouch方法,那么,我们就要找到li对象的mOnTouchListener是在哪里初始化的,其实就是mListenerInfo.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;
}

很明显,这里就是初始化mOnTouchListener的地方,而setOnTouchListener方法是我们最熟悉不过的方法了,它就是我们的栗子中触摸监听,所以在输出Log时首先会输出button touch,为什么会输出两次,因为触摸事件分为ACTION_DOWNACTION_UP
细心的童鞋也许会发现在栗子中OnTouchListener::onTouch方法还有一个返回值,这个返回值有什么用处呢,它默认为return false的,我们把它改成return true,再看看Log,就变成这样了:

12-21 17:15:44.227 22589-22589/com.sye.eventdispatchdemo I/Event Dispatch: button touch
12-21 17:15:44.281 22589-22589/com.sye.eventdispatchdemo I/Event Dispatch: button touch

是的,Click事件没有了!
为什么会没有,我们来分析下,在上面View类的dispatchTouchEvent方法中第一个if条件中的li.mOnTouchListener.onTouch(this, event)正好是我们栗子中的返回值,当我们return true时,相应的result = true(前面的三个条件会为true),那么下面的第二个if条件中的onTouchEvent方法就不会执行,到这里有的童鞋就会想,点击事件的处理难道会在onTouchEvent方法中?来看看就知道了:

public boolean onTouchEvent(MotionEvent event) {
...
switch (action) {
    case MotionEvent.ACTION_UP:
        ...
        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();
        }
        ...
        break;
    ...
...
}

根据栗子的Log,Click事件应该是在ACTION_UP时触发的,所以我们只需要看MotionEvent.ACTION_UP条件即可。在这个条件中会执行performClick方法,一是因为PerformClick这个Runnable中会执行performClick方法:

private final class PerformClick implements Runnable {
    @Override
    public void run() {
        performClick();
    }
}

二是如果!post(mPerformClick)返回为false,也会执行performClick方法。
我们来看看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;
}

这段代码与前面dispatchTouchEvent方法中那段代码有点相似,li对象的mOnTouchListener换成了mOnClickListener,同样的,我们找到mOnClickListener在哪里赋值的就行了,具体代码如下:

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

是不是很明白了,这就是我们栗子中设置的点击监听事件!然后再在performClick方法中回调onClick方法。

总结

经过上面的源码分析,View的事件分发机制大致应该清楚了。下面来总结一下:
首先点击一个View,会调用其dispatchTouchEvent方法,在dispatchTouchEvent方法中会先调用mOnTouchListener::onTouch回调方法,如果回调return true,则不会执行onTouchEvent方法,而click事件就是在onTouchEvent方法中执行的,所以当return false时才会响应click事件。
为了更好理解,贴出上面栗子的时序图:

Android事件分发机制详解(一)_第1张图片

留下一个问题:dispatchTouchEvent方法的返回值作用是什么?
下一篇文章介绍ViewGroup的事件分发机制,以及在点击屏幕后,上层事件是怎么传递的。并且上面的问题答案也会出来。
ps:源码为Android N的源码

你可能感兴趣的:(Android)