Android事件分发之View事件处理关键及示例分析
大胃粥 人气:0目的
网上已经有很多关于事件分发的优秀文章,为何我还要自己写?因为别人总结的毕竟都是别人的,自己亲自阅读源码不仅会让自己更懂得原理,也会让自己记得更清楚,而且也会发现另一番天地。
View处理事件的关键
由于所以的控件都直接或者间接继承自View,因此View的事件分发机制就是最基础的一环,需要首先掌握其原理。
那么View的事件从哪里来的呢?当然是父View(一个ViewGroup)。父View在寻找能处理事件的子View的时候,会调用子View的dispatchTouchEvent()
把事件传递给子View,如果子View的dispatchTouchEvent()
返回true
,代表子View处理了该事件,如果返回flase
就代表该子View不处理事件。如果所有子View都不处理该事件,那么就由父View自己处理。
今天我们这篇文章就是来分析View如何处理事件。我们重点关心View.dispatchTouchEvent()
啥时候返回true(代表处理了事件),啥时候返回false(代表不处理事件)。
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) { boolean result = false; final int actionMasked = event.getActionMasked(); // 当窗口被遮挡,是否过滤掉这个触摸事件 if (onFilterTouchEventForSecurity(event)) { ListenerInfo li = mListenerInfo; // 1. 外部监听器处理 if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } // 2. 自己处理 if (!result && onTouchEvent(event)) { result = true; } } return result; }
可以看到View.dispatchTouchEvent()
处理事件非常简单,要么交给外部处理,要么自己来处理。只要任何一方处理了,也就相应的处理函数返回true
,View.dispatchTouchEvent()
就返回true
,代表View处理了事件。否则,View.dispatchTouchEvent()
返回false
,也就是View不处理该事件。
首先它把事件交给外部进行处理。外部处理指的什么呢?它指的就是交给setOnTouchListener()
设置的监听器来处理。如果这个监听器处理时返回true
,也就是OnTouchListener.onTouch()
方法返回true
,View.dispatchTouchEvent()
就返回true
,也就说明View处理了该事件。否则交给自己来处理,也就是交由onTouchEvent()
处理。
当然,如果要让事件监听器来处理,还必须要让View处于enabled
状态。可以通过setEnabled()
方法来改变View的enabled
状态。并且可以通过isEnabled()
查询View是否处于enabled
状态。
当外部无法处理时,也就是上面的三个条件有一个不满足时,就交给View.onTouchEvent()
来处理。此时View.onTouchEvent()
的返回值就决定了View.dispatchTouchEvent()
的返回值。也就是决定了View是否处理该事件。那么,我们来看下View.onTouchEvent()
什么时候返回true
,什么时候返回false
。
public boolean onTouchEvent(MotionEvent event) { // 判断View是否可点击(点击/长按) final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; // 处理View是disabled状态的情况 if ((viewFlags & ENABLED_MASK) == DISABLED) { // ... return clickable; } // 如果有处理代表,就先交给它处理。如果它不处理,就继续交给自己处理 if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } // 如果可以点击,最后会返回true,代表处理了View处理了事件 if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { // ... return true; } return false; }
这太让人意外了,只要View可以点击(点击/长按),就返回true
,否则返回false
。
忽略触摸代理(Touch Delegate
)和CONTEXT_CLICKABLE
的特性,因为不常用,如果遇到了,可以再来查看。
那么,View默认可以点击,长按吗?当然是不能。这需要子View自己去设置,例如Button在构造函数中就设置了自己可以点击。
我们从代码角度解释下View默认是否可以点击和长按
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { // View构造函数中解析android:clickable属性 case com.android.internal.R.styleable.View_clickable: // 第二个参数false,表示View默认不可点击 if (a.getBoolean(attr, false)) { viewFlagValues |= CLICKABLE; viewFlagMasks |= CLICKABLE; } break; // View构造函数中解析android:longClickable属性 case com.android.internal.R.styleable.View_longClickable: // 第二个参数false,表示View默认不可长按 if (a.getBoolean(attr, false)) { viewFlagValues |= LONG_CLICKABLE; viewFlagMasks |= LONG_CLICKABLE; } break; }
在View的构造函数中分别接下了android:clickable
和android:longClickable
属性,从默认值可以看出,View默认是不可点击和长按的。也就是说View默认不处理任何事件。
那么,我们用一张图来解释View如何处理触摸事件的
通过这张图,我们就可以清楚的了解到View.dispatchTouchEvent()
在什么情况下返回 true
,在什么情况下返回false
。也就了解了View在什么情况下处理了事件,在什么情况下不处理事件。
View.onTouchEvent()分析
View事件处理就这么简单吗?如果你只关心事件分发到哪里,以及谁处理了事件,那么掌握上面的流程就够了。
但是你是否还有个疑问,View.onTouchEvent()
在干啥呢?OK,如果你保持这份好奇心,那么接着往下看。
View.onTouchEvent()
其实处理了三种情况
- 处理点击事件
- 处理长按事件
- 处理View状态改变
- 处理tap事件
处理长按事件
case MotionEvent.ACTION_DOWN: mHasPerformedLongPress = false; // 1. 判断是否在一个滚动的容器中 boolean isInScrollingContainer = isInScrollingContainer(); if (isInScrollingContainer) { // 1.1 在滚动容器中 // ... } else { // 1.2 不是在滚动容器中 // 设置按下状态 setPressed(true, x, y); // 检测长按动作 checkForLongClick(0, x, y); } break;
setPressed()
方法首先会设置View为按下状态, 代码如下
mPrivateFlags |= PFLAG_PRESSED;
然后,通过checkForLongClick()
来检测长按动作,这是如何实现呢
private void checkForLongClick(int delayOffset, float x, float y) { if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) { mHasPerformedLongPress = false; if (mPendingCheckForLongPress == null) { mPendingCheckForLongPress = new CheckForLongPress(); } // 在长按超时的时间点,执行一个Runable,也就是CheckForLongPres postDelayed(mPendingCheckForLongPress, ViewConfiguration.getLongPressTimeout() - delayOffset); } } private final class CheckForLongPress implements Runnable { @Override public void run() { if (isPressed() && (mParent != null) && mOriginalWindowAttachCount == mWindowAttachCount) { // 执行长按动作 if (performLongClick(mX, mY)) { // 如果处理了长按动作,mHasPerformedLongPress为true mHasPerformedLongPress = true; } } } }
其实它是把CheckForLongPress
这个Runnable
加入到Message Queue
中,然后在ViewConfiguration.getLongPressTimeout()
这个长按超时的时间点执行。
这是什么意思呢?首先在ACTION_DOWN
的时候我检测到按下的动作,那么在还没有执行ACTION_UP
之前,如果按下动作超时了,也就是超过了长按的时间点,那么我会执行长按动作performLongClick()
。我们现在看下执行长按做了哪些事情
public boolean performLongClick(float x, float y) { // 记录长按的位置 mLongClickX = x; mLongClickY = y; // 执行长按的动作 final boolean handled = performLongClick(); // 重置数据 mLongClickX = Float.NaN; mLongClickY = Float.NaN; return handled; } public boolean performLongClick() { return performLongClickInternal(mLongClickX, mLongClickY); } private boolean performLongClickInternal(float x, float y) { boolean handled = false; final ListenerInfo li = mListenerInfo; // 1. 执行长按监听器处理动作 if (li != null && li.mOnLongClickListener != null) { handled = li.mOnLongClickListener.onLongClick(View.this); } // 2. 如果长按监听器不处理,就显示上下文菜单 if (!handled) { final boolean isAnchored = !Float.isNaN(x) && !Float.isNaN(y); handled = isAnchored ? showContextMenu(x, y) : showContextMenu(); } // 3. 如果处理了长按事件,就执行触摸反馈 if (handled) { performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); } return handled; }
有三个动作在长按的时候执行
- 如果调用过
setOnLongClickListener()
给View设置长按事件监听器,那么首先把长按事件交给这个监听器处理。如果这个监听器返回true
,代表监听器已经处理了长按事件,那么直接执行第三步的触摸反馈,并返回。如果这个监听器返回了false
,代表监听没有处理长按事件,那么就执行第二步,交给系统处理。 - 当第一步处理不了时,系统自己来处理,它会显示一个上下文菜单。
- 执行触摸反馈。
如果你不了解什么是上下文菜单(Context Menu
)和触摸反馈(Haptic Feednack
),可以自行搜索下。
我们已经了解了如果触发长按做了哪些动作,但是我们也要记得触发长按的时机,那就是从手指按下到抬起的时间要超过长按的超时时间。如果没有超过这个长按超时时间,在ACTION_UP
的时候,系统会怎么做呢?
case MotionEvent.ACTION_UP: // 处于按下状态 if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { // 没有执行长按动作 if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { // 移除长按动作 removeLongPressCallback(); } } break;
当检测到ACTION_UP
时,如果见到了View处于按下状态,但是还没有执行长按动作。也就是说,还没有达到长按的时间点,手指就抬起了,那么系统就会移除在ACTION_DOWN
添加的长按动作,之后长按动作就不会触发了。
处理点击事件
我们先分析了长按事件而没有分析点击事件,其实是为了更好的讲清楚点击事件,看代码
case MotionEvent.ACTION_DOWN: if (isInScrollingContainer) { } else { // 设置按下状态 setPressed(true, x, y); } break;
当检测到ACTION_DOWN
事件,首先的给它设置一个按下标记,这个前面说过。然后在没有达到长按超时这个时间点前,如果检测到ACTION_UP
事件,那么我们就可以认为这是一次点击事件
case MotionEvent.ACTION_UP: // 处于按下状态 if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { // 没有执行长按 if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { // 移除长按动作 removeLongPressCallback(); // focusTaken是在touch mode下有效,现在讨论的是简单的手指触摸 if (!focusTaken) { // 创建点击事件 if (mPerformClick == null) { mPerformClick = new PerformClick(); } // 通过Message Queue执行点击事件 if (!post(mPerformClick)) { performClick(); } } } } break;
Touch Mode
模式与D-pad
有关,读者可以查阅官方文档说明。
有了前面关于长按事件的知识,这里就非常好理解了。
如果没有执行长按动作,就先移除长按回调,那么以后就不会再执行长按动作了。相反,如果已经执行长按动作,那么就不会执行点击事件。
performClick()
用来执行点击事件,那么来看下它做了什么
public boolean performClick() { final boolean result; final ListenerInfo li = mListenerInfo; if (li != null && li.mOnClickListener != null) { // 1. 首先交给外部的点击监听器处理 playSoundEffect(SoundEffectConstants.CLICK); li.mOnClickListener.onClick(this); result = true; } else { // 2. 如果没有外部监听器,就不处理了 result = false; } return result; }
点击事件的处理非常简单粗暴,默认就不处理,也就是返回false
。当然,如果你想处理,调用setOnClickListener()
即可。
###处理View状态改变
响应View状态改变的操作都集中在setPressed()
方法中,其实我们再进一步思考下,View只对按下和抬起的状态进行响应
public void setPressed(boolean pressed) { // 1. 判断是否需要执行刷新动作 final boolean needsRefresh = pressed != ((mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED); // 2. 设置状态 if (pressed) { // 设置按下状态 mPrivateFlags |= PFLAG_PRESSED; } else { // 取消按下状态 mPrivateFlags &= ~PFLAG_PRESSED; } // 3. 如果需要刷新就刷新View管理的Drawable状态 if (needsRefresh) { refreshDrawableState(); } // 4. 如果是ViewGroup,就需要把这个状态分发给子View dispatchSetPressed(pressed); }
如果手指按下了,会调用setPressed(true)
,如果手指抬起了,会调用setPressed(false)
。
假设我们手指刚按下,那么就需要执行第三步的刷新Drawable状态的动作
public void refreshDrawableState() { // 标记Drawable状态需要刷新 mPrivateFlags |= PFLAG_DRAWABLE_STATE_DIRTY; // 执行Drawable状态改变的动作 drawableStateChanged(); // 通知父View,子View的Drawable状态改变了 ViewParent parent = mParent; if (parent != null) { parent.childDrawableStateChanged(this); } }
首先设置PFLAG_DRAWABLE_STATE_DIRTY
标记,表示Drawable状态需要更新,然后调用drawableStateChange()
来执行Drawable状态改变动作
@CallSuper protected void drawableStateChanged() { // 1. 获取Drawable新状态 final int[] state = getDrawableState(); boolean changed = false; // 2. 为View管理的各种Drawable设置新状态 final Drawable bg = mBackground; if (bg != null && bg.isStateful()) { changed |= bg.setState(state); } final Drawable fg = mForegroundInfo != null ? mForegroundInfo.mDrawable : null; if (fg != null && fg.isStateful()) { changed |= fg.setState(state); } if (mScrollCache != null) { final Drawable scrollBar = mScrollCache.scrollBar; if (scrollBar != null && scrollBar.isStateful()) { changed |= scrollBar.setState(state) && mScrollCache.state != ScrollabilityCache.OFF; } } // 3. 为StateListAnimator设置新状态,从而改变Drawable if (mStateListAnimator != null) { mStateListAnimator.setState(state); } // 4. 如果有Drawable状态更新了,就重绘 if (changed) { invalidate(); } }
既然我们要给Drawable更新状态,那么就的获取新的状态值,这就是第一步所做的事情,我们来看下getDrawableState()
如何获取新状态的
public final int[] getDrawableState() { // 如果Drawable状态没有改变,就直接返回之前的状态值 if ((mDrawableState != null) && ((mPrivateFlags & PFLAG_DRAWABLE_STATE_DIRTY) == 0)) { return mDrawableState; } // 如果状态值不存在,或者Drawable状态需要更新 else { // 创建状态值 mDrawableState = onCreateDrawableState(0); // 重置PFLAG_DRAWABLE_STATE_DIRTY状态 mPrivateFlags &= ~PFLAG_DRAWABLE_STATE_DIRTY; return mDrawableState; } }
刚刚,我们设置了PFLAG_DRAWABLE_STATE_DIRTY
,标志着Drawable状态需要更新,因此这里会调用onCreateDrawableState()
来获取
protected int[] onCreateDrawableState(int extraSpace) { // 默认是没有设置DUPLICATE_PARENT_STATE状态 if ((mViewFlags & DUPLICATE_PARENT_STATE) == DUPLICATE_PARENT_STATE && mParent instanceof View) { return ((View) mParent).onCreateDrawableState(extraSpace); } int[] drawableState; // 2. 根据各种flag, 获取状态 int privateFlags = mPrivateFlags; int viewStateIndex = 0; if ((privateFlags & PFLAG_PRESSED) != 0) viewStateIndex |= StateSet.VIEW_STATE_PRESSED; if ((mViewFlags & ENABLED_MASK) == ENABLED) viewStateIndex |= StateSet.VIEW_STATE_ENABLED; if (isFocused()) viewStateIndex |= StateSet.VIEW_STATE_FOCUSED; if ((privateFlags & PFLAG_SELECTED) != 0) viewStateIndex |= StateSet.VIEW_STATE_SELECTED; if (hasWindowFocus()) viewStateIndex |= StateSet.VIEW_STATE_WINDOW_FOCUSED; if ((privateFlags & PFLAG_ACTIVATED) != 0) viewStateIndex |= StateSet.VIEW_STATE_ACTIVATED; if (mAttachInfo != null && mAttachInfo.mHardwareAccelerationRequested && ThreadedRenderer.isAvailable()) { // This is set if HW acceleration is requested, even if the current // process doesn't allow it. This is just to allow app preview // windows to better match their app. viewStateIndex |= StateSet.VIEW_STATE_ACCELERATED; } if ((privateFlags & PFLAG_HOVERED) != 0) viewStateIndex |= StateSet.VIEW_STATE_HOVERED; final int privateFlags2 = mPrivateFlags2; if ((privateFlags2 & PFLAG2_DRAG_CAN_ACCEPT) != 0) { viewStateIndex |= StateSet.VIEW_STATE_DRAG_CAN_ACCEPT; } if ((privateFlags2 & PFLAG2_DRAG_HOVERED) != 0) { viewStateIndex |= StateSet.VIEW_STATE_DRAG_HOVERED; } // 2. 把状态值转化为一个数组 drawableState = StateSet.get(viewStateIndex); if (extraSpace == 0) { return drawableState; } }
首先根据各种标志位,例如mPrivateFlags
和mPrivateFlags2
,来获取状态的值,然后根据状态的值获取一个状态的数组。
我想你一定想直到这个状态数组是咋样的,我举个例子,View默认是enabled
状态,那么mViewFlags
默认设置了ENABLED
标记,当我们手指按下的时候,mPrivateFlags
设置了PFLAG_PRESSED
按下状态标记。如果值选择这两个情况来获取状态值,那么viewStateIndex = VIEW_STATE_PRESSED | VIEW_STATE_ENABLED
,用二进制表示就是11000
。然后通过StateSet.get(viewStateIndex)
转化为数组就是[StateSet.VIEW_STATE_ENABLED, StateSet.VIEW_STATE_PRESSED]
。
现在,我们获取到Drawable新的状态值,那么就可以进行drawableStateChanged()
函数的第二步,为各种Drawable设置新的状态值,例如背景Drawable,前景Drawable。这些Drawable根据这些新的状态值,自己判断是否需要更新Drawable,例如更新显示的大小,颜色等等。如果更新了Drawable,那么就会返回true
,否则返回false
。
drawableStateChanged()
函数的第三步,还针对了StateListAnimator
的处理。StateListAnimator
会根据View状态值,改变Drawable的显示。
如果大家不了解StateListAnimator
,可以网上查阅下它的使用,这样就可以对View
状态改变有更深层次的理解。
drawableStateChanged()
函数的第四步,如果有任意Drawable改变了状态,那么就需要View进行重绘。
处理tap事件
case MotionEvent.ACTION_DOWN: mHasPerformedLongPress = false; // 1. 判断View是否在滚动容器中 boolean isInScrollingContainer = isInScrollingContainer(); if (isInScrollingContainer) { // 标记要触发tab事件 mPrivateFlags |= PFLAG_PREPRESSED; if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } mPendingCheckForTap.x = event.getX(); mPendingCheckForTap.y = event.getY(); // 2. 如果View在滚动容器中,那么检测一个tab动作 postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { } break;
第一步,判断View是否在一个滚动的容器中
public boolean isInScrollingContainer() { ViewParent p = getParent(); while (p != null && p instanceof ViewGroup) { if (((ViewGroup) p).shouldDelayChildPressedState()) { return true; } p = p.getParent(); } return false; }
通过循环遍历父View,并调用父View的shouldDelayChildPressedState()
方法来判断父View是否是一个滚动容器。
那么什么样的ViewGroup
是滚动容器呢?例如ScrollView
就是一个滚动容器,因为它有让子View滚动的特性,所以shouldDelayChildPressedState()
返回true
。而LinearLayout
就不是一个滚动容器,它本身没有设计滚动特性,因此shouldDelayChildPressedState()
返回false
。
当View处于一个滚动容器中,并且容器处于滚动中,这个View需要检测一个tap
事件,也就是表示快速点击。它有个触发的超时时间,大小为100ms(长按的触发超时时间是500ms),因此只要按下的事件超过100ms, 都算作一次tap
事件。那么,我们先来看下触发tap
事件都做了啥事
private final class CheckForTap implements Runnable { public float x; public float y; @Override public void run() { // 先取消tab的标记 mPrivateFlags &= ~PFLAG_PREPRESSED; // 设置按下状态 setPressed(true, x, y); // 检测长按事件 checkForLongClick(ViewConfiguration.getTapTimeout(), x, y); } }
当触发了tap
事件,首先取消标记,表示tap
事件已经执行。然后,既然已经发生了点击事件,那么自然要设置按下状态。最后由于tap
事件是在长按事件之前触发,那么当tap
事件触发后,自然要去检测长按事件是否触发。
我们刚刚说到,tap
事件触发的条件是,在滚动容器中,从手指按下到抬起的时间要过100ms。那么如果在100ms之前抬起了手指,那么会怎么处理呢,我们来看下ACTION_UP
的处理逻辑
case MotionEvent.ACTION_UP: // 判断tap动作是否已经完成 boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; // 如果是按下状态或者还没有触发tap动作 if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { // 1. 如果还没有触发tap动作,就设置按下状态 if (prepressed) { setPressed(true, x, y); } if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { // 移除长按回调 removeLongPressCallback(); // 2. 执行点击事件 if (!focusTaken) { if (mPerformClick == null) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { performClick(); } } } // 3. 移除tap回调 removeTapCallback(); } break;
prepressed
为true
表示没有执行tap
事件,那么当检测到手指抬起时,先设置按下状态。如果连tap
都没执行,肯定也不会执行长按事件,因此只会执行点击事件。最后,移除长按回调,这样tap
事件就不会再触发。
如果tap
事件执行了呢?只有一点差别,将会在第二步,根据是否执行了长按来决定是否执行点击事件。
总结
通过本文的分析,我们可以清楚的知道View如何处理父View传递过来的事件,也可以清楚知道View在什么时候处理事件,什么时候不处理事件。
另外,本文也对View.onTouchEvent()
作出分析,我们可以清楚知道View如何处理点击事件,如何处理长按事件,如何处理状态改变,以及如何处理tap
事件。
加载全部内容