Android 源码解析View的touch事件分发机制

Posted yuminfeng728

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 源码解析View的touch事件分发机制相关的知识,希望对你有一定的参考价值。

概述

本篇主要分析的是touch事件的分发机制,网上关于这个知识点的分析文章非常多。但是还是想通过结合自身的总结,来加深自己的理解。对于事件分发机制,我将使用两篇文章对其进行分析,一篇是针对View的事件分发机制解析,一篇是针对ViewGroup的事件分发机制解析。本片是对View的事件分发机制进行解析,主要采用案例结合源码的方式来进行分析。

前言

在分析事件分发机制之前,我们先来学习一下基本的知识点,以便后面的理解。
View中有两个关键方法参与到Touch事件分发
dispatchTouchEvent(MotionEvent event) 和 onTouchEvent(MotionEvent event)
所有Touch事件类型都被封装在对象MotionEvent中,包括ACTION_DOWN,ACTION_MOVE,ACTION_UP等等。
每个执行动作必须执行完一个完整的流程,再继续进行下一个动作。比如:ACTION_DOWN事件发生时,必须等这个事件的分发流程执行完(包括该事件被提前消费),才会继续执行ACTION_MOVE或者ACTION_UP的事件。

案例分析

为了能够清楚的监视事件的分发过程,我们采用自定义View的形式,查看内部的方法执行过程。
上代码:

package com.yuminfeng.touch;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.Button;

public class MyButton extends Button {

    public MyButton(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {

        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            Log.i("yumf", "MyButton=====dispatchTouchEvent ACTION_DOWN");
            break;

        case MotionEvent.ACTION_UP:
            Log.i("yumf", "MyButton=====dispatchTouchEvent ACTION_UP");
            break;
        }
        return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            Log.i("yumf", "MyButton=====onTouchEvent ACTION_DOWN");
            break;

        case MotionEvent.ACTION_UP:
            Log.i("yumf", "MyButton=====onTouchEvent ACTION_UP");
            break;
        }
        return super.onTouchEvent(event);
    }

}

在XML布局中引用该控件,非常简单。

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.yuminfeng.myviewpager.FirstActivity" >

    <com.yuminfeng.touch.MyButton
        android:id="@+id/mybutton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/hello_world" />

</RelativeLayout>

以上代码都非常简单,没有什么逻辑,就是重写Button的dispatchTouchEvent和onTouchEvent的方法,然后引用该控件即可。
然后执行代码,查看日志打印,如下:
这里写图片描述
由此看到,当点击控件时,首先执行的是dispatchTouchEvent方法,然后再执行onTouchEvent的方法。
如果此时我们修改dispatchTouchEvent的返回值为true时(默认为false),那么onTouchEvent方法便不再执行,如下:
这里写图片描述
流程示意图如下:
这里写图片描述

接着我们恢复之前的返回值false,继续让mybutton设置一个setOnTouchListener监听事件,关键代码如下:

     mybutton = (Button) findViewById(R.id.mybutton);

        mybutton.setOnTouchListener(new OnTouchListener() {

            @Override
            public boolean onTouch(View v, MotionEvent event) {

                switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    Log.i("yumf", "Activity=====onTouch ACTION_DOWN");
                    break;

                case MotionEvent.ACTION_UP:
                    Log.i("yumf", "Activity=====onTouch ACTION_UP");
                    break;
                }
                return false;
            }
        });

执行后,日志打印如下:
这里写图片描述
由此我们可以看到,首先执行方法dispatchTouchEvent,然后再执行OnTouchListener中onTouch方法,最后执行onTouchEvent方法。
同上,如果我们继续修改dispatchTouchEvent的返回值为true时,那么后面的方法onTouch,onTouchEvent均不执行。
如果我们修改onTouch的返回值为true,那么后面的onTouchEvent事件就不会执行了。
流程示意图如下:
这里写图片描述
如上,恢复默认返回值false,然后在button上设置一个监听点击事件,代码如下:

     mybutton.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                Log.i("yumf", "Activity=====onClick");
            }
        });

执行后,查看日志打印信息,如下:
这里写图片描述
由此我们可以知道,在完整的事件结束之后(从ACTION_DOWN开始,到ACTION_UP结束),这时才会去执行button的onClick方法。
综合以上所述,View在处理Touch事件时,都是从dispatchTouchEvent方法开始的,因此我们在分析源码时,可以从该方法入手。

源码阅读

我们当前的MyButton是继承自Button,而Button又是继承自TextView,TextView继承自View,逐步往上查看,可以发现父类的dispatchTouchEvent方法,就是View的dispatchTouchEvent方法。如下:

   /**
     * Pass the touch screen motion event down to the target view, or this
     * view if it is the target.
     *
     * @param event The motion event to be dispatched.
     * @return True if the event was handled by the view, false otherwise.
     */
    public boolean dispatchTouchEvent(MotionEvent event) {
        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)) {
            //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;
    }

如上代码中,我们来逐一进行分析,首先是

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

通过查看mInputEventConsistencyVerifier,得知这段代码主要是用来调试的,可以不用关注。接着继续查看下一段代码

        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }

当执行ACTION_DOWN事件时,进入方法stopNestedScroll()中,进入该方法中

 /**
     * Stop a nested scroll in progress.
     *
     * <p>Calling this method when a nested scroll is not currently in progress is harmless.</p>
     *
     * @see #startNestedScroll(int)
     */
    public void stopNestedScroll() {
        if (mNestedScrollingParent != null) {
            mNestedScrollingParent.onStopNestedScroll(this);
            mNestedScrollingParent = null;
        }
    }

该方法主要是用来停止View的滑动,当一个滚动的view不是当前进行接收事件的View时不会受到影响。下面的一段代码是关键的代码,我们来看看

        if (onFilterTouchEventForSecurity(event)) {
            //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;
            }
        }

上面的代码中,首先根据安全策略过滤event,来确定是否响应这个事件,返回true表示响应。响应该事件后,将mListenerInfo赋值给ListenerInfo对象。那么这个mListenerInfo到底是什么呢,我们现在来分析一下mListenerInfo的初始化
首先,我们可以在View的属性中,能看到该对象的引用:

ListenerInfo mListenerInfo;

接着,在getListenerInfo()方法中初始化:

    ListenerInfo getListenerInfo() {
        if (mListenerInfo != null) {
            return mListenerInfo;
        }
        mListenerInfo = new ListenerInfo();
        return mListenerInfo;
    }

最后,在为该View的对象设置监听器时,会将对应的监听器对象返回赋值给mListenerInfo对象,如下:

/**
     * Register a callback to be invoked when focus of this view changed.
     *
     * @param l The callback that will run.
     */
    public void setOnFocusChangeListener(OnFocusChangeListener l) {
        getListenerInfo().mOnFocusChangeListener = l;
    }

    /**
     * Returns the focus-change callback registered for this view.
     *
     * @return The callback, or null if one is not registered.
     */
    public OnFocusChangeListener getOnFocusChangeListener() {
        ListenerInfo li = mListenerInfo;
        return li != null ? li.mOnFocusChangeListener : null;
    }

    /**
     * Register a callback to be invoked when this view is clicked. If this view is not
     * clickable, it becomes clickable.
     *
     * @param l The callback that will run
     *
     * @see #setClickable(boolean)
     */
    public void setOnClickListener(OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }

    /**
     * Register a callback to be invoked when this view is clicked and held. If this view is not
     * long clickable, it becomes long clickable.
     *
     * @param l The callback that will run
     *
     * @see #setLongClickable(boolean)
     */
    public void setOnLongClickListener(OnLongClickListener l) {
        if (!isLongClickable()) {
            setLongClickable(true);
        }
        getListenerInfo().mOnLongClickListener = l;
    }

    /**
     * Register a callback to be invoked when a touch event is sent to this view.
     * @param l the touch listener to attach to this view
     */
    public void setOnTouchListener(OnTouchListener l) {
        getListenerInfo().mOnTouchListener = l;
    }

如上,其实里面涉及的方法非常多,我只抽出了几个常见的方法,如:setOnClickListener,setOnTouchListener等。
所以说当我们给View的对象设置监听器时,通过回调的方式,最后都会赋值到mListenerInfo对象中。mListenerInfo类里面包含了许多的监听器类型,如下:

    static class ListenerInfo {
        /**
         * Listener used to dispatch focus change events.
         * This field should be made private, so it is hidden from the SDK.
         * {@hide}
         */
        protected OnFocusChangeListener mOnFocusChangeListener;

        /**
         * Listeners for layout change events.
         */
        private ArrayList<OnLayoutChangeListener> mOnLayoutChangeListeners;

        /**
         * Listeners for attach events.
         */
        private CopyOnWriteArrayList<OnAttachStateChangeListener> mOnAttachStateChangeListeners;

        /**
         * Listener used to dispatch click events.
         * This field should be made private, so it is hidden from the SDK.
         * {@hide}
         */
        public OnClickListener mOnClickListener;

        /**
         * Listener used to dispatch long click events.
         * This field should be made private, so it is hidden from the SDK.
         * {@hide}
         */
        protected OnLongClickListener mOnLongClickListener;

        /**
         * Listener used to build the context menu.
         * This field should be made private, so it is hidden from the SDK.
         * {@hide}
         */
        protected OnCreateContextMenuListener mOnCreateContextMenuListener;

        private OnKeyListener mOnKeyListener;

        private OnTouchListener mOnTouchListener;

        private OnHoverListener mOnHoverListener;

        private OnGenericMotionListener mOnGenericMotionListener;

        private OnDragListener mOnDragListener;

        private OnSystemUiVisibilityChangeListener mOnSystemUiVisibilityChangeListener;

        OnApplyWindowInsetsListener mOnApplyWindowInsetsListener;
    }

完成了监听器类型的赋值后,我们分析继续下面的代码逻辑:

        if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }

这里在if条件里面我们看到了一个属性的方法li.mOnTouchListener.onTouch(this, event),这就是我们在Activity中设置的setOnTouchListener中,重写的onTouch方法。当返回为true时,result = true,这时便不执行下面代码中的onTouchEvent(event)方法。result 为false时,才执行onTouchEvent(event)方法。这段关键性的代码中,对应了我之前所做的实验结果。
这里写图片描述

下面,我们继续分析方法View的onTouchEvent(MotionEvent event)的内部执行。

 /**
     * Implement this method to handle touch screen motion events.
     * <p>
     * If this method is used to detect click actions, it is recommended that
     * the actions be performed by implementing and calling
     * {@link #performClick()}. This will ensure consistent system behavior,
     * including:
     * <ul>
     * <li>obeying click sound preferences
     * <li>dispatching OnClickListener calls
     * <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
     * accessibility features are enabled
     * </ul>
     *
     * @param event The motion event.
     * @return True if the event was handled, false otherwise.
     */
    public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;

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

        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

        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, x, y);
                       }

                        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();
                        }
                        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);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    setPressed(false);
                    removeTapCallback();
                    removeLongPressCallback();
                    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;
    }

首先看这个方法的说明,实现这个方法来处理触摸屏幕的动作事件。如果这个方法被用来检测点击动作,它是建议执行和调用的操作。如果这个事件被处理,返回true,否则返回false。
现在我们来看逐一代码,第一个if语句块中,判断View的状态是否可用,如果不可用则设置为不可按压,否则为设置为可点击和可长按。然后下面在可点击和可长按的条件下,进行touch事件的逻辑处理。在这个if语句内部有switch条件判断,将分别对不同的事件进行处理,如MotionEvent.ACTION_UP,MotionEvent.ACTION_DOWN,MotionEvent.ACTION_CANCEL 和MotionEvent.ACTION_MOVE几个不同的事件。下面我们将逐一对其进行分析。
首先是MotionEvent.ACTION_UP中:
判断prepressed为true后,进入执行体;
设置setPressed(true, x, y);
判断mHasPerformedLongPress是否执行长按操作,如果mOnLongClickListener.onLongClick 返回true时,mHasPerformedLongPress = true,这时便不会执行performClick()方法。否则继续执行如下,判断mPerformClick为空,初始化一个实例。添加到消息队列中,如果添加失败则直接执行performClick()方法,否则在PerformClick对象的run中执行performClick()。查看一下performClick()方法,如下:

  /**
     * Call this view's OnClickListener, if it is defined.  Performs all normal
     * actions associated with clicking: reporting accessibility event, playing
     * a sound, etc.
     *
     * @return True there was an assigned OnClickListener that was called, false
     *         otherwise is returned.
     */
    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;
    }

如上,我们可以看到一个非常熟悉的方法onClick。在if中判断,如果我们给View设置了mOnClickListener的监听接口,在这里我们会回调mOnClickListener中的onClick方法。(原来点击事件的onClick方法是在ACTION_UP时,执行的)
接着,看到创建UnsetPressedState对象,然后执行UnsetPressedState对象中的run方法,我们进入这个方法查看,

  private final class UnsetPressedState implements Runnable {
        @Override
        public void run() {
            setPressed(false);
        }
    }

    /**
     * Sets the pressed state for this view.
     *
     * @see #isClickable()
     * @see #setClickable(boolean)
     *
     * @param pressed Pass true to set the View's internal state to "pressed", or false to reverts
     *        the View's internal state from a previously set "pressed" state.
     */
    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);
    }

可以看到,这里面是用来取消mPrivateFlags 中的PFLAG_PRESSED标志,然后刷新背景。
ACTION_UP最后一步,removeTapCallback() 移除消息队列中的之前加入的所有回调操作。

接着分析MotionEvent.ACTION_DOWN中内部代码:
首先mHasPerformedLongPress = false,设置长按操作为false。
接着判断View是否处在可滑动的容器中,如果为false,则设置View的PRESSED状态和检查长按动作。

接着分析MotionEvent.ACTION_CANCEL的事件:
代码非常简单,设置PRESSED为false,移除所有的回调,移除长按的回调。

最后来分析MotionEvent.ACTION_MOVE的事件:
判断触摸点是否移出View的范围,如果移出了则执行removeTapCallback(),取消所有的回调。接着判断是否包含PRESSED标识,如果包含则执行方法removeLongPressCallback() 和 setPressed(false);

到这里我们可以知道,onTouchEvent方法中处理Touch事件的具体操作,并控制了View的点击事件。在如果在点击View时,想要长按和短按都产生效果,即setOnLongClickListener和setOnClickListener都能够执行的话,只需要在setOnLongClickListener的onLongClick方法中返回false,这时两个方法便都能执行。
至此关于View的Touch事件分发流程已经分析完成,下一篇将介绍ViewGroup的分发机制。

以上是关于Android 源码解析View的touch事件分发机制的主要内容,如果未能解决你的问题,请参考以下文章

Android Touch事件分发(源码分析)

源码阅读分析 - View的Touch事件分发

View源码-Touch事件

Android Touch事件传递机制全面解析(从WMS到View树)

Android 1.6View和ViewGroup的touch事件分析和总结

Android Touch事件相关源码Android SourceCode 2.3.6