我们都知道自定义 View 的操作步骤是 measure,layout,draw,不过除此之外对自定义控件的响应事件也是非常重要的,即对 touchEvent 的响应,在进行自定义 View 时我们通常需要重写onMeasure(),onLayout(),onTouchEvent() 等方法,当然了,我们都知道自定义最难的地方在于 draw(即画)的过程,需要理解原理才有助于深入学习,有些难以理解,不过今天这一篇文章要说的不是 draw,而是 onTouchEvent( )方法。我们都知道,自定义 View 的第一步是测量当前剩余空间,或者说是界面的大小,也就是 measure 了;然后是 layout,即判断自定义 view 在父控件上显示的位置,这两点在上一篇通过讲解过了,所以今天我们要说的就是对 TouchEvent 的处理。
我们假设,一个 View 接收到了 Touch 事件,它会把这个事件传递给谁呢?
答案是 dispatchTouchEvent() 方法,所以我们就从这个方法入手
老规矩了,看一下 Google 官方文档对 dispatchTouchEvent() 这个方法的描述吧:
Pass the touch screen motion event down to the target view, or this view if it is the target.
将触摸屏运动事件向下传递到目标视图,如果它是目标,则视图。
嗯,这个方法是用于将触摸事件传递到目标并视图的,那么现在一起来看一下它的源码吧:
public boolean dispatchTouchEvent(MotionEvent event) {
if (event.isTargetAccessibilityFocus()) {
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
event.setTargetAccessibilityFocus(false);
}
boolean result = false;
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
stopNestedScroll();
}
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;
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
方法接手一个 MotionEvent 对象,这个 MotionEvent 就是触摸事件了,有很多常量可供选择,这里介绍几个常用的常量,需要注意常量是整形的:
MotionEvent.ACTION_DOWN : 表示按下的手势开始时,该运动包含初始起始位置。
MotionEvent.ACTION_MOVE : 表示按下手势开始时和结束时中间所发生的改变
MotionEvent.ACTION_UP : 表示按下的手势已经完成,运动包含最后释放位置以及自上次向下或移动事件以来的任何中间点。
我们可以看到这个方法的返回值是 boolean 类型的,那么它的返回值又是什么意思呢?
其实它的返回值表示的是 Touch 事件是否被消费。
好了,接下来我们分析一下源码:
源码采用了两种方式来处理 Touch 事件,一是调用 onTouchListener 中的 onTouch() 方法处理 Touch 事件,二是调用 View 自身的 onTouchEvent() 方法处理 Touch 事件。
来看一下Google 官方文档对 TouchListener.onTouch() 这个方法的描述吧:
Called when a touch event is dispatched to a view. This allows listeners to get a chance to respond before the target view.
将触摸事件分派到视图时调用,允许监听器有机会在目标视图之前响应。
调用 onTouchListener 中的 onTouch( )方法处理 Touch 事件
if (li != null && li.mOnTouchListener != null &&
(mViewFlags&ENABLED_MASK)==ENABLED && li.mOnTouchListener.onTouch(this,event))
此时必须同时满足四个条件才能证明 Touch 事件被消费
ListenerInfo 是 View 中的一个静态类,包含了几个 Listener,比如 TouchListener,FocusChangeListener,LayoutChangeListeners,ScrollChangeListener 等等。一般情况下它均不为 null,所以我们不用过多关注它。
mOnTouchListener 是由 View 设置的,比如 mButton.setOnTouchListener()。所以如果 View 设置了 Touch 监听那么,那么 mOnTouchListener 不空;反之,mOnTouchListener 为null
当前 View 可用(ENABLED)。通常可调用 view.setEnabled( ) 设置 View 是否可用
这一点其实是在 li.mOnTouchListener != null 的基础上继续判断。判断 TouchListener的onTouch( ) 方法是否消耗了 Touch 事件。返回值为 true 表示消费掉该事件,false 表示未消费。
先一起看一下 Google 官方文档对 android.view.View.onTouchEvent() 这个方法的描述吧:
Implement this method to handle touch screen motion events.
通过实现此方法来处理触摸屏运动事件。
这个描述就已经概括了这个方法的作用,用于处理用户对手机屏幕的触摸运动事件。
调用 View 自身的 onTouchEvent() 方法处理 Touch 事件
if (!result && onTouchEvent(event)) {
result = true;
}
如果在上一步中 Touch 事件被消费 result 为 true,就不会执行这三行代码。该处调用了 onTouchEvent() 若该方法返回值false那么 dispatchTouchEvent() 的返回值也为 false;反之,若该方法返回值为 true,那么 dispatchTouchEvent() 的返回值亦为 true。
源码如下:
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
setPressed(true, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
removeLongPressCallback();
if (!focusTaken) {
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)) {
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
if (performButtonActionOnTouchDown(event)) {
break;
}
boolean isInScrollingContainer = isInScrollingContainer();
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
setPressed(true, x, y);
checkForLongClick(0);
}
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);
if (!pointInView(x, y, mTouchSlop)) {
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
removeLongPressCallback();
setPressed(false);
}
}
break;
}
return true;
}
return false;
}
源码复杂一点,我们一起来看一下:
若一个 View 是 disable 的,如果它是 CLICKABLE 或者 LONG_CLICKABLE 或 CONTEXT_CLICKABLE 的就返回 true,表示消耗掉了 Touch 事件。
但是该 view 所对应的 ClickListener.onClick( ) 不会有任何的响应。官方文档的描述:
A disabled view that is clickable still consumes the touch events, it just doesn’t respond to them.
若 View 虽然是 disable 的,但只要满足这三个条件中的一个,它就会消费掉 Touch 事件,但不再回调 view 的 onClick( ) 方法
可以看到在处理 MotionEvent.ACTION_U P时调用了 performClick() 方法
public boolean performClick() {
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);
return result;
}
然后在该方法中调用了 view 的 onClick( ) 方法,可见常见的 Click 事件是在 View 的 onTouchEvent( ) 中处理ACTION_UP 时调用的。
如果 View 是 enable 的,只要该 View 满足 CLICKABLE 和 LONG_CLICKABLE 以及 CONTEXT_CLICKABLE 这三者的任意一个,不管当前的 action 是什么,该 onTouchEvent() 返回的均是 true;而且会在 ACTION_UP 时处理 click 事件。
同理,如果这三个条件都不满足,该 onTouchEvent() 返回的是 false。
View 的 clickable 属性视不同的子 View 有所差异
比如:Button 的 clickable 默认为 true,但是 TextView 的 clickable 属性默认为 false。
View 的 longClickable 属性默认为 false。
当然,我们可以通过代码修改这些默认的属性。
比如:setClickable() 和 setLongClickListener() 可以改变 View 的 CLICKABLE 和 LONG_CLICKABLE 属性。
除此以外,通过设置监听器也可改变某些属性。
例如:setOnClickListener() 会将 View 的 CLICKABLE 设置为 true;setOnLongClickListener()会将 View 的 LONG_CLICKABLE 设置为 true。
Vie w处理 Touch 事件的总体流程
dispatchTouchEvent()—>onTouch()—>onTouchEvent()—>onClick()
Touch 事件最先传入 dispatchTouchEvent() 中;如果该 View 存在 TouchListener 那么会调用该监听器中的 onTouch()。在此之后如果 Touch 事件未被消费,则会执行到 View 的 onTouchEvent() 方法,在该方法中处理 ACTION_UP 事件时若该 View 存在 ClickListener 则会调用该监听器中的 onClick()
onTouch() 与 onTouchEvent()以及click三者的区别和联系 :
onTouch() 与 onTouchEvent() 都是处理触摸事件的 API
onTouch() 属于 onTouchListener 接口中的方法,是 View 暴露给用户的接口便于处理触摸事件,而 onTouchEvent() 是 Android 系统自身对于 Touch 处理的实现
先调用 onTouch() 后调用 onTouchEvent()。而且只有当 onTouch() 未消费 Touch 事件才有可能调用到onTouchEvent()。即 onTouch() 的优先级比 onTouchEvent() 的优先级更高。
在 onTouchEvent() 中处理 ACTION_UP 时会利用 ClickListene r执行 Click 事件,所以 Touch 的处理是优先于 Click 的
三者执行顺序为:onTouch()–>onTouchEvent()–>onClick()
View 没有事件的拦截(onInterceptTouchEvent( )),ViewGroup 才有