关于Android的事件分发机制,一直想系统的整理下思路写下来。刚好最近在看github上封装的仿QQ的列表的滑动效果的项目(SwipeListView)源码,作者对嵌套的布局滑动事件进行处理来区别上下滑动和左右滑动针对不同控件的响应。
对Android的事件分发机制的理解,我们结合View和ViewGroup的事件分发一起来了解,就能比较全面了解。首先,我们来学习事件从Activity页面捕捉到,怎么一级一级往下传递。我们通过分析Activity的源码,我们在新建一个Activity的时候在重写onCreate方法的时候,我们会去调用setContentView方法给Activity加载布局。
public void setContentView(int layoutResID) {
getWindow().setContentView(layoutResID);
initActionBar();
}
通过源码我们知道,我们getWindow()方法返回的是一个PhoneWindow对象,然后我们看PhoneWindow的setContentView实现如下:
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) { // 对mContentParent官方解释:This is the view in which the window contents are placed. It is either mDecor itself, or a child of mDecor where the contents go
installDecor(); //此处开始创建根视图
} else {
mContentParent.removeAllViews();
}
mLayoutInflater.inflate(layoutResID, mContentParent);
final Callback cb = getCallback();
if (cb != null) {
cb.onContentChanged();
}
}
我们查看installDecor代码如下
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor(); //Actitity的根视图就是mDecore
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
mTitleView = (TextView)findViewById(com.android.internal.R.id.title);
if (mTitleView != null) {
if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) {
View titleContainer = findViewById(com.android.internal.R.id.title_container);
if (titleContainer != null) {
titleContainer.setVisibility(View.GONE);
} else {
mTitleView.setVisibility(View.GONE);
}
if (mContentParent instanceof FrameLayout) {
((FrameLayout)mContentParent).setForeground(null);
}
} else {
mTitleView.setText(mTitle);
}
}
}
}
然后,我们查看generateDecor方法。
protected DecorView generateDecor() {
return new DecorView(getContext(), -1);
}
我们知道DecorView的超类是FrameLayout,因此我们可以这样理解为Acitity到子控件事件的传递,实际是mDecor这个根ViewGroup到嵌套的ViewGroup和View的传递。马上我们可能又会有一个问题,mDecor的事件怎么开始分发的呢?我们知道我们事件的分发是通过dispatchTouchEvent来一级一级向下分发的。我们查看Activity的dispatchToucuEvent方法如下:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction(); //可以理解为Activity在事件分发前的需要的处理工作,该方法默认实现为空
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
我们已经知道此处的getWindow()获取的是PhoneWindow对象,我们查看PhoneWindow的superDispatchTouchEvent方法实现。
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
此处我们应该就可以说,我们在Activity的dispatchToucuEvent在分发事件的时候,实际上是调用Activity的根对象mDecor的dispatchToucuEvent方法开始分发事件。下面我们分别从ViewGroup和View来分析事件在各自内的分发机制。
一、ViewGroup的事件分发
下面我们我们查看Android 2.2的源码,至于为什么不用最新的源码,一方面个人觉得Android的事件分发机制是不变的,最新的源码和2.2版本的源代码已经发生了很大的变化,最新的代码更健壮稳定和重构的更好。如果你是通过Android源码深入理解设计的艺术,那看最新的代码还是比较好,此处我们只是学习Android的事件分发机制,2.2的源码更简洁易懂,没有太多的方法的提取重构。另外一方面,个人能力有限,不能完全解释清楚最新(如4.4)的实现,怕误导大家。我们看ViewGroup的dispatchTouchEvent方法:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
final float xf = ev.getX();
final float yf = ev.getY();
final float scrolledXFloat = xf + mScrollX;
final float scrolledYFloat = yf + mScrollY;
final Rect frame = mTempRect;
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
//判断是否禁用事件拦截的功能
if (action == MotionEvent.ACTION_DOWN) {
if (mMotionTarget != null) {
// this is weird, we got a pen down, but we thought it was
// already down!
// XXX: We should probably send an ACTION_UP to the current
// target.
mMotionTarget = null;
}
// If we're disallowing intercept or if we're allowing and we didn't
// intercept
if (disallowIntercept || !onInterceptTouchEvent(ev)) { //当事件拦截功能没有被禁用,此处的值就由onInterceptTouchEvent(ev)的实现决定,如:如果我们在子类重写onInterceptTouchEvent(ev),给它返回true,表示此事件被拦截
// reset this event's action (just to protect ourselves)
ev.setAction(MotionEvent.ACTION_DOWN);
// We know we want to dispatch the event down, find a child
// who can handle it, start with the front-most child.
final int scrolledXInt = (int) scrolledXFloat;
final int scrolledYInt = (int) scrolledYFloat;
final View[] children = mChildren;
final int count = mChildrenCount;
for (int i = count - 1; i >= 0; i--) {
final View child = children[i]; //此处开始遍历ViewGroup的子控件,分发事件
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
child.getHitRect(frame);
if (frame.contains(scrolledXInt, scrolledYInt)) {
// offset the event to the view's coordinate system
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
if (child.dispatchTouchEvent(ev)) {//当我们的子控件分发事件的时候,当返回值为true的时候,表示事件被消费表,此处我们注意当child.dispatchTouchEvent(ev)返回false的情况,表示我们的子控件没有消费掉事件,那我们的事件此次没有找到能消费自己的子控件(View或者VIewGroup)。
// Event handled, we have a target now.
mMotionTarget = child; //注意此处,当我们的事件被消费掉的时候,我们给mMotionTarget赋值child,并且我们停止了对事件的分发,直接返回true退出了方法。为什么需要把child这个控件传递给mMotionTargetchild呢,我们后面还会用到mMotionTarget。
return true;
}
// The event didn't get handled, try the next view.
// Don't reset the event's location, it's not
// necessary here.
}
}
}
}
}
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
if (isUpOrCancel) {
// Note, we've already copied the previous state to our local
// variable, so this takes effect on the next event
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// The event wasn't an ACTION_DOWN, dispatch it to our target if
// we have one.
final View target = mMotionTarget;//此处把mMotionTarget赋值给一个View引用,我们还记得上面的注释,当我们有控件消费了事件的时候,mMotionTaget是获得了一个控件的引用
if (target == null) { //此处我们会去判断,在我们的ViewGroup里有没有控件去消费我们的事件,如果没有我们接下来继续处理。
// We don't have a target, this means we're handling the
// event as a regular view.
ev.setLocation(xf, yf);
if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
}
return super.dispatchTouchEvent(ev); //注意此处的super,此处是取调用ViewGroup的超类View的dispatchTouchEvent方法,我们不要忘记ViewGroup本身也是一个View,当它发现它在给子控件分发的时候被拦截,最后,自己最为View来继续分发消费事件。
}
// if have a target, see if we're allowed to and want to intercept its
// events
if (!disallowIntercept && onInterceptTouchEvent(ev)) { //此处的代码执行已经不是处理ACTION_DOWN事件,如果此处开始执行ACTION_MOVE,先判断拦截方法onInterceptTouchEvent执行,当返回true的时候,我们会往下执行。
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
ev.setAction(MotionEvent.ACTION_CANCEL);
ev.setLocation(xc, yc);
if (!target.dispatchTouchEvent(ev)) {
// target didn't handle ACTION_CANCEL. not much we can do
// but they should have.
}
// clear the target
mMotionTarget = null;//当我们拦截了事件往下分发,我们重新把接受事件的View或者子类引用清空。
// Don't dispatch this event to our own view, because we already
// saw it when intercepting; we just want to give the following
// event to the normal onTouchEvent().
return true; //返回true,表示我们把当前事件给消费。然后,dispatchTouchEvent会继续分发其他的事件(ACTION_MOVE或者ACTION_UP)。
}
if (isUpOrCancel) {
mMotionTarget = null;
}
// finally offset the event to the target's coordinate system and
// dispatch the event.
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
ev.setLocation(xc, yc);
if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
mMotionTarget = null;
}
return target.dispatchTouchEvent(ev);
}
从上面的代码实现过程,我们可以画出ViewGroup的事件分发的简单流程图如下所示:
二、 View的事件分发
我们查看View的dispatchTouchEvent方法如下:
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) { //此处mOnTouchListener!= null 为我们给View注册的Touch事件,(mViewFlags & ENABLED_MASK) == ENABLED表示View时候可用,mOnTouchListener.onTouch(this, event) 的返回值为true的时候,表示我们不再将事件继续下发,View的事件消费完毕。
return true;
}
return onTouchEvent(event);
}
当以上的三个条件其中的任意一个不满足的时候,比如:View的onTouch返回值为false的时候,我们就要执行onTouchEvent方法。我们看onTouchEvent的实现:
public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
// 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));
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { //View可以被点击或者被长期点击
switch (event.getAction()) {
case MotionEvent.ACTION_UP: //我们从代码知道,我们把点击动作绑定的事件放在点击动作放开的瞬间来处理,我们在一次点击的时候,可能会有多次ACTION_DOWN。
boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
if ((mPrivateFlags & 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 (!mHasPerformedLongPress) {
// 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实现Runnable,run方法也是执行performClick方法,只是没明白这里的双保险为保证什么?
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
mPrivateFlags |= PRESSED;
refreshDrawableState();
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
break;
case MotionEvent.ACTION_DOWN:
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPrivateFlags |= PREPRESSED;
mHasPerformedLongPress = false;
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
break;
case MotionEvent.ACTION_CANCEL:
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
removeTapCallback();
break;
case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();
// Be lenient about moving outside of buttons
int slop = mTouchSlop;
if ((x < 0 - slop) || (x >= getWidth() + slop) ||
(y < 0 - slop) || (y >= getHeight() + slop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
// Need to switch from pressed to not pressed
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
}
}
break;
}
return true;
}
return false;
}
分析onTouchEvent方法,我们可以看出关键的代码就在performClick()。我们查看实现:
public boolean performClick() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
if (mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
}
return false;
}
从源代码我们知道,performClick方法就是回调View注册的点击方法。我们也可以画出View的事件分发过程的流程图如下:
以上为我对Android事件的分发机制的一点整理,后面有机会还会结合例子加深对事件分发机制的讲解,希望对还不完全清楚的朋友能有点帮助。
三、事件的分发、拦截、消费的返回值
通过源代码我们知道,Android的事件分发是从Activit顶层的DecorView根容器开始往控件树下分发,事件的处理是从最底层的叶子控件开始尝试消费事件,如果最底层叶子控件没有消费掉事件,我们的事件就会回溯到叶子控件的父控件来消费。我们判断一个控件有没有消费掉事件,我们通过分发方法dispatchTouchEvent的返回值来判断,如果返回true表示事件被消费,如果没有表示事件没有消费,会继续回溯。事件传递过程如下图所示:
通过分析事件分发的机制,在我们的事件分发过程都有哪些方法可以消费事件,首先View的mOnTouchListener的回调方法onTouch、onTouchEvent都可以返回true,表示事件被消耗掉。我们注意View的onTouchEvent方法的一段源码如下:
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
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);
}
if (!mHasPerformedLongPress) {
// 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();
}
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();
}
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true);
checkForLongClick(0);
}
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
break;
case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();
// 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;
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) 的意思是当View是可以点击或者长按的话,最后肯定是返回true,意思是当我们的View是可以点击或者长按的话,我们的事件一定会在当前控件被消费掉。
三、事件的分发“记忆”功能
我们可以通过自己的Demo来试验,我们知道我们的事件ACTION_DOWN,ACTIN_MOVE和ACTION_UP通常会有大致相同的事件分发消费回路。当我们的事件发生过拦截操作后,从该拦截后的事件分发回路默认相同。比如,我们一次ACTION_DOWN事件从Activity开始分发,我们在中间层的ViewGroup拦截了ACTIION_DOWN事件,后续的ACTION_MOVE和ACTION_UP的事件分发都有相同的回路。
注意,当我们的事件在一次事件从Activity分发下去,到Acitivity的dispatchTouchEvent返回false,表示该次事件没有得到分发,我们下一个事件分发,我们的事件不需要在向下层分发,只会在我们的Activity分发(dispatchTouchEvent)和消费(onTouchEvent)。
转载请注明出处:http://blog.csdn.net/johnnyz1234/article/details/43244129 Johnnie Zhang