android-----事件分发机制测试系列

Posted 她说巷尾的樱花开了

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了android-----事件分发机制测试系列相关的知识,希望对你有一定的参考价值。

        上一篇我们主要主要是从ViewGroup分发的角度测试了下事件分发机制,但没有涉足多少View的事件分发,也就是说我们没有为MyRelativeLayout、MyLinearLayout、以及MyButton设置Touch和Click监听事件,这一篇将来测试下View的事件分发过程,为了比较简洁的显示打印信息,我简化了布局文件,具体的布局文件代码如下:

<com.hzw.eventtest.MyRelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/myRelativeLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <com.hzw.eventtest.MyButton
        android:id="@+id/myButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="我的按钮" />
</com.hzw.eventtest.MyRelativeLayout>
        也即布局文件图是酱紫的:

                                                                     

        具体的测试代码就是在MainActivity和MyButton中的dispatchTouchEvent以及onTouchEvent方法中打印Log,以及在MyRelativeLayout的dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent中打印Log,并且为MyRelativeLayout、MyButton设置了onTouchListener、onLongClickListener、以及onClickListener事件监听器;

        点击下载测试代码!!!!!

        我们点击MyButton按钮,查看Logcat输出结果如下:

06-30 10:27:37.127: I/System.out(2705): MainActivity--->dispatchTouchEvent--->ACTION_DOWN
06-30 10:27:37.127: I/System.out(2705): MyRelativeLayout--->dispatchTouchEvent--->ACTION_DOWN
06-30 10:27:37.132: I/System.out(2705): MyRelativeLayout--->onInterceptTouchEvent--->ACTION_DOWN
06-30 10:27:37.132: I/System.out(2705): MyRelativeLayout--->onInterceptTouchEvent--->ACTION_DOWN--->false
06-30 10:27:37.132: I/System.out(2705): MyButton--->dispatchTouchEvent--->ACTION_DOWN
06-30 10:27:37.132: I/System.out(2705): MyButton--->onTouch--->DOWN
06-30 10:27:37.132: I/System.out(2705): MyButton--->onTouchEvent--->ACTION_DOWN
06-30 10:27:37.144: I/System.out(2705): MyButton--->onTouchEvent--->ACTION_DOWN--->true
06-30 10:27:37.144: I/System.out(2705): MyButton--->dispatchTouchEvent--->ACTION_DOWN--->true
06-30 10:27:37.144: I/System.out(2705): MyRelativeLayout--->dispatchTouchEvent--->ACTION_DOWN--->true
06-30 10:27:37.144: I/System.out(2705): MainActivity--->dispatchTouchEvent--->ACTION_DOWN--->true
06-30 10:27:37.689: I/System.out(2705): MyButton--->onLongClick
06-30 10:27:37.832: I/System.out(2705): MainActivity--->dispatchTouchEvent--->ACTION_UP
06-30 10:27:37.832: I/System.out(2705): MyRelativeLayout--->dispatchTouchEvent--->ACTION_UP
06-30 10:27:37.832: I/System.out(2705): MyRelativeLayout--->onInterceptTouchEvent--->ACTION_UP
06-30 10:27:37.832: I/System.out(2705): MyRelativeLayout--->onInterceptTouchEvent--->ACTION_UP--->false
06-30 10:27:37.832: I/System.out(2705): MyButton--->dispatchTouchEvent--->ACTION_UP
06-30 10:27:37.832: I/System.out(2705): MyButton--->onTouch--->UP
06-30 10:27:37.832: I/System.out(2705): MyButton--->onTouchEvent--->ACTION_UP
06-30 10:27:37.842: I/System.out(2705): MyButton--->onTouchEvent--->ACTION_UP--->true
06-30 10:27:37.842: I/System.out(2705): MyButton--->dispatchTouchEvent--->ACTION_UP--->true
06-30 10:27:37.842: I/System.out(2705): MyRelativeLayout--->dispatchTouchEvent--->ACTION_UP--->true
06-30 10:27:37.842: I/System.out(2705): MainActivity--->dispatchTouchEvent--->ACTION_UP--->true
06-30 10:27:37.852: I/System.out(2705): MyButton--->OnClick

        如果你仔细查看输出的话,有一部分会让你觉得很奇怪的,就是输出的第12行的onLongClick方法和第24行的onClick方法是在dispatchTouchEvent方法执行结束之后才开始执行的,这一点让我感到很诧异,所以专门写了这篇博客来试着从代码层面解释下这种现象的原因,因为网上看别人的分析过程均没有涉足到我想要的部分,所以打算自己分析一次View分发过程的源码,有什么错误还请指正,源码分析结束之后我们再来看看Logcat输出或许你会明白点了;

        一个事件传递到View上面首先执行的就是他的dispatchTouchEvent方法,那么很自然首先应该从View的dispatchTouchEvent开始分析:

 public boolean dispatchTouchEvent(MotionEvent event) {
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

        if (onFilterTouchEventForSecurity(event)) {
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                return true;
            }

            if (onTouchEvent(event)) {
                return true;
            }
        }

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }
        return false;
    }

        第6行判断是否过滤掉当前事件系列,什么情况下会被过滤呢?View在被遮盖的时候,onFilterTouchEventForSecurity方法会返回true,进而直接在第22行dispatchTouchEvent返回了false;如果当前View没有被遮盖的话,执行7--16行的if语句块,首先获取到ListenerInfo对象,他是View的静态内部类,这个对象主要存储的就是一些我们所设置的事件监听器了,稍微看看里面的几个属性字段:

static class ListenerInfo {
    public OnClickListener mOnClickListener;
    protected OnLongClickListener mOnLongClickListener;
    private OnTouchListener mOnTouchListener;
}
        接着走到第9行的if判断语句处,这个地方有四个判断条件,第1个li指的就是 ListenerInfo对象,第2个 li.mOnTouchListener其实是在判断是否设置Touch事件监听器,具体 li.mOnTouchListener的值等于什么呢?从下面代码中可以看出来:

  public void setOnTouchListener(OnTouchListener l) {
        getListenerInfo().mOnTouchListener = l;
    }
这是一个public类型的方法,我们通常在程序中为某个控件设置Touch监听器就是调用的这个方法,那么其实第2个判断条件是在查看我们是否有设置Touch事件监听器,第三个条件是在查看我们当前的View是否是enable的,也就是说当前View本身是否能够接受触摸事件,第4个就是onTouch方法的返回值了,这个方法可以被重写,默认情况下是返回false的;如果这个if判断的四个条件都满足的话,执行11行,直接返回,也就是当前事件已经分发结束了,从这里可以看出View事件分发首先执行的是onTouch(当然你必须设置Touch事件监听器);如果if的四个条件中有一个是false,就会执行第14行的if语句,调用onTouchEvent来处理事件,这里我们有必要来看看 onTouchEvent方法了;

        该方法是public修饰的,所以你可以在子类中重写它,方法比较长,我们截段分析:

if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (event.getAction() == 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));
        }

        首先判断当前View如果本身就不支持触摸的话,进入if语句块,第2行判断当前事件是UP并且设置了PFLAG_PRESSED标志的话,则调用setPressed将标志置位,因为整个事件的最后一步就是UP了,所以我们必须在事件结束之前将设置的标志还原;

public void setPressed(boolean pressed) {
        final boolean needsRefresh = pressed != ((mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED);

        if (pressed) {
            mPrivateFlags |= PFLAG_PRESSED;
        } else {
            mPrivateFlags &= ~PFLAG_PRESSED;
        }

        if (needsRefresh) {
            refreshDrawableState();
        }
        dispatchSetPressed(pressed);
    }
这里传给setPressed的参数是false,所以执行7行代码取反还原;回到onTouchEvent方法中,第7行查看View是否有设置点击和长点击,有的话返回true,没有返回false,从这里可以看出onTouchEvent的返回值是跟你View是enable还是disable没有多大关系,只要你设置了clickable或者longClickable,那么他就会返回true;

        接着分析onTouchEvent下面代码:

  if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
        查看是否有设置事件代理,有的话,则将事件交给代理处理,根据代理事件onTouchEvent方法来判断是否返回true;

        接下来的onTouchEvent代码比较长,我们先来整理一个大体框架:

 if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
           ...............
           ...............
           ...............
        return true;
 }
 return false;
        可以看到只要clickable和longClickable有一个被设置就会返回true,只有在两者都没设置的情况下才会返回false,这也更加印证了onTouchEvent方法的返回值只和你有没有设置clickable和longClickable有关,和View的enable和disable没什么关系;

        如果clickable和longClickable有一个被设置,那么进入if语句块中,该语句块是一个switch语句,我们按照事件的触发顺序来进行分析,即DOWN--->MOVE--->UP:

        先来看DOWN部分:

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;
        刚进来首先设置 mHasPerformedLongPress的值为false,这个比较关键了,用来表示是否有执行长点击事件,如果有设置长点击事件并且onLongClick方法返回true的话,这个值是会被改变成true的,等会你就看到什么原因啦,接着第9行判断当前View是否在正在滚动的控件中,在的话就满足第13行的if条件语句调用postDelay方法来延期press的反馈,为什么要这么做呢?从第11行的注释看出来是为了防止当前事件是一个滚动事件,进入if语句块之后执行第14行,设置PREPRESSED标志,这个标志表示的是prepressed状态,这个状态存在于ACTION_DOWN和真正意识到是press之间,用于识别是不是tap事件,接着第18行执行了postDelayed方法,参数ViewConfiguration.getTapTimeout()的值是150ms,这个方法的代码如下:

public boolean postDelayed(Runnable action, long delayMillis) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            return attachInfo.mHandler.postDelayed(action, delayMillis);
        }
        // Assume that post will succeed later
        ViewRootImpl.getRunQueue().postDelayed(action, delayMillis);
        return true;
    }
        可以看到这个方法其实是调用了handler的postDelayed方法,将action加入到了MessageQueue消息队列中,熟悉handler机制的应该知道随后调用的将是action的run方法了,也就是 mPendingCheckForTap的run方法了, mPendingCheckForTap是CheckForTap类型的对象,具体定义如下:

 private final class CheckForTap implements Runnable {
        public void run() {
            mPrivateFlags &= ~PFLAG_PREPRESSED;
            setPressed(true);
            checkForLongClick(ViewConfiguration.getTapTimeout());
        }
    }
        run方法首先是将PREPRESSED标志置位,接着执行setPressed方法,设置PRESSED标志,这个方法在前面又出现过,只不过前面调用的是setPressed(false)而已;接着便调用 checkForLongClick来查看是否有长点击事件了,传入的参数是150ms;如果当前View不在滚动的控件中的话,则直接执行第19行的else语句,接着调用setPressed以及 checkForLongClick方法,这里执行的内容就和CheckForTap的run方法一致了,只不过传入的 checkForLongClick参数值不同而已,那么我们就该看看checkForLongClick方法了:

    private void checkForLongClick(int delayOffset) {
        if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
            mHasPerformedLongPress = false;

            if (mPendingCheckForLongPress == null) {
                mPendingCheckForLongPress = new CheckForLongPress();
            }
            mPendingCheckForLongPress.rememberWindowAttachCount();
            postDelayed(mPendingCheckForLongPress,
                    ViewConfiguration.getLongPressTimeout() - delayOffset);
        }
    }
        该方法第2行判断你有没有设置longClick事件,有的话进入if语句块,首先还是将mHasPerformedLongPress设置为false,接着第9行同样调用了postDelayed方法,传入的第二个参数是 ViewConfiguration.getLongPressTimeout() - delayOffset, ViewConfiguration.getLongPressTimeout()的默认值是500ms,从这句话我们可以看出来不管你是通过 checkForLongClick(0)还是 checkForLongClick( delayOffset)其中 delayOffset大于0,调用 checkForLongClick方法,其实检测你是不是长点击的时间是一致的,都是500ms,你点击的时间超过500ms的话,会认为是长点击,很自然调用的是mPendingCheckForLongPress的run方法:

       public void run() {
            if (isPressed() && (mParent != null)
                    && mOriginalWindowAttachCount == mWindowAttachCount) {
                if (performLongClick()) {
                    mHasPerformedLongPress = true;
                }
            }
        }
        注意到这个方法第2行会判断isPressed(),什么意思呢?就是说如果你500ms之后还是处于点击状态,那么你就是长点击了,执行if语句块中的内容,第4行执行的是 performLongClick方法,而这个方法就主要是执行的我们的OnLongClickListener监听方法了,来看看里面的代码:

public boolean performLongClick() {
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);

        boolean handled = false;
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnLongClickListener != null) {
            handled = li.mOnLongClickListener.onLongClick(View.this);
        }
        if (!handled) {
            handled = showContextMenu();
        }
        if (handled) {
            performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
        }
        return handled;
    }
        同样OnLongClickListener存储在ListenerInfo对象里面,第7行执行了onLongClick方法,并且获取到返回值,回到前面的run方法第4行会判断这个返回值是true的话,执行 mHasPerformedLongPress = true语句,我们有必要说明下mHasPerformedLongPress
的作用,他是用于表示你的onLongClick是否返回true的,如果没有设置LongClick监听事件或者设置了LongClick监听事件但是onLongClick方法返回false,那么mHasPerformedLongPress的值将是false,随后在UP事件判断中才会执行接下来的click点击事件,如果设置了LongClick监听事件并且onLongClick方法返回true,那么在随后的UP事件判断中将不再会执行click点击事件,从这里我们可以看出其实onLongClick方法是优先于onClick执行的,这也就解释了我们平常使用onLongClick方法有返回值而onClick方法没有返回值的问题了;这样的话DOWN事件处理结束了;

        从DOWN事件的处理中,我们可以知道longclick是在它里面进行检测的,并且如果500ms之后还处于press状态的话会调用它的performLongClick,而这个方法是在子线程中调用的,所以就出现了我们上面Log输出第12行在dispatchTouchEvent返回之后才执行的结果;

        接下来分析的是MOVE事件:

        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;
        MOVE相对来说比较简单,首先是获取你当前触摸处的位置,接着第6行判断你触摸的地方是否处于当前View的边界内,不处于的话会执行7--15行代码,处于的话不做任何事,我们来看看不处于情况下做了些什么,首先执行第8行的 removeTapCallback方法,这个方法:

private void removeTapCallback() {
        if (mPendingCheckForTap != null) {
            mPrivateFlags &= ~PFLAG_PREPRESSED;
            removeCallbacks(mPendingCheckForTap);
        }
    }
主要是置位PREPRESSED标志,并且从当前的MessageQueue消息队列中移出封装有mPendingCheckForTap这个线程的消息,为什么要这么做呢?因为你都已经不在我当前View的控制范围内了,我也没必要看你接下来的一些操作了;第9行如果我们设置了 PRESSED标志的话,说明在DOWN事件中也在MessageQueue里面添加了封装有监听长点击事件的Message,那么就需要调用第11行的 removeLongPressCallback方法,将该Message从MessageQueue中移出,并且13行调用setPressed方法将 PRESSED标志置位;

        接下来就是UP事件了:

        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;
        UP事件代码相对来说比较多,第3行的判断条件说明只要你在MOVE的过程中没有移出边界都会满足,接着第19行会判断 mHasPerformedLongPress的值,这个值只有在你设置了longclick监听事件,并且在onLongClick方法中返回true的情况下才会是true,否则均是false,这个在上面已经说过了,我们假定这里 mHasPerformedLongPress的值是false,进入if语句块,首先调用 removeLongPressCallback,从MessageQueue中移出长点击监听Message,接着第28--33行的代码比较关键,这里将是解释我们上面Log输出的重要部分,如果没有 PerformClick对象则创建,并在第31行通过post方法将 PerformClick对象添加到MessageQueue消息队列中,接下来将是执行 PerformClick的run方法了:

private final class PerformClick implements Runnable {
        public void run() {
            performClick();
        }
    }

很明显PerformClick是一个线程,在他的run方法里面也会执行performClick,也就是说不管第31行post方法有没有执行成功都会执行performClick方法的,那么这里为什么要用到post通过子线程来执行performClick而不是直接执行performClick呢?根据官方的注释看到这样做的目的是为了在click执行之前让view上面的其他visual 状态能够更新,来看看performClick方法:

public boolean performClick() {
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            return true;
        }

        return false;
    }
        可以看到如果我们设置了OnClickListener监听器的话,会执行第7行代码,也就执行了我们的onclick方法了;因为如果我们调用post的话,performClick是执行在 PerformClick类型的子线程中的,所以我们上面的Log 输出会出现onClick方法在dispatchTouchEvent事件返回之后才执行的情况了;UP事件后面的一些操作是用于状态置位的,我们再次不做过多牵涉;

       这样的话,View的事件分发源码分析完毕了,我们做个小结以此来解释上面的Log输出:

        (1)View中如果我们设置了onTouchListener、onLongClickListener以及onClickListener的话,三者的执行顺序是onTouch--->onLongClick--->onClick;

        (2)如果我们在onLongClick方法中返回true的话,那么随后的onClick方法将不再会执行;

        (3)我们的onLongClick方法以及onClick方法可能会在dispatchTouchEvent方法返回之后才去执行,原因在于onLongClick方法是在CheckForLongPress类型的子线程中执行的onClick是在PerformClick类型的子线程中执行的,也即解释了上面Log输出第12行出现在第9行之后,以及第24行出现在第21行之后的问题;

        好了,这篇先到这里了,下篇从实例测试的角度进行不同情况下的分析;








以上是关于android-----事件分发机制测试系列的主要内容,如果未能解决你的问题,请参考以下文章

android-----事件分发机制测试系列

Android系列View的事件分发机制

Android View 事件分发机制

“framework必会”系列:Android Input系统事件分发机制

Android 事件分发事件分发源码分析 ( ViewGroup 事件传递机制 四 | View 事件传递机制 )

Android事件分发机制五:面试官你坐啊