Activity.onTouchEvent等方法的调用顺序
有一种面试题会问题Activity.onTouchEvent, View.onTouchEvent, OnTouchListener.onTouch的调用顺序
太长不看: OnTouchListener.onTouch > View.onTouchEvent > Activity.onTouchEvent
有一件事情我们可以看成是已知的: View和Activity中的事件是从ViewRootImpl这个类出发调用的
我需要从这个起点开始, 其它的太底层了
ViewRootImpl对象持有DecorView mView对象
但是如果在ViewRootImpl的代码里搜索mView.dispatchTouchEvent会发现没有这个调用
实际运行会发现实际调用的是View.dispatchPointerEvent这个方法
public final boolean dispatchPointerEvent(MotionEvent event) {
if (event.isTouchEvent()) {
return dispatchTouchEvent(event);
} else {
return dispatchGenericMotionEvent(event);
}
}
接着进入DecorView的dispatch方法:
public boolean dispatchTouchEvent(MotionEvent ev) {
final Window.Callback cb = mWindow.getCallback();
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}
说明Window的Callback对象拥有优先处理权
而Activity的attach方法中创建了PhoneWindow对象并且设置了自身为Callback:
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
说明dispatch首先会到activity中
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
但是activity又首先把处理权交给了window的superDispatchTouchEvent方法, 这个方法会调用decor的同名方法, 接着会调用到默认的View的dispatchTouchEvent方法
换句话说, 如果我们不覆盖Activity的dispatch方法, 那么Activity.onTouchEvent的优先级是最低的
View.onDispatchTouchEvent中处理顺序是这样的:
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;
}
说明listener的优先级更高, 这样处理的原因是当View的内部实现是不能更改的时候, 我们可以通过设置listener来覆盖view的默认行为
另一个问题是View.OnClickListener是如何被触发的, 观看代码后会发现, 这里涉及的东西其实也不少, 但是最常出现的流程其实只有两步
一个最简单的流程是这样的:
View.onTouchEvent:
ACTION_DOWN: setPressed()
ACTION_UP: post(mPerformClick) || performClickInternal();
performClickInternal -> performClick -> onClickListener != null && onClickListener.onClick(this)
onTouchEvent -> onClick 的流程中考虑了这些东西:
第一:
- View是否是clickable的, 是否是enabled
- 如果!clickable的 => 没有操作, 并且不消耗touch事件, 返回false
- 如果是clickable的
- 如果disabled => 没有操作, 但是消耗touch事件, 返回true, 这个有点儿奇怪, 可是它就是这么设定的
- enabled && clickable => 这种情况才会处理touch事件
View的默认clickable是从View_clickable属性读取的, 常见的控件中Button, ImageButton, EditText是默认clickable的
如果查看过Button的源码会发现这个类继承TextView, 但是却几乎没有任何改变, 最重要的改动是它的第二个构造方法(inflater调用的构造方法)中, 指定了从当前主题的buttonStyle中读取属性
而默认主题的buttonStyle指向了Widget.Button, 其中设置了clickable:
<style name="Theme">
<item name="buttonStyle">@style/Widget.Button</item>
</style>
<style name="Widget.Button">
...
<item name="focusable">true</item>
<item name="clickable">true</item>
...
</style>
可是button的默认background太丑了, 所以我一般用textview来代替button, 在设置onClickListener的时候clickable也会被设为true:
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
第二: 其次会考虑的是View是否在一个可以滚动的parent中, 如果不是的, 那么会直接setPressed(true, x, y), 如果是的话, 那不会直接调用, 会设置为PrePressed状态, 并postDelay一个CheckForTap对象, 如果100ms后这个对象还没有被移除, 说明此时手势还在进行, 这时CheckForTap.runnable会调用setPressed(true, x, y)
这个机制可以解释为什么在ListView/RecyclerView中快速滚动时, item的background不会触发pressed状态, 但是如果很慢地挪动, 那么手指下方地view的pressed状态会被触发
判断是否在“可以滚动的parent”中的方法是isInScrollingContainer, 它其实是递归调用parent的shouldDelayChildPressedState方法, 这个方法在ViewGroup的默认实现中其实是返回true的, 在FrameLayout/LinearLayout等类中覆盖为false. 文档说默认为true的理由是兼容性, 不知道是为啥
判断是否是长按也用了一样的机制, 在ACTION_DOWN时, 如果不是在可滚动的parent中, 会postDelayed一个CheckForLongPress对象, 如果是在可滚动的parent中, 那么在CheckForTap.run触发的时候会postDelayed一个CheckForLongPress对象
第三: 在ACTION_UP事件中, 还会多考察一个focus状态, 基本的逻辑是:
boolean focusTaken = isFocusable() && isFocusableInTouchMode() && !isFocused() && requestFocus();
if (!focusTaken) {
post(mPerformClick) || performClickInternal();
}
focusTaken只有在当前View是可focusableInTouchMode并且当前状态为非focused, 并且获取focus成功的情况下才是true
我们先拿三个常见的控件的默认情况来对比一下:
- TextView: 既不是focusable, 也不是focusableInTouchMode
- Button: 是focusable, 但是不是focusableInTouchMode
- EditText: 既是focusable, 也是focusableInTouchMode
那么:
- TextView: isFocusableInTouchMode为false -> focusTaken = false
- Button: isFocusableInTouchMode为false -> focusTaken = false;
- EditText: isFocusableInTouchMode为true, 如果当前不是焦点, 那么EditText一般会成功获取焦点, focusTaken = true, 否则如果已经是焦点, 那么focusTaken = false
那么绝大多数情况下都会触发performClick
只有EditText不一样, 给EditText设置一个OnClickListener的话, 第一次点击会获取焦点, 但是不触发点击事件, 第二次才会触发点击事件