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方法进行事件的处理

相关文章
|
5月前
|
Android开发
39. 【Android教程】触摸事件分发
39. 【Android教程】触摸事件分发
43 2
|
6月前
|
XML Java Android开发
Android App开发触摸事件中手势事件Event的分发流程讲解与实战(附源码 简单易懂)
Android App开发触摸事件中手势事件Event的分发流程讲解与实战(附源码 简单易懂)
99 0
|
Android开发 开发者 容器
Android事件分发机制
Android事件分发机制
|
程序员 Android开发
牛逼!终于有人能把Android事件分发机制讲明白了
在Android开发中,事件分发机制是一块Android比较重要的知识体系,了解并熟悉整套的分发机制有助于更好的分析各种点击滑动失效问题,更好去扩展控件的事件功能和开发自定义控件,同时事件分发机制也是Android面试必问考点之一,如果你能把下面的一些事件分发图当场画出来肯定加分不少。废话不多说,总结一句:事件分发机制很重要。
牛逼!终于有人能把Android事件分发机制讲明白了
|
Android开发 容器
事件分发四部曲之一《深度遍历讲解Android事件分发机制》
事件分发四部曲之一《深度遍历讲解Android事件分发机制》
事件分发四部曲之一《深度遍历讲解Android事件分发机制》
|
Android开发
事件分发四部曲之二《嵌套滑动事件分析》
事件分发四部曲之二《嵌套滑动事件分析》
事件分发四部曲之二《嵌套滑动事件分析》
|
Android开发
事件分发四部曲之三《CoordinatorLayout事件分析》
事件分发四部曲之三《CoordinatorLayout事件分析》
事件分发四部曲之三《CoordinatorLayout事件分析》
|
Android开发
Android事件分发机制之ACTION_DOWN
Android的事件分发机制也是老生常谈了,这篇文章并不是笼统的介绍这个机制,而是针对ACTION_DOWN这个事件探讨相关的细节。
360 0