事件分发是安卓中一个很重要的机制,是实现用户交互的必要存在。那么啥是事件呢,事件就是用户与应用UI交互的动作。通俗点说,就是你对手机触摸屏幕摸摸点点的各种猥琐操作。一个完整的触摸事件分为按下,移动,抬起三个过程。也可能没有移动的过程。那么今天咱们就通过一个简单的点击事件的流程来了解一下View的事件分发是如何实现的。
首先,我们得知道一个必要的知识点,那就是事件分发是从View的dispatchTouchEvent()方法开始的。那么咱们就来看看View里面的dispatchTouchEvent()方法是怎么写的。研究一下当初设计这个的作者的实现思想。这次咱们的代码版本是比较新的API24 android N 版本的。代码贴上:
<span style="font-family:Microsoft YaHei;font-size:18px;"> /**
* 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 (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
return true;
}
if (onTouchEvent(event)) {
return true;
}
}
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
return false;
} </span>
API24的dispatchOnTouchEvent()方法相比旧一些的API来说,多了一些代码,比如校验事件的完整性等等。在上面的代码看来,代码还不是很多,虽然有些看不懂的,但咱们挑懂的看也足以让我们了解清楚它的机制。
首先,判断一下InputEventConsistencyVerifier的实例对象mInputEventConsistencyVerifier是否不等于null,如果不等于null,那就去执行它里面的onTouchEvent方法。我也不知道这个方法到底干了什么,不过InputEventConsistencyVerifier作用就是对输入事件的完整性做一个检查,检查事件的ACTION_DOWN 和 ACTION_UP 是否一一配对。很多同学可能在Android Logcat 里看到过以下一些类似的打印:"ACTION_UP but key was not down." 就出自此处。(备注,这段InputEventConsistencyVerifier作用是我Copy来的,看我多么诚实)所以我们就知道了,这里估计是用来做一个事件完成性的检查的。所以,咱们继续往下看。
if (onFilterTouchEventForSecurity(event)),这句话咱们点过去这个方法里面,可以看到这么个注释:
/**
* Filter the touch event to apply security policies.
*
* @param event The motion event to be filtered.
* @return True if the event should be dispatched, false if the event should be dropped.
*
* @see #getFilterTouchesWhenObscured
*/
public boolean onFilterTouchEventForSecurity(MotionEvent event) {
//noinspection RedundantIfStatement
if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
&& (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
// Window is obscured, drop this touch.
return false;
}
return true;
}
通过注释可以知道,这是筛选应用安全策略的触摸事件,嗯。好像知道了也没啥卵用。至少听上去跟我们的事件分发好像没什么关系。那咱们就不管它了。继续往下走!
接下来终于到关键部分的代码了:
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
return true;
}
if (onTouchEvent(event)) {
return true;
}
这么第一行代码咱们就忽略掉,应该不是什么重要的信息。好吧,其实是我实在翻译不出来。咱们往下看,2行,用一个引用接收ListenerInfo类型的成员变量。然后首先去判断它是否不等于null,继而判断他的OnTouchListener对象是否也不等于null,最后如果都成立即不等于空,那就去判断接下来一个,mViewFlags & ENABLED_MASK 的结果是否等于 ENABLED,这里就涉及一个很巧妙的构思了。用&来判断是否存在这个状态,这里说到底就是判断当前这个view是否enable的。简单解释一下这个巧妙的构思,因为不是咱们文章内容重点。平时我们区分状态的时候,比如打开状态OPEN,或者关闭状态CLOSE。那么一般的写法就是用两个常量分别代表这两种状态,然后判断的时候就去用多个if else一个个判断。这里呢,也是用两个常量去分别代表。可是它们常量的值却不是普通的1,0;而是1<<0,1<<1;这样。那么1<<0也就是0..0000001,0..0000010,这里因为int类型长度是32位,所以就是有32个位置可以存,每个位置表示一种状态,假如咱们的view的状态值是0..0000011的话,与上0..0000001结果就是0..0000001。结果是一样的,就可以断定它是存在这么一种状态,咱们用的时候就不需要用多个if else来判断了,而是用一个if去判断即可,节省的大量的代码。而且也是一种非常高逼格的写法。好。话不多说,说了简单说明就简单说明。咱们继续看。
如果以上3种条件都满足,那么就会去执行li.mOnTouchListener.onTouch(this,event)这句话,根据它的返回值来决定这个if条件是否成立,如果成立,那么就直接返回了true;这么来,第一个流程就走完了。这里的事件处理就交由OnTouchListener 里的onTouch方法去处理了。其实,如果你的mOnTouchListener对象不为空,那么就肯定是通过
<span style="font-family:Microsoft YaHei;font-size:18px;">public void setOnTouchListener(OnTouchListener l) {
getListenerInfo().mOnTouchListener = l;
}</span>
去设置了它的值,所以它不会为空,onTouch也是处理触摸事件的,既然你有了一个处理事件的入口,那么接下来的onTouchEvent也就没有执行的必要啦。一口饭完整的饭只能给一个人吃。对吧。所以就直接return了,下面的
<span style="font-family:Microsoft YaHei;font-size:18px;">if (onTouchEvent(event)) {
return true;
}</span>
就自然没法执行到了。
然后呢,ouTouch方法里面的代码,也是你通过setOnTouchListener传入的匿名内部类OnTouchListener 里面去实现了。至于如何实现的,那就看你代码怎么写了。
接下来,我们看看如果不符合第一个if的判断条件,代码往下走会怎么样,也就是
<span style="font-family:Microsoft YaHei;font-size:18px;">if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
&& (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0)</span>
这句话有任何一个结果是false,那么就不会进去这里面,然后往下走到了
<span style="font-family:Microsoft YaHei;font-size:18px;">if (onTouchEvent(event)) {
return true;
}</span>
这里来。那么咱们就又点击进去这个onTouchEvent方法里面去看看。
<span style="font-family:Microsoft YaHei;font-size:18px;"> /**
* 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 int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == 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));
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
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);
}
if (!mHasPerformedLongPress) {
// 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();
}
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();
}
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true);
checkForLongClick(0);
}
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
break;
case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();
// 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;
}</span>
这里的代码不是一般的多了,咱们来好好研读研读。这种代码怎么看呢,首先看看注释,注释是一个表明作者写这个方法的目的或者一些他的思想,所以很重要的。首先第一句说明了,实现这个方法去处理屏幕触摸事件。那这么一来,我就突然很好奇onTouch方法的注释是怎么写的了
/**
* 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);
这里写着,这个onTouch是在触摸事件分发到view的时候被调用,这允许监听者在触摸事件发送到这个目标view之前做出一些响应。好吧,这也间接说明了onTouch方法是比onTouchEvent方法早一步执行的。
咱们回到上面的代码,矮油,好长啊。没事,咱们坚持住,继续往下看,下面还有个小提示,如果这个方法用来检测点击操作,就是单击事件咯。那么这个作者建议我们去实现和调用这个performClick()方法。这样确保系统行为的一致性。
接下来就走进这个方法去看看。看源码得先挑看得懂的看,然后在扩散去理解。咱们看看有哪些是我们看得懂的。咱们可以看到37行的地方,有个判断viewFlags,也就是view的状态标志。判断它是否存在点击状态可点击,或者长点击。如果当前View是可以点击的,那么就会走39行的switch判断里面去,然后又经过了一堆堆的判断,走到了咱们有些熟悉的71行,performClick()方法里面去了,咱们点进去看看。
<span style="font-size:18px;"> /**
* 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() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
return true;
}
return false;
}</span>
又经过一些判断,比如如果mListenerInfo不为null,最后条件满足就会调用它的onClick方法,这么一来,点击事件就被执行到了。mListenerInfo又怎么来的呢?
public void setOnClickListener(OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
一番寻找,就会发现,它是通过setOnClickListener给设置上的,这么说来,onClick方法的具体操作也是由我们来决定的。所以,当你给一个控件通过setOnClickListener 设置了点击事件监听的时候,屏幕被点击时,事件一层层的传递,最后到了你的这个view的dispatchOnTouchEvent方法里面去,然后又经过我们上面的那么一大堆操作,最后调用到你设置好的OnClickListener的onClick方法。这么一来,我们的View的整个事件分发的流程也就清楚了。dispatchOnTouchEvent到onTouchEvent再到具体的onClick等各种事件处理方法。
最后还有个重要的点要说明。如果你给一个控件注册了onTouch事件,每次点击这个控件的时候就会触发一系列的DOWN,MOVE,UP的action,如果你在DOWN的action里面返回了false,那么接下来的MOVE,UP等所有在DOWN之后的action都不会被执行到了,只有上一个action返回了true才会执行下一个action。这里咱们来两个例子,分别给button和imageview注册一个onTouch事件
首先来个imageView的
<span style="font-size:18px;">imageView.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("Sakura", "actionId" + event.getAction());
return false;
}
}); </span>
结果:
在我疯狂的点击4次,加很多次的拖动下,它都只能是DOWN被执行到。DOWN的action对应值就是0。咱们接下来看button的。
<span style="font-size:18px;">button.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("Sakura", "actionId" + event.getAction());
return false;
}
});</span>
结果:
哎呦,卧槽。见鬼了,这怎么都可以执行,这不打我脸么。其实一切都在我的掌控当中,button有些不太一样。我们回到onTouchEvent方法里面去看看。因为你在onTouch里面返回了false之后,在dispatchOnTouchEvent方法里面,就肯定会走onTouchEvent方法。那么在onTouchEvent方法37行的位置,判断如果这个view是可点击的,那就会走到里面去。然后咱们使劲往下拖,在139行的位置,就会看到一个return true;这么一来,就可以知道了,如果当前控件是可点击的,那么它就会默认给我们return true。从而使下面的action可以被执行到。那么imageView呢,默认是不可点击的,所以就不会走到里面去,该false还是false。符合我们所说的。
这么一来,View的事件分发的所有事,咱们都讲完啦。想必大家也比之前知道了不少东西。