前言
在android开发中经常会碰到各种View,ViewGroup嵌套等问题。在这种嵌套问题中经常就会碰到滑动,点击等的冲突问题,要想正确的处理这种问题,对android的事件传递机制的理解尤其重要 。平时看书自己测试各种方法都是一知半解,感觉最好的方式还是打开源码看看内部的实现方能理清这些思路。于是参考书籍和郭神的博客,开启了android事件传递源码的分析。
分析思路
其实最主要的,自己对自己准备理清的内容要有一定的思路,也就是看源码要有针对性,因为android的源码太多了,不适合一点一点的精读,所以就要先理清思路,顺藤摸瓜地去读源码,才能搞清楚自己想理清的内容。我们分析android的事件传递机制,首先就要理清,从哪开始分析。我们主要分析的是View和ViewGroup的事件传递,ViewGroup又是View的子类,而且View只是一个控件,比ViewGroup简单,所以我们先理清View的事件传递机制,其实整块80%的内容就解决了。
View的事件传递机制
我们先做一个简单的例子,一个activity,一个可点击的button
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.i(TAG, "onClick: 按钮被点击了--");
}
});
我们通常都是这样子添加监听的方式,按钮点击里面的内容就会被执行。那么我们再给它添加上onTouch方法看一看效果
btn.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d(TAG, "onTouch: " + event.getAction());
return false;
}
});
控制台log结果如下:
可以看到onTouch方法被调用了两次,onClick被调用了一次,并且是先执行onTouch再执行onClick的。我们这里onTouch方法的返回值是false,我们改为true再试一下,结果如下:
可以看到,onTouch仍然是被执行了两次,但是没有执行onClick了,这是为什么呢?我们先理清楚一些内容。
MotionEvent:触摸事件的抽象,事件类型主要有如下几种:
1. ACTION_DOWN:
用户手指按下操作。
2. ACTION_MOVE:
用户手指按压屏幕之后,在松开之前,如果移动的距离超过一定的阀值,就会被判定为ACTION_MOVE操作,这个阀值很小,所以一般手指在屏幕上轻微地滑动都会触发一系列的移动事件。
3. ACTION_UP:
当用户手指离开屏幕的操作。public boolean dispatchTouchEvent(MotionEvent ev)
这是所有事件分发的总线,每次的MotionEvent操作都会调用到这个方法,而其它诸如onTouch,onTouchEvent等方法的调用等逻辑都在这里面。比如一次按钮的点击操作:
按下会触发一次,移动也会触发,抬起也会触发一次,这就是为什么上面我们点击一次按钮onTouch被调用了两次的原因。
所以我们就从这个方法的源码入手,来分析事件传递机制。
View的dispatchTouchEvent源码
public boolean dispatchTouchEvent(MotionEvent event) {
// If the event should be handled by accessibility focus first.
if (event.isTargetAccessibilityFocus()) {
// We don't have focus or no virtual descendant has it, do not handle the event.
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
// We have focus and got the event, then use normal event dispatch.
event.setTargetAccessibilityFocus(false);
}
boolean result = false;
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
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;
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
// Clean up after nested scrolls if this is the end of a gesture;
// also cancel it if we tried an ACTION_DOWN but we didn't want the rest
// of the gesture.
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
可以看到,view的dispatchTouchEvent源码不是很多,我们找到关键代码:
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;
}
我们可以看到,当4个条件完全为真的时候,result=true,那么前两个条件在哪赋值的呢:我们顺着源码找到 :
public void setOnTouchListener(OnTouchListener l) {
getListenerInfo().mOnTouchListener = l;
}
ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}
也就是在setOntouchListener的时候,前两个变量都被赋值了,而mViewFlags & ENABLED_MASK判断当前控件是否是enable的,按钮默认是的,所以为true,所以,内部的赋值操作是否会执行就取决于onTouch的返回值了。可以看到我们上面的例子里,返回false的时候,会进入下面的if判断:
if (!result && onTouchEvent(event)) {
result = true;
}
因为result初始化为false,如果没进入上面的语句块赋值则不会更改,所以如果onTouch返回为true,判断条件不成立,则onTouchEvent就肯定不会执行的。我们还记的上面的测试,在onTouch方法里,如果返回false,则onTouch和onClick都会执行,如果返回true,那么onClick就不会执行,看到这里我们肯定会猜想onClick就是在OnTouchEvent方法内部执行的,既然有了这个猜想我们就再进去OnTouchEvent方法里看看内部如何实现调用关系的。
View的OnTouchEvent方法源码
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);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
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) {
// 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)) {
performClick();
}
}
}
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:
mHasPerformedLongPress = false;
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(0, x, y);
}
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
setPressed(false);
}
}
break;
}
return true;
}
return false;
}
这个方法里代码可不少,但是没关系,我们仍然是抓住关键点来看:
从这个判断语句可以知道,如果控件可以点击,那么会进入下面的switch语句。
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
然后在ACTION_UP中,往下看
// 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)) {
performClick();
}
}
也就是当我们处于按下状态的时候,也就是手指按下以后还在接触屏幕,进入大的语句块,在没有执行该click的时候,会调用performClick方法,我们在进入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;
}
其实看到这里,要求li != null && li.mOnClickListener != null,那么这两个变量在哪里赋值的呢:
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}
看到了吧,跟onTouch类似,这边是在设置监听的时候,为这两个变量赋值的,那么这两个变量都不为空的情况下,就会调用ClickListener的onClick方法,也就是我们自己实现的监听器逻辑。
到这里,验证了我们的猜想,也就是先调用onTouch,根据其返回值会直接决定onTouchEvent方法的执行与否,从而决定onClick方法是否会被执行。
注意,还有一点关键的地方:就是dispatchTouch的返回值会决定一些内容,我们做一个实验:声明一个自定义的layout
package pg.com.mylibrary;
import android.content.Context;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.LinearLayout;
import static pg.com.mylibrary.MyActivity.TAG;
/**
* 作者:潘浩
* 学校:南华大学
* 时间:17-7-31
*/
public class MyLayout extends LinearLayout {
public MyLayout(Context context) {
super(context);
}
public MyLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public MyLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "dispatchTouchEvent: down");
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, "dispatchTouchEvent: move");
break;
case MotionEvent.ACTION_UP:
Log.d(TAG, "dispatchTouchEvent: up");
break;
default:
break;
}
return true;
}
}
重写了它的dispatchTouchEvent方法,并且返回值没有super。当我们返回true时,我们测试按下,拖动和抬起操作,控制台打印结果如下:
然后我们把返回值修改为false,再执行上述操作,控制台打印结果如下:
注意,这里我是连续执行了按下,移动和抬起操作两次,而每次只会打印down事件,也就是说,当该方法返回true时,下次的除了down其它的事件都不会被接受了。这里和dispatchTouchEvent的源码无关,因为没有super默认也是这么执行的,这是touch事件层级传递的机制,具体肯定是更高层的代码调用了dispatchTouchEvent方法,在哪里调用的?我还没找到~~
也就是我们如果不重写dispatchTouchEvent方法的情况下,能影响该方法返回值的将在onTouch和onTouchEvent方法里,我们看到,其实不管down还是up,如果控件是可点击的,直接会返回true,否则没进入语句块直接在外层返回false,如果我们这里测试的是imageView,那么这里的onTouch方法也就只会执行一次,因为它默认是不可点击的,所以如果进入onTouchEvent的话,会直接进入不了语句块返回false,那么后续的除了down的操作也就都接受不到了。
结语
其实刚开始读源码的时候,感觉特别吃力,一个个变量一个个参数的,一个类上千上万行,但是慢慢地发现,只要理清思路,抓住主要的东西,看源码的时候带着猜测去找结果,其实也没那么复杂。后续还会有一篇ViewGroup事件传递的源码分析。今天就先睡了,溜了溜了。