我的个人网站
Google Blog
Android自定义View学习
Android自定义View之onMeasure()源码分析
如果觉得我的文章还行的话,也可以关注我的公众号,里面也会第一时间更新,并且会有更多的关于技术的最新资讯和一些个人感想。
扫码关注
我们都知道自定义View的操作步骤是measure,layout,draw,不过除此之外对自定义控件的响应事件也是非常重要的,即对touchEvent的响应,在进行自定义View时我们通常需要重写onMeasure(),onLayout(),onTouchEvent()等方法,当然了,我们都知道自定义最难的地方在于draw(即画)的过程,需要理解原理才有助于深入学习,有些难以理解,不过今天这一篇文章要说的不是draw,而是onTouchEvent()方法。我们都知道,自定义View的第一步是测量当前剩余空间,或者说是界面的大小,也就是measure了;然后是layout,即判断自定义view在父控件上显示的位置,这两点在上一篇通过讲解过了,所以今天我们要说的就是对TouchEvent的处理。
我们假设,一个View接收到了Touch事件,它会把这个事件传递给谁呢?
答案是dispatchTouchEvent()方法,所以我们就从这个方法入手
android.view.View.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事件。
onTouchListener.onTouch()
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事件被消费
- li != null
ListenerInfo是View中的一个静态类,包含了几个Listener,比如TouchListener,FocusChangeListener,LayoutChangeListeners,ScrollChangeListener等等。一般情况下它均不为null,所以我们不用过多关注它。
- li.mOnTouchListener != null
mOnTouchListener是由View设置的,比如mButton.setOnTouchListener()。所以如果View设置了Touch监听那么,那么mOnTouchListener不空;反之,mOnTouchListener为null
- (mViewFlags & ENABLED_MASK) == ENABLED
当前View可用(ENABLED)。通常可调用view.setEnabled( )设置View是否可用
- li.mOnTouchListener.onTouch(this, event)
这一点其实是在li.mOnTouchListener != null的基础上继续判断。判断TouchListener的onTouch( )方法是否消耗了Touch事件。返回值为true表示消费掉该事件,false表示未消费。
android.view.View.onTouchEvent()
先一起看一下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时对Touch进行处理
若一个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( )方法
- 处理ACTION_DOWN,ACTION_MOVE,ACTION_UP事件
可以看到在处理MotionEvent.ACTION_UP时调用了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时调用的。
- 返回onTouchEvent()方法的输出结果
如果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。
返回Touch事件是否被消费
View处理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时会利用ClickListener执行Click事件,所以Touch的处理是优先于Click的
三者执行顺序为:onTouch()–>onTouchEvent()–>onClick()
View没有事件的拦截(onInterceptTouchEvent( )),ViewGroup才有