这篇文章我们来探究下Android中关于事件分发机制的一些细节和流程,由于这部分源码比较繁杂,拆开来讲,本文只探究View的事件分发流程,ViewGroup留到之后再说,在分析完这两者的事件分发机制之后我们来对Android的时间分发机制进行总结。那么本文就从View的子类Button来着手分析事件分发的流程,之后我们再从源码角度分析具体实现过程。

举个栗子

这里用一个很简单的小例子来演示View中dispatchTouchEvent、onTouchEvent、TouchListener的执行顺序,继承自Button的TestButton代码很简单,几个log:

@Overridepublic boolean onTouchEvent(MotionEvent event) {    switch (event.getAction()) {        case MotionEvent.ACTION_DOWN:            Log.e(TAG, "onTouchEvent ACTION_DOWN");            break;        case MotionEvent.ACTION_MOVE:            Log.e(TAG, "onTouchEvent ACTION_MOVE");            break;        case MotionEvent.ACTION_UP:            Log.e(TAG, "onTouchEvent ACTION_UP");            break;        default:            break;    }    return super.onTouchEvent(event);}@Overridepublic boolean dispatchTouchEvent(MotionEvent event) {    switch (event.getAction()) {        case MotionEvent.ACTION_DOWN:            Log.e(TAG, "dispatchTouchEvent ACTION_DOWN");            break;        case MotionEvent.ACTION_MOVE:            Log.e(TAG, "dispatchTouchEvent ACTION_MOVE");            break;        case MotionEvent.ACTION_UP:            Log.e(TAG, "dispatchTouchEvent ACTION_UP");            break;        default:            break;    }    return super.dispatchTouchEvent(event);}

Activity中设置onTouchListener,也是几个log:

testView.setOnTouchListener(new View.OnTouchListener() {    @Override    public boolean onTouch(View v, MotionEvent event) {        switch (event.getAction()) {            case MotionEvent.ACTION_DOWN:                Log.e(TAG, "onTouch ACTION_DOWN");                break;            case MotionEvent.ACTION_MOVE:                Log.e(TAG, "onTouch ACTION_MOVE");                break;            case MotionEvent.ACTION_UP:                Log.e(TAG, "onTouch ACTION_UP");                break;            default:                break;        }        return false;    }});

接下来是执行流程,第一次点击按钮:

第二次点击后鼠标移动一下松开:

可以看到一次事件分发从ACTION_DOWN开始,到ACTION_UP结束,且其传递顺序是从dispatchTouchEvent –> onTouch –> onTouchEvent。下面我们来从源码中对这几个方法进行查看。

源码分析

我们先从View的dispatchTouchEvent开始吧:

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;}

我这里略去了部分代码,来看核心代码,可以看到第10行开始这里开始进行了判断,若同时附和这几个条件,则返回true,注意这里调用了OnTouchListener的onTouch方法,也就是说如果我们调用了View的setOnTouchListener方法那么在dispatchTouchEvent方法执行过程中,会调用OnTouchListener的onTouch方法,若onTouch方法返回true,则设置result为true,onTouchEvent不再执行,若onTouch方法返回为false,则第十行if语句不成立,result为改变,执行下一个判断语句,同时会执行onTouchEvent,若返回true则result为true,反之亦然。

我们继续来看一看onTouchEvent中的代码:

public boolean onTouchEvent(MotionEvent event) {    //View状态为Disable并且可点击,返回true    if ((viewFlags & ENABLED_MASK) == DISABLED) {        if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {            setPressed(false);        }        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;        return clickable;    }    //若TouchDelegate(触摸代理类)不为空,则调用其onTouchEvent方法并返回true    if (mTouchDelegate != null) {        if (mTouchDelegate.onTouchEvent(event)) {            return true;        }    }    //若可点击或者可长按以及长按出现ToolTip    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {        switch (action) {            case MotionEvent.ACTION_UP:                ...                break;            case MotionEvent.ACTION_DOWN:                ...                break;            case MotionEvent.ACTION_CANCEL:                ...                break;            case MotionEvent.ACTION_MOVE:                ...                break;        }        return true;    }    return false;}

这段代码极长,有兴趣的朋友可以通读一下源码,我这里略去了一部分,前两个判断已经在注释里写清楚了,不多赘述,我们重要看下switch语句中的内容,接下来一个一个分析:

ACTION_DOWN

ACTION_DOWN是整个Touch流程的起点,代表触摸点按下操作。我们来看看onTouchEvent中的判断是怎样的:

case MotionEvent.ACTION_DOWN:    mHasPerformedLongPress = false;    //判断是否为鼠标右键或者手写笔第一个按钮,若是,返回true后续代码不执行    if (performButtonActionOnTouchDown(event)) {        break;    }    //当前视图是否可滚动(例如:当前是ScrollView视图,返回true)    boolean isInScrollingContainer = isInScrollingContainer();    if (isInScrollingContainer) {        // 滚动视图内,先不设置为按下状态,因为用户之后可能是滚动操作        // 不是此次分析的重点,感兴趣可以自己了解下        mPrivateFlags |= PREPRESSED;        if (mPendingCheckForTap == null) {            mPendingCheckForTap = new CheckForTap();        }        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());    } else {        // 不在滚动视图内,立即反馈为按下状态        mPrivateFlags |= PRESSED;        // 刷新为按下状态        refreshDrawableState();        //通过Handler发送一个延迟消息来判断是否是长按,500ms        checkForLongClick(0);    }break;

各个步骤的注释已经写清楚了,下面分解一下各个方法:

首先是performButtonActionOnTouchDown

protected boolean performButtonActionOnTouchDown(MotionEvent event) {    // 如果是鼠标右键,手写笔第一个按钮,看BUTTON_SECONDARY注释    if ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) {        if (showContextMenu(event.getX(), event.getY(), event.getMetaState())) {            return true;        }    }    return false;}

很简单的判断,如果成功就返回true,不成功就false。

接下来是isInScrollingContainer

public boolean isInScrollingContainer() {    ViewParent p = getParent();    while (p != null && p instanceof ViewGroup) {        if (((ViewGroup) p).shouldDelayChildPressedState()) {            return true;        }        p = p.getParent();    }    return false;}

这里获取到了当前View的父控件,而后一层一层向上判断是否处于滚动容器中(shouldDelayChildPressedState返回true),如果是,则返回true

接下来是checkForLongClick

private void checkForLongClick(int delayOffset) {    // 当前视图可以执行长按操作    if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {        mHasPerformedLongPress = false;        if (mPendingCheckForLongPress == null) {            mPendingCheckForLongPress = new CheckForLongPress();        }        mPendingCheckForLongPress.rememberWindowAttachCount();        // 延迟一段时间(500ms)把runnable添加到消息队列        postDelayed(mPendingCheckForLongPress,                    ViewConfiguration.getLongPressTimeout() - delayOffset);    }}

ACTION_DOWN中的逻辑大概就是这样

ACTION_MOVE

ACTION_MOVE代表触摸点发生滑动

case MotionEvent.ACTION_MOVE:    //这个方法暂时不用关注    if (clickable) {        drawableHotspotChanged(x, y);    }    // Be lenient about moving outside of buttons    if (!pointInView(x, y, mTouchSlop)) {        // Outside button        // Remove any future long press/tap checks        removeTapCallback();        removeLongPressCallback();        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {            setPressed(false);        }        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;    }break;

我们先来看看pointInView方法里做了什么:

public boolean pointInView(float localX, float localY, float slop) {    return localX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) &&        localY < ((mBottom - mTop) + slop);}

这个方法是判断是否划出控件可视区域,为了保证触摸点发生及轻微变化就导致ACTION_MOVE被执行,这里加入了一个slop的边界值,即在视图上下左右扩大slop

接下来是两个remove方法,这里我们放在一起讲:

private void removeTapCallback() {    if (mPendingCheckForTap != null) {        mPrivateFlags &= ~PFLAG_PREPRESSED;        removeCallbacks(mPendingCheckForTap);    }}private void removeLongPressCallback() {    if (mPendingCheckForLongPress != null) {        removeCallbacks(mPendingCheckForLongPress);    }}

这两个方法主要是删除触摸和长按回调(还记得之前按下后发送的长按延时消息吗)

ACTION_CANCLE

ACTION_CANCLE代表取消触摸操作,触摸流程结束

if (clickable) {    setPressed(false);}removeTapCallback();removeLongPressCallback();mInContextButtonPress = false;mHasPerformedLongPress = false;mIgnoreNextUpEvent = false;mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;

这里代码很简单,清除View状态

ACTION_UP

ACTION_UP代表触摸点抬起操作,同样是一个触摸流程的结束

case MotionEvent.ACTION_UP:        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;    if ((viewFlags & TOOLTIP) == TOOLTIP) {        handleTooltipUp();//popWindow之类相关的,不必关心    }    //清除触摸状态以及触摸回调和长按的回调    if (!clickable) {        removeTapCallback();        removeLongPressCallback();        mInContextButtonPress = false;        mHasPerformedLongPress = false;        mIgnoreNextUpEvent = false;        break;    }    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) {                // 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)) {            //执行失败的话,保证视图不会永远处于按下状态            //直接执行一次            mUnsetPressedState.run();        }        // 清除轻触回调        removeTapCallback();    }    mIgnoreNextUpEvent = false;break;

这短代码是触摸流程里最重要的一部分。代码中已经注释清除,不必全部理解,理解流程即可。

到此,我们整个View的触摸流程就结束了,光看这部分还是很绕的,下一篇文章我们会详细讲讲ViewGroup中事件分发流程,并且和本篇做对照,可能会理解的更透彻。

enjoy~

我的个人博客

更多相关文章

  1. AsyncTask的使用和原理探究(一)
  2. Android蓝牙播放如何显示歌曲信息?
  3. 解锁Retrofit -- 浅析Retrofit源码
  4. Android笔试和面试常见题目(一)
  5. Activity life times——Android
  6. Android使用AIDL设计和调用远程接口
  7. 针对网上流传的"Android(安卓)再按一次后退键退出应用程序"方法
  8. android smack源码分析——接收消息以及如何解析消息
  9. Android的App Widget实现

随机推荐

  1. php反射机制用法详解
  2. 通过实例详细讲解PHP垃圾回收机制
  3. php对象转数组的函数
  4. 正则表达式详细基础实例解析
  5. 详细介绍php迭代器的作用
  6. php日期格式化方法详解
  7. PHP实现文件上传下载实例详细讲解
  8. php排序函数详细讲解(附实例)
  9. PHP魔术常量、魔术函数、预定义常量详细
  10. php安全问题思考