网上已经有不少关于事件分发的优秀文章,为什么我还要本身写?由于别人总结的毕竟都是别人的,本身亲自阅读源码不只会让本身更懂得原理,也会让本身记得更清楚,并且也会发现另外一番天地。java
因为因此的控件都直接或者间接继承自View,所以View的事件分发机制就是最基础的一环,须要首先掌握其原理。android
那么View的事件从哪里来的呢?固然是父View(一个ViewGroup)。父View在寻找能处理事件的子View的时候,会调用子View的dispatchTouchEvent()
把事件传递给子View,若是子View的dispatchTouchEvent()
返回true
,表明子View处理了该事件,若是返回flase
就表明该子View不处理事件。若是全部子View都不处理该事件,那么就由父View本身处理。windows
今天咱们这篇文章就是来分析View如何处理事件。咱们重点关心View.dispatchTouchEvent()
啥时候返回true(表明处理了事件),啥时候返回false(表明不处理事件)。数组
/** * 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不处理该事件。app
首先它把事件交给外部进行处理。外部处理指的什么呢?它指的就是交给setOnTouchListener()
设置的监听器来处理。若是这个监听器处理时返回true
,也就是OnTouchListener.onTouch()
方法返回true
,View.dispatchTouchEvent()
就返回true
,也就说明View处理了该事件。不然交给本身来处理,也就是交由onTouchEvent()
处理。ide
固然,若是要让事件监听器来处理,还必需要让View处于enabled
状态。能够经过setEnabled()
方法来改变View的enabled
状态。而且能够经过isEnabled()
查询View是否处于enabled
状态。函数
当外部没法处理时,也就是上面的三个条件有一个不知足时,就交给View.onTouchEvent()
来处理。此时View.onTouchEvent()
的返回值就决定了View.dispatchTouchEvent()
的返回值。也就是决定了View是否处理该事件。那么,咱们来看下View.onTouchEvent()
何时返回true
,何时返回false
。post
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
。this
忽略触摸代理(
Touch Delegate
)和CONTEXT_CLICKABLE
的特性,由于不经常使用,若是遇到了,能够再来查看。spa
那么,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事件处理就这么简单吗?若是你只关心事件分发到哪里,以及谁处理了事件,那么掌握上面的流程就够了。
可是你是否还有个疑问,View.onTouchEvent()
在干啥呢?OK,若是你保持这份好奇心,那么接着往下看。
View.onTouchEvent()
其实处理了三种状况
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进行重绘。
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
事件。