前言
Android开发中一些简单的UI开发其实用不到事件分发,但是要做一些特殊的功能的时候会用到触摸事件的处理,那么我们就需要了解View和ViewGoup的事件分发机制,这样才能在触摸屏幕的时候在不同的View和ViewGroup直之间切换事件处理。下面将从源码入手分析View的的事件分发机制。
View事件分发源码分析
事件的分发机制由dispatchTouchEvent进行控制,根据里面的逻辑进行判断是执行OnTouchListener还是OnClickListener;所以要了解事件的分发原理,就需要了解dispatchTouchEvent的源码。以下代码片段展示了dispatchTouchEvent中控制OnTouchListener和TouchEvent的核心代码:
/**
* 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;
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
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;
}
}
...
return result;
}
从dispathcTouchEvent的方法注释中可以看出,该方法的作用是将触摸屏运动事件传递到目标视图或者自身,针对View传递的对象就是自身,而针对ViewGroup,就可能会存在目标对象。
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
从这段代码中可以看出,如果我们设置了OnTouchListener并且返回true,那么result为true,后续的onToucheEvent就不能执行。接下来看一下onTouchEvent的源码处理流程:
/**
*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
*/
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
//获取点击坐标和事件
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
//判断该View是否可以点击
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
handleTooltipUp();
}
if (!clickable) {
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
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, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// 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)) {
performClickInternal();
}
}
}
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();
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
}
mHasPerformedLongPress = false;
if (!clickable) {
checkForLongClick(
ViewConfiguration.getLongPressTimeout(),
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
break;
}
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();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
checkForLongClick(
ViewConfiguration.getLongPressTimeout(),
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
}
break;
case MotionEvent.ACTION_CANCEL:
if (clickable) {
setPressed(false);
}
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
break;
case MotionEvent.ACTION_MOVE:
if (clickable) {
drawableHotspotChanged(x, y);
}
final int motionClassification = event.getClassification();
final boolean ambiguousGesture =
motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE;
int touchSlop = mTouchSlop;
if (ambiguousGesture && hasPendingLongPressCallback()) {
if (!pointInView(x, y, touchSlop)) {
// The default action here is to cancel long press. But instead, we
// just extend the timeout here, in case the classification
// stays ambiguous.
removeLongPressCallback();
long delay = (long) (ViewConfiguration.getLongPressTimeout()
* mAmbiguousGestureMultiplier);
// Subtract the time already spent
delay -= event.getEventTime() - event.getDownTime();
checkForLongClick(
delay,
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
}
touchSlop *= mAmbiguousGestureMultiplier;
}
// Be lenient about moving outside of buttons
if (!pointInView(x, y, touchSlop)) {
// Outside button
// Remove any future long press/tap checks
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
final boolean deepPress =
motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS;
if (deepPress && hasPendingLongPressCallback()) {
// process the long click action immediately
removeLongPressCallback();
checkForLongClick(
0 /* send immediately */,
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS);
}
break;
}
return true;
}
return false;
}
该方法的处理较复杂,我们简化操作,只分析简单流程和clickListener的执行,进入到该方法后会获取点击的坐标、view是否可用、点击事件是否可用。满足条件后会在MotionEvent.ACTION_UP中进入下面方法:
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)) {
performClickInternal();
}
}
该逻辑会在UI线程红执行PerformClick
private final class PerformClick implements Runnable {
@Override
public void run() {
recordGestureClassification(TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SINGLE_TAP);
performClickInternal();
}
}
最后再执行:
public boolean performClick() {
// We still need to call this method to handle the cases where performClick() was called
// externally, instead of through performClickInternal()
notifyAutofillManagerOnClick();
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
如果我们设置了OnClickListener,最终会在这里消费事件。
分析了相关的源码后我们可以得到下面的结论:
- 针对View的事件分发,都是由dispatchTouchEvent进行控制是否像后传递
- dispatchTouchEvent首先执行OnTouchListener,如果该方法中有对应的事件被消费,那么不会执行后续的onTouchEvent()的对应事件
- 我们平时开发设置的OnClickListener和OnLongClickListener的执行在OnTouchListener之后。
了解MotionEvent中的action
了解View事件分发的基本流程后,我们平时开发中更多的是需要根据业务场景处理Down、Move、Up、Cancel等事件,那么下面通过在OnTouchListener中通过action的DOWN、MOVE、UP、Cancel进行处理验证事件的传递流程:
//设置OnTouchListener,输出acition信息
this.waterView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d("waterView","onTouch:"+event.getAction());
int action = event.getAction();
if(action == MotionEvent.ACTION_DOWN){
Log.d("EventTag","onTouchListener:"+"ACTION_DOWN");
}else if(action == MotionEvent.ACTION_MOVE){
Log.d("EventTag","onTouchListener:"+"ACTION_MOVE");
}else if(action == MotionEvent.ACTION_UP){
Log.d("EventTag","onTouchListener:"+"ACTION_UP");
}
return false;
}
});
//重写View的onTouchEvent,输出事件的传递流程
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
if(action == MotionEvent.ACTION_DOWN){
Log.d("EventTag","onTouchEvent:"+"ACTION_DOWN");
}else if(action == MotionEvent.ACTION_MOVE){
Log.d("EventTag","onTouchEvent:"+"ACTION_MOVE");
}else if(action == MotionEvent.ACTION_UP){
Log.d("EventTag","onTouchEvent:"+"ACTION_UP");
}
return super.onTouchEvent(event);
}
1. 测试一:OnTouchListener返回false,onTouchEvent调用return super.onTouchEvent(event),不设置OnClickListener;得到以下日志信息:
17631-17631/com.water.view.demo D/EventTag: onTouchListener:ACTION_DOWN
17631-17631/com.water.view.demo D/EventTag: onTouchEvent:ACTION_DOWN
通过日志发现值输出的ACTION_DOWN其他的action并没有输出,这里主要是有ViewGroup的事件分发决定的,子View不处理Down事件,那么后续的事件将不会收到。
2. 测试二:将onTouchEvent的ACTION_DWON返回true:
18130-18130/com.water.view.demo D/EventTag: onTouchListener:ACTION_DOWN
18130-18130/com.water.view.demo D/EventTag: onTouchEvent:ACTION_DOWN
18130-18130/com.water.view.demo D/EventTag: onTouchListener:ACTION_MOVE
18130-18130/com.water.view.demo D/EventTag: onTouchEvent:ACTION_MOVE
18130-18130/com.water.view.demo D/EventTag: onTouchListener:ACTION_UP
18130-18130/com.water.view.demo D/EventTag: onTouchEvent:ACTION_UP
通过上面日志,将onTouchEvent的ACTION_DWON返回true,后面的事件能够正常接收, 所以:在开发中需要在子view中处理事件,那么一定要消费DOWN,否则后续的Action将不能接受 。
3. 测试三:将OnTouchListener的ACTION_DWON返回true:
19016-19016/com.water.view.demo D/EventTag: onTouchListener:ACTION_DOWN
19016-19016/com.water.view.demo D/EventTag: onTouchListener:ACTION_MOVE
19016-19016/com.water.view.demo D/EventTag: onTouchEvent:ACTION_MOVE
19016-19016/com.water.view.demo D/EventTag: onTouchListener:ACTION_UP
19016-19016/com.water.view.demo D/EventTag: onTouchEvent:ACTION_UP
通过上面的日志,我们可以看到View的onTouchEvent中没有收到ACTION_DOWN事件。这里的控制逻辑是由dispatchTouchEvent控制的,判断依据是result,如果OnToucheListern中对应的Action返回了true,那么该值为true,那么onTouchEvent()方法就不会执行了。
public boolean dispatchTouchEvent(MotionEvent event) {
......
//判断事件的后续分发
boolean result = false;
if (onFilterTouchEventForSecurity(event)) {
......
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;
}
}
...
return result;
}
4. 测试四:将OnTouchListener的ACTION_MOVE和ACTION_DWON返回true:
20211-20211/com.water.view.demo D/EventTag: onTouchListener:ACTION_DOWN
20211-20211/com.water.view.demo D/EventTag: onTouchListener:ACTION_MOVE
20211-20211/com.water.view.demo D/EventTag: onTouchListener:ACTION_MOVE
20211-20211/com.water.view.demo D/EventTag: onTouchListener:ACTION_UP
20211-20211/com.water.view.demo D/EventTag: onTouchEvent:ACTION_UP
从上面的输出日志中可以看出,onTouchEvent接受不到ACTION_DOWN和ACTION_UP事件,其他的事件的处理这里不再列举,流程和上面的类似。
最后
分析了源码,demo进行测试,写了这么多的文字进行说明,那么我们可以收获什么呢?
- View屏幕触摸事件由dispatchToucheEvent()进行控制,由其内部逻辑决定后续执行的方法
- OnTouchListener的处理优先级要高于onTouchEvent
- 如果在OnTouchListener中消费了ACTION_DOWN、ACTION_MOVE、ACTION_UP等事件,对应的事件则不能传递到onTouchEvent中
- 如果我们自定义的View需要处理事件,那么ACTION_DOWN一定要消费,否则不能接受到后续的其他事件
- 要对一个知识点进行熟练掌握,查看底层的实现原理,更容易掌握得精准和牢固
在实际的项目的开发中,事件的分发的复杂处理在于ViewGroup和View之间的嵌套使用,后续将进一步进行解析。
事件分发的相关文章可参考: