0
点赞
收藏
分享

微信扫一扫

事件分发(上篇)


上篇:探究View的事件分发(下篇探究ViewGroup的事件分发)

// 我们为一个按钮注册一个点击事件(onClick将会被回调)
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Log.d("TAG", "onClick execute");
}
});

// 我们再为这个按钮注册一个触摸事件(onTouch将会被回调)
button.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("TAG", "onTouch execute, action " +
event.getAction());
return false; // 这里我们返回false(默认)

运行
程序点击按钮,打印结果如下所示:

... onTouch execute, action 0
... onTouch execute, action 1
...

根据 Log 可知,onTouch是优先于onClick执行的,并且执行了两次。

一次点击事件是指:从 ACTION_DOWN 到 ACTION_UP (可能还会有多次 ACTION_MOVE 的执行)。

因此事件传递的顺序是先经过onTouch,再传递到onClick

注意到onTouch方法是有返回值的,这里我们返回的是false,
如果我们尝试把onTouch方法里的返回值改成true,再运行一次:

... onTouch execute, action 0
... onTouch execute, action 1

我们发现,onClick方法不再执行了!为什么会这样呢?

我们可以先理解成当onTouch方法返回true时点击事件被onTouch方法消耗掉了,因而不会再继续向下传递。

我们知道,只要点击了某个控件,该控件的dispatchTouchEvent方法就会被调用。

然而控件本身并没有这个方法,于是最终找到其父类View的dispatchTouchEvent方法:

public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event); // 否则

在这个方法内,进行了一个if判断,如果这个if判断中的三个条件都为true,就返回true,否则就去执行onTouchEvent方法并返回其值。(爆料:该值由控件是否可点击决定)

很显然onTouch方法总是优先于其他方法被执行。

另外注意:只有当if判断中的前两个条件都为true,onTouch方法才会被执行。

因此如果你有一个控件是disable的,给它注册touch事件,onTouch方法将永远不会被执行。

对于这一类控件,如果想要监听它的touch事件,就必须重写该控件的onTouchEvent方法。

继续分析,先看第一个条件:mOnTouchListener != null。该变量是在哪里被赋值的呢?

public void setOnTouchListener(OnTouchListener l) {
mOnTouchListener = l;
}

最终我们发现这个变量正是在View的setOnTouchListener方法中被赋值的。

也就是说只要我们给控件注册了touch事件,mOnTouchListener就会被赋值(非空)。

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

再看第三个条件:OnTouchListener.onTouch(this, event)。
关键条件。就是去回调该控件注册touch事件时的onTouch方法。
很显然如果我们在onTouch方法里返回true,就会使全部条件成立,从而使整个方法返回true。
否则就会导致onTouchEvent方法被执行。

以上的分析恰恰验证了:onTouch优先于onClick执行。
并且如果onTouch方法返回true,那么dispatchTouchEvent方法也将直接返回true,
程序不会再继续向下执行,onClick方法也就不会被执行了。

以上的分析还透漏出了:onClick方法的调用肯定是发生在onTouchEvent方法中的!
我们来验证一下。请看onTouchEvent方法:

public boolean onTouchEvent(MotionEvent event) {
// ...
// Button默认都是clickable可点击的
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
// 经过各种if判断...
performClick();
break;
// ...
}
return true; // 第89行
}
return

onTouchEvent方法复杂了很多,不过没关系,我们只挑重点看。
首先在第14行:如果该控件是可点击的,就会进入到第16行的switch判断中。
而如果当前的事件是ACTION_UP,则会进入到对应的case之中。
然后经过各种if判断,最终会执行到第38行performClick()方法,于是我们进入到这个方法中查看:

public boolean performClick() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
if (mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
}
return false;
}

可以看到,这里的mOnClickListener只要不为空,就会去调用它的onClick方法,
那么mOnClickListener又是在哪里被赋值的呢?
于是我们找到setOnClickListener方法:

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

很显然,只要我们给控件注册了click事件,就会给mOnClickListener赋值。
每当控件被点击时,都会在performClick()方法里回调该控件注册click事件时的onClick方法。

还有一个重要的知识点需要说明:点击事件的层级传递。
如果给一个控件注册了点击事件,每次点击都会触发ACTION_DOWN,ACTION_MOVE,ACTION_UP系列事件。
如果在执行ACTION_DOWN的时候返回了false,后面一系列其它的ACTION就不会再被执行。
也就是说,dispatchTouchEvent在进行事件分发的时候,只有前一个ACTION返回true,才会触发后一个ACTION。

View(可点击的)的onTouchEvent方法默认都会消耗事件(返回true),
因此这个View的dispatchTouchEvent方法默认返回true。该结论将会在下篇被用到。

我们回头看看,如果在onTouch方法中返回了false,进入到onTouchEvent方法中,
由于按钮默认都是可点击的,导致该方法总是会执行到第89行,返回true。
于是ACTION_DOWN后面的ACTION仍会被执行。
如果将按钮替换成ImageView(默认是不可点击的)则会符合我们所期望的那样。
ImageView的布局文件中增加android:clickable=”true”的属性,即变成可点击的。


举报

相关推荐

0 条评论