Android触摸事件派发机制源码分析之View

在开始之前,我们先写一个小案例,代码如下
Activity中的代码

package com.sparkhuu.testevent;

import android.nfc.Tag;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.RelativeLayout;

import java.io.Serializable;

public class MainActivity extends AppCompatActivity implements View.OnTouchListener, View.OnClickListener {

    private static final String TAG = "MainActivity";
    RelativeLayout rl_layout;
    Button btn_view;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        rl_layout = (RelativeLayout) this.findViewById(R.id.rl_layout);
        btn_view = (Button) this.findViewById(R.id.btn_view);

        btn_view.setOnTouchListener(this);
        rl_layout.setOnTouchListener(this);

        btn_view.setOnClickListener(this);
        rl_layout.setOnClickListener(this);
    }

    @Override
    public boolean onTouch(View view, MotionEvent motionEvent) {
        Log.i(TAG, "OnTouchListener----onTouch----action" + motionEvent.getAction() + "-----" + view);
        return false;
    }

    @Override
    public void onClick(View view) {
        Log.i(TAG, "OnClickListener---onClick---" + view);
    }
}

XML中的代码

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/rl_layout"
    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.sparkhuu.testevent.MainActivity"
    >

    <Button
        android:id="@+id/btn_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button" />
</RelativeLayout>

运行以后,我们点击Button,这个时候log日志如下
这里写图片描述
当我们点击Button以后,左右移动一下再抬起,这个时候log日志如下
这里写图片描述
当我们点击Button以外区域的log信息如下
这里写图片描述
从日志大概能猜出,我们点击Button先执行了onTouch并且当我们滑动时候会有action的改变,然后再执行onClick方法,我们很容易联想到会不会是Action_down,Action_move,Action_up分别对应action为0,2,1;
眼睑的可以看到onTouch中返回的是false,如果我们改为true呢?结果如下
这里写图片描述
则onClick不执行了
总结:
1,View的事件触发先出发onTouch,再触发onClick;
2,如果onTouch中返回true,则不会触发onClick,onTouch默认返回false
这是为什么呢?我们通过源码来进行分析
先分析下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) {
        // 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;
    }

我们重点来看这几个地方,
if (onFilterTouchEventForSecurity(event)) 判断当前view有没有被挡住,然后定义一个 ListenerInfo li = mListenerInfo;
这个ListenerInfo是view的一个静态内部类,如下

    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;

        protected OnScrollChangeListener mOnScrollChangeListener;

        /**
         * 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 dispatch context click events. This field should be made private, so it
         * is hidden from the SDK.
         * {@hide}
         */
        protected OnContextClickListener mOnContextClickListener;

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

首先li不会为空,li.mOnTouchListener呢可以发现在

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

可以发现li.mOnTouchListener是否为空取决于当前view是否设置了setOnTouchListener方法,而我们的案例中设置了,自然也不为空,接着通过位与运算判断当前view是不是enabled,默认都是enabled,接着就会判断onTouch,所以我们之前onTouch返回true的时候就直接result=true不往下执行了
当onTouch返回false的时候,这个时候会执行onTouchEvent,从

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

可以看出result的值取决于onTouchEventd的返回,

总结:
触摸事件view会先调用dispatchTouchEvent,
在dispatchTouchEvent会先执行onTouch后执行onClick
如果控件的onTouch返回false或者mOnTouchListener为null(没有setOnTouchListener的情况下)或者控件不是enabled的情况下,会调用onTouchEvent,并且dispatchTouchEvent的返回值和onTouchEvent的返回值一致,
如果控件不是enabled,设置了onTouch也不会执行,只能通过重写onTouchEvent处理,dispatchTouchEvent返回值和onTouchEvent返回值一致
如果控件是enable且onTouch返回true,dispatchTouchEvent直接返回true不会调用onTouchEvent
下面我们来看看onClick和onTouchEvent是什么东东

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

代码过多,我们来分析这段代码

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

可以看出,当控件是disabled状态下onTouchEvent的返回值与状态是否是clickable有关,是clickable的返回true,反之,返回false,view的clickable,enable可以通过xml或者java来设置
通过该方法的返回值,可以看出,如果一个控件是enabled并且是disclickabled,则直接返回false,反之,如果控件是enabled并且是clickable的则会进入一个MotionEvent的switch判断中,最终onTouchEvent返回true,ACTION_DOWN和ACTION_MOVE都做了必要的设置和置位,当ACTION_UP时,首先判断是不是被按下,同时是否可以得到焦点,然后判断如果不是longPress则通过post再UI Thread执行一个performClicl的Runnable,也就是performcilck方法,如下

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

这个方法也是先定义一个ListenerInfo然后赋值,然后判断li.mOnClickListener 是否为null,决定是否执行onClck,而li.mOnClickListener 是否为null是在setOnClick中赋值的,如下:

 /**
     * 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(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }

可以看出控件只要设置了setOnClickListener那么 li.mOnClickListener 就不为null,而且如果控件设置了setOnClickListener,那么会自动设置控件是ciclable的,

总结:
onTouchEvent的 MotionEvent.ACTION_UP中触发onCilck的监听,
当dispatchTouchEvent进行事件分发的时候,只有前一个action返回true。才会触发下一个action。

下面通过自定义一个view来测试

package com.sparkhuu.testevent;

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

/**
 * author:sparkhuu
 * email:sparkhuu@gmail.com
 */

public class TestButton extends Button {
    private static final String TAG = "TestButton";
    public TestButton(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.i(TAG, "dispatchTouchEvent -- action" + event.getAction());
        return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i(TAG, "onTouchEvent -- action" + event.getAction());
        return super.onTouchEvent(event);
    }
}

运行以后结果如下
这里写图片描述
可以看出dispatchTouchEvent先派发down事件,然后调用onTouch然后调用onTouchEvent返回true,同时dispatchTouchEvent返回true,然后dispatchTouchEvent调用move和up事件,调用完up事件以后调用onClick事件,同时dispatchTouchEvent返回true,至此,一次完整的view事件派发流程结束
将onTouchEvnet返回改为true
这里写图片描述
点击button运行结果如下
这里写图片描述
可以发现,当自定义控件只返回了true而没调用super.onTouchEvent(event),事件派发机制和上面类似,只是在up事件中没有调用onClick而已,如果稍作修改如下,可想结果和派发一致

 @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i(MainActivity.TAG, "onTouchEvent -- action" + event.getAction());
         super.onTouchEvent(event);
        return true;
    }

这里写图片描述
如果对onTouchEvent的返回再做修改,返回false,如下

@Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i(MainActivity.TAG, "onTouchEvent -- action" + event.getAction());
         super.onTouchEvent(event);
        return false;
    }

这里写图片描述
可以看出dispatchTouchEvent在派发down事件以后,紧接着调用onTouch再调用onTouchEvnet的down事件,由于onToucnEvent返回false,立即停止派发,至于后面的RelativeLayout的touch与click事件,下篇博客解释,我们可以确定如果onTouchEvent返回false,那么会阻止事件继续派发

继续修改dispatchTouchEvent返回true

 @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.i(MainActivity.TAG, "dispatchTouchEvent -- action" + event.getAction());
//        return super.dispatchTouchEvent(event);
        return true;
    }

这里写图片描述
可以发现dispatchTouchEvent直接返回true且不调用super,任何事件都得不到触发,同类如果调用super,代码修改如下

@Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.i(MainActivity.TAG, "dispatchTouchEvent -- action" + event.getAction());
         super.dispatchTouchEvent(event);
        return true;
    }

这里写图片描述
结果正常
如果修改dispatchTouchEvent返回值为false,代码如下

 @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.i(MainActivity.TAG, "dispatchTouchEvent -- action" + event.getAction());
//         super.dispatchTouchEvent(event);
        return false;
    }

这里写图片描述
可以看出事件不进行任何触发,关于点击Button触发了Relatvie的事件暂不用关注,下篇解释
继续修改调用super,代码如下

 @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.i(MainActivity.TAG, "dispatchTouchEvent -- action" + event.getAction());
         super.dispatchTouchEvent(event);
        return false;
    }

这里写图片描述
可以看出第一次down事件派发完成,返回false不进行下一次事件派发
再对代码进行如下修改

package com.sparkhuu.testevent;

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

/**
 * author:sparkhuu
 * email:sparkhuu@gmail.com
 */

public class TestButton extends Button {
    public TestButton(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.i(MainActivity.TAG, "dispatchTouchEvent -- action" + event.getAction());
         super.dispatchTouchEvent(event);
        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i(MainActivity.TAG, "onTouchEvent -- action" + event.getAction());
         super.onTouchEvent(event);
        return false;
    }
}

这里写图片描述
再对代码作修改

package com.sparkhuu.testevent;

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

/**
 * author:sparkhuu
 * email:sparkhuu@gmail.com
 */

public class TestButton extends Button {
    public TestButton(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.i(MainActivity.TAG, "dispatchTouchEvent -- action" + event.getAction());
         super.dispatchTouchEvent(event);
        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i(MainActivity.TAG, "onTouchEvent -- action" + event.getAction());
         super.onTouchEvent(event);
        return true;
    }
}

这里写图片描述
由此,dispatchTouchEvent是负责事件派发的,返回false将停止下次事件派发,返回true将继续下次事件派发,
总结
1触摸控件view首先执行dispatchTouchEvent进行事件派发
2在dispatchTouchEvnet中会首先执行onTouch方法,后执行onClick方法
3如果控件不是enabled设置了onTouch也不会执行,只能通过重写onTouchEvent方法来实现,dispatchTouchEvent的返回值和onTouchEvent的返回值一致
4如果控件的onTouch返回false或者onTouchlistener为null,或者控件不是enabled情况下调用onTouchEvent,dispatchTouchEvent和ontoucheEvent的返回一样
5如果控件是enable且onTouch返回true情况下,dispatchTouchEvent直接返回true,不会调用onTouchEvent
6当dispatchTouchEvent在事件派发的时候,只有前一个action返回true,才会触发下一个action

可以查看ViewGroup分析
ViewGroup分析

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值