17.源码阅读(Touch事件分发)

简介: 有这样一个布局,Activity中有一个ViewGroup,ViewGroup中又放了一个View,我重写了Activity的dispatchTouchEvent和onTouchEvent,重写了ViewGroup的dispatchTouchEvent...

有这样一个布局,Activity中有一个ViewGroup,ViewGroup中又放了一个View,我重写了Activity的dispatchTouchEvent和onTouchEvent,重写了ViewGroup的dispatchTouchEvent,onInterceptTouchEvent和onTouchEvent,重写了View的dispatchTouchEvent和onTouchEvent,然后触摸一下TestTouchView,看看控制台打印了什么结果

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical"
    tools:context=".test.source.TestViewTouchActivity">
    <com.app.rzm.test.view.TestTouchViewGroup
        android:layout_width="match_parent"
        android:background="#f00"
        android:layout_height="400dp">
        <com.app.rzm.test.view.TestTouchView
            android:layout_width="200dp"
            android:background="#0f0"
            android:id="@+id/touch"
            android:layout_height="200dp" />
    </com.app.rzm.test.view.TestTouchViewGroup>

</LinearLayout>
1.默认没有任何拦截和事件处理的情况下打印结果
05-28 08:07:49.305 1590-1590/com.app.rzm D/Activity: dispatchTouchEvent:0
05-28 08:07:49.306 1590-1590/com.app.rzm D/ViewGroup: dispatchTouchEvent:0
    onInterceptTouchEvent:0
05-28 08:07:49.306 1590-1590/com.app.rzm D/View: dispatchTouchEvent:0
05-28 08:07:49.307 1590-1590/com.app.rzm D/View: onTouchEvent:0
05-28 08:07:49.307 1590-1590/com.app.rzm D/ViewGroup: onTouchEvent:0
05-28 08:07:49.309 1590-1590/com.app.rzm D/Activity: onTouchEvent:0
05-28 08:07:49.327 1590-1590/com.app.rzm D/Activity: dispatchTouchEvent:2
    onTouchEvent:2
05-28 08:07:49.344 1590-1590/com.app.rzm D/Activity: dispatchTouchEvent:2
    onTouchEvent:2
05-28 08:07:49.361 1590-1590/com.app.rzm D/Activity: dispatchTouchEvent:2
    onTouchEvent:2
05-28 08:07:49.377 1590-1590/com.app.rzm D/Activity: dispatchTouchEvent:2
05-28 08:07:49.378 1590-1590/com.app.rzm D/Activity: onTouchEvent:2
05-28 08:07:49.394 1590-1590/com.app.rzm D/Activity: dispatchTouchEvent:2
05-28 08:07:49.395 1590-1590/com.app.rzm D/Activity: onTouchEvent:2

可以看到在没有进行事件拦截的情况下,打印的顺序如上,我们可以简单分析以下事件传递流程:

有一点需要注意,事件的按下,滑动和抬起三种类型事件是一个一个进行传递的
1.当手指触摸屏幕时,按下事件收到响应,Activity的dispatchTouchEvent收到回调,将按下事件传递到了ViewGroup中的dispatchTouchEvent方法
2.ViewGroup收到按下事件时,调用onInterceptTouchEvent判断是否拦截按下事件,因为没有设置拦截,所以按下事件又被传递到View的dispatchTouchEvent方法中
3.View收到按下事件,传递到它的onTouchEvent方法去处理,由于这个View并没有设置处理这个事件,所以又将它传递到父布局ViewGroup的onTouchEvent中,看它是不是需要处理
4.ViewGroup的onTouchEvent收到这个事件,仍没有进行处理,所以又将事件传递到了Activity的onTouchEvent方法
5.Activity同样不处理,所以这个事件因为没人处理,所以就不了了之
6.ACTION_DOWN事件没有被处理,ACTION_MOVE事件就一直在Activity的dispatchTouchEvent和onTouchEvent方法间传递,直到ACTION_UP事件响应


img_ba8c4f8b0a04b849fd37be11e24d057b.jpe
Touch事件传递流程.jpg

源码中事件的传递,首先来到Activity的dispatchTouchEvent中

    /**
     * Called to process touch screen events.  You can override this to
     * intercept all touch screen events before they are dispatched to the
     * window.  Be sure to call this implementation for touch screen events
     * that should be handled normally.
     *
     * @param ev The touch screen event.
     *
     * @return boolean Return true if this event was consumed.
     */
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            //这个方法是提供出来进行重写的,不用管
            onUserInteraction();
        }
        //开始分发触摸事件
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        //如果事件没有被处理,则执行onTouchEvent方法
        return onTouchEvent(ev);
    }

来到PhoneWindow中的superDispatchTouchEvent,可以看到它调用了DecorView中的方法,通过之前看setContentView的源码(https://www.jianshu.com/p/2f87ebe77f4e)我们已经认识了DecorView,接下来进入DecorView源码中

    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

可以看到DecorView又调用了它父类的方法

    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

进入ViewGroup中的dispatchTouchEvent方法,这里是事件分发的源头。我们可以看到在这个方法中,做了许多的条件判断,而最终都会执行方法dispatchTransformedTouchEvent,区别在于方法中传递的参数会根据场景而有所不同

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        ......
        boolean handled = false;
        //过滤掉一些不安全的劫持式的触摸事件
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            //新的按下事件产生时就初始化所有之前的触摸状态

            //检测事件是否被拦截

            if (!canceled && !intercepted) {

                //获取到当前得到焦点的view
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;

                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
          
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        
                        //获取到所有可以接收这个触摸事件的view封装到集合中
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        ......
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }

                            ......
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                ......
                                break;
                            }
                            .......
                        }
                        .......
                    }

                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        ......
                    }
                }
            }
            ......
        return handled;
    }

我们来到方法dispatchTransformedTouchEvent中,这个方法的作用就是将触摸事件传递到当前的view中,如果当前view为null,就传递到它的上一层的ViewGroup,最终的是携带的MotionEvent参数,可以看到当存在按下滑动等事件时,这些事件会被封装到MotionEvent中传递到view

/**
     * Transforms a motion event into the coordinate space of a particular child view,
     * filters out irrelevant pointer ids, and overrides its action if necessary.
     * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
     */
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;

        // Canceling motions is a special case.  We don't need to perform any transformations
        // or filtering.  The important part is the action, not the contents.
        final int oldAction = event.getAction();
        //如果当前事件是取消,则将取消参数设置给MotionEvent传递到view中
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }

        //获取到手指触摸移动的位置信息
        // Calculate the number of pointers to deliver.
        final int oldPointerIdBits = event.getPointerIdBits();
        final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;

        // If for some reason we ended up in an inconsistent state where it looks like we
        // might produce a motion event with no pointers in it, then drop the event.
        if (newPointerIdBits == 0) {
            return false;
        }

        // If the number of pointers is the same and we don't need to perform any fancy
        // irreversible transformations, then we can reuse the motion event for this
        // dispatch as long as we are careful to revert any changes we make.
        // Otherwise we need to make a copy.
        final MotionEvent transformedEvent;

        //回调位置信息到下一级的view的dispatchTouchEvent方法
        if (newPointerIdBits == oldPointerIdBits) {
            if (child == null || child.hasIdentityMatrix()) {
                if (child == null) {
                    handled = super.dispatchTouchEvent(event);
                } else {
                    final float offsetX = mScrollX - child.mLeft;
                    final float offsetY = mScrollY - child.mTop;
                    event.offsetLocation(offsetX, offsetY);

                    handled = child.dispatchTouchEvent(event);

                    event.offsetLocation(-offsetX, -offsetY);
                }
                return handled;
            }
            transformedEvent = MotionEvent.obtain(event);
        } else {
            transformedEvent = event.split(newPointerIdBits);
        }

        // Perform any necessary transformations and dispatch.
        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            final float offsetX = mScrollX - child.mLeft;
            final float offsetY = mScrollY - child.mTop;
            transformedEvent.offsetLocation(offsetX, offsetY);
            if (! child.hasIdentityMatrix()) {
                transformedEvent.transform(child.getInverseMatrix());
            }

            handled = child.dispatchTouchEvent(transformedEvent);
        }

        // Done.
        transformedEvent.recycle();
        return handled;
    }

事件从ViewGroup分发到了View的dispatchTouchEvent方法,我们进入View的这个方法中,这里涉及到onTouch,onTouchEvent和onClick方法的回调顺序问题,可以在代码中看到,在onClickListener 和onTouchListener都设置了的情况下,最先执行的会是onTouch,并且如果onTouch方法返回值设置为true,那么将不会再往下回调onTouchEvent和onClick方法,事件就被onTouch拦截了;当其返回值为false,则会继续向下开始执行onTouchEvent,onClick方法是在onTouchEvent之后执行的,所以三者的执行顺序,在没有拦截的情况下,可见是onTouch > onTouchEvent > onClick

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

        ......

        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            //按下后停止view的滑动
            stopNestedScroll();
        }
        //安全性过滤
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //ListenerInfo 对象封装了所有的view的监听相关的信息
            //例如onTouchListener onClickListener等,只要给view设置
            //了监听,这个对象就会被创建,可以在下一个代码块中
            //看到ListenerInfo 的结构类型
            ListenerInfo li = mListenerInfo;

            //这里会决定onTouch方法是否执行,如果给view设置了
            //onTouchListener,那么ListenerInfo 对象存在,li != null满足
            //li.mOnTouchListener != null也满足,
            //(mViewFlags & ENABLED_MASK) == ENABLED
            //用来判断这个view是否可用,如果被设置为enable false,那么
            //是无法处理事件的,条件都满足了,就会回调onTouch方法了
            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;
    }

ListenerInfo

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;

        OnCapturedPointerListener mOnCapturedPointerListener;
    }

进入onTouchEvent方法,这个方法处理的比较复杂,我们删除一些非核心的代码来看.setOnClickListener的onClick方法最后是在抬起手指的时候执行的,所以这里有一个问题,如果你自定义的View重写了onTouchEvent,并且没有调用父类的super.onTouchEvent方法,那么你设置的onClickListener将会失效

/**
     * 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) {

        //检查是否可点击

        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                //在手势抬起时执行onClick方法
                case MotionEvent.ACTION_UP:
                     ......
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();
                    ......
                    break;

                case MotionEvent.ACTION_DOWN:
                    ......
                    break;

                case MotionEvent.ACTION_CANCEL:
                    ......
                    break;

                case MotionEvent.ACTION_MOVE:
                    ......
                    break;
            }

            return true;
        }

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

        notifyEnterOrExitForAutoFillIfNeeded(true);

        return result;
    }

总结一下,到这里touch事件传递机制基本上简单的走了以下流程。事件开始的起点在所触摸的Activity的dispatchTouchEvent方法,通过这个方法将事件通过PhoneWindow传递到DecorView,然后又传递到ViewGroup中,在ViewGroup中进行事件的分发,首先获取到所有可以接收这个事件的View集合,然后将事件传递到当前获取焦点的View,一层一层的传递,经过ViewGroup的dispatchTouchEvent onInterceptTouchEvent方法进入View的dispatchTouchEvent方法,最后进入onTouchEvent方法进行事件的处理

相关文章
|
4月前
|
XML Java Android开发
Android App开发触摸事件中手势事件Event的分发流程讲解与实战(附源码 简单易懂)
Android App开发触摸事件中手势事件Event的分发流程讲解与实战(附源码 简单易懂)
43 0
|
10月前
|
Android开发 开发者 容器
Android事件分发机制
Android事件分发机制
|
Android开发
Android事件分发机制之ACTION_DOWN
Android的事件分发机制也是老生常谈了,这篇文章并不是笼统的介绍这个机制,而是针对ACTION_DOWN这个事件探讨相关的细节。
319 0
|
Android开发
事件分发四部曲之三《CoordinatorLayout事件分析》
事件分发四部曲之三《CoordinatorLayout事件分析》
事件分发四部曲之三《CoordinatorLayout事件分析》
|
Android开发
事件分发四部曲之二《嵌套滑动事件分析》
事件分发四部曲之二《嵌套滑动事件分析》
事件分发四部曲之二《嵌套滑动事件分析》
|
JSON 移动开发 人工智能