码迷,mamicode.com
首页 > 其他好文 > 详细

【自定义View系列】04--谈谈事件分发

时间:2016-07-10 18:45:00      阅读:179      评论:0      收藏:0      [点我收藏+]

标签:

引言:这部分会分三个模块来讲,先讲View对Touch的处理,再讲ViewGroup的事件分发,最后讲如何解决滑动冲突。

我习惯通过在源码中添加注释来理解源码,以下是我提取出来几个重要方法,将不重要的部分删掉,并且添加了中文注释。


一、先从View讲起

如果一个View(比如Button)接收到Touch,那么该Touch事件首先会传入到它的dispatchTouchEvent( )方法,所以我们从这里开始学习View对Touch事件的处理。

    // 返回值表示Touch事件是否被该View消费
    public boolean dispatchTouchEvent(MotionEvent event) {
        //result的值决定最后该方法的返回值,也就是决定Touch事件是否被消费
        boolean result = false;

        /***/

        if (onFilterTouchEventForSecurity(event)) {
            ListenerInfo li = mListenerInfo;
            //该if判断中一共包含了4个条件,必须同时满足时才表示Touch事件被消费
            //在这四个条件中,我们通常最关心的就是最后一个:TouchListener的onTouch()方法。假如这四个条件中的任意一个不满足,那么result仍为false;则进入下一步调用自身的onTouchEvent()
            if (li != null && li.mOnTouchListener != null &&
                (mViewFlags&ENABLED_MASK)==ENABLED && li.mOnTouchListener.onTouch(this,event)) {
                        result = true;
            }

            //调用View自身的onTouchEvent()处理Touch事件,由onTouchEvent的返回值决定result的值(当然,如果在上一步中,如果已经将result设为true,就不会去判断onTouchEvent()了)
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

        /***/

        return result;
    }

onTouchEvent()是决定事件是否被消耗的最后一道门,如果返回false,则它的父View的onTouchEvent会被调用,否则不会;
先来看看重要部分的源码:

public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();

        //如果一个View是disable的,CLICKABLE,LONG_CLICKABLE,CONTEXT_CLICKABLE消耗掉,且不会触发onClick事件回调
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            return (((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
        }

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

        //如果View不是disable的,会继续执行,对CLICK,LONG_CLICK,CONTEXT_CLICKABLE进行处理
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    /***/
                    break;

                case MotionEvent.ACTION_MOVE:
                    /***/
                    break;

                case MotionEvent.ACTION_CANCEL:
                    /***/
                    break;

                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        /***/
                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            /***/
                            if (!focusTaken) {
                                /***/
                                if (!post(mPerformClick)) {
                                    //performClick()中会回调onClick,所以我们平时常见的onClick回调都是在ACTION_UP的时候触发的
                                    performClick();
                                }
                            }
                        }
                    }
                    /***/
            }

            //这里表示,只要View是enable的,Touch事件都会被消耗掉
            return true;
        }

        return false;
    }

引用一张谷歌的小弟画的流程图:

技术分享

需要注意的点:

  • onTouch()与onTouchEvent()以及click三者的区别和联系 :

    • onTouch()与onTouchEvent()都是处理触摸事件的API
    • onTouch()属于TouchListener接口中的方法,是View暴露给用户的接口便于处理触摸事件,而onTouchEvent()是Android系统自身对于Touch处理的实现
    • 先调用onTouch()后调用onTouchEvent()。而且只有当onTouch()未消费Touch事件才有可能调用到onTouchEvent()。即onTouch()的优先级比onTouchEvent()的优先级更高。
    • 在onTouchEvent()中处理ACTION_UP时会利用ClickListener执行Click事件。所以Touch的处理是优先于Click的
    • 简单地说三者执行顺序为:onTouch()–>onTouchEvent()–>onClick()
  • View没有事件的拦截(onInterceptTouchEvent( )),ViewGroup才有,请勿混淆


二、ViewGroup的事件分发

  • Touch事件会从PhoneWindow开始一直传递到最顶层的ViewGroup,然后调用到最顶层的dispatchTouchEvent()

事件分发体系最重要的几个方法:

  • dispatchTouchEvent(event)

    • 主要完成事件分发的逻辑,只要事件到达该View,一定会调用这个方法,返回值表示是否消耗当前事件。
  • onInterceptTouchEvent(Event)

    • 判断是否拦截某个事件,返回值表示是否拦截
  • onTouchEvent(Event)

    • 用来处理Touch事件,返回值表示是否消耗该事件。
  • 他们的关系可以这样表示:

public boolean dispatchTouchEvent(MotionEvent e) {
    if(onInterceptTouchEvent(ev)) {
        //如果拦截,就自己处理,调用自己的onTouchEvent,如果onTouchEvent返回true就消费掉,如果返回false就传给上层处理
        return onTouchEvent(ev);
    } else {
        //如果不拦截,就走子view的分发流程
        return child.dispatchTouchEvent(ev);
    }
}
  • 来看看源码:
public boolean dispatchTouchEvent(MotionEvent ev) {

        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            if (actionMasked == MotionEvent.ACTION_DOWN) {
                //如果是DOWN事件,进行初始化和还原操作
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

            // 判断是否需要拦截事件,根据intercepted的值确定
            // mFirstTouchTarget用于多点触控
            // mFirstTouchTarget不为空,表示有子View消费了Touch事件
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    // 如果是DOWN或者有子View消费,则根据onInterceptTouchEvent判断是否拦截
                    // ViewGroup的onInterceptTouchEvent默认返回false,也就是不拦截
                    // 所以我们往往可以在自定义控件中重写这个方法,来决定什么情况下拦截事件
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action);
                } else {
                    // 如果disallowIntercept == false,就是不允许拦截,可以在子View设置不允许父View拦截
                    intercepted = false;
                }
            } else {
                // 执行到这里,说明mFirstTouchTarget为null或者不是DOWN事件,需要ViewGroup自己处理此次Touch事件
                // 也就是拦截本次Touch事件
                intercepted = true;
            }

            // 如果Touch事件没有被取消也没有被拦截,那么ViewGroup将类型为ACTION_DOWN的Touch事件分发给子View。
            if (!canceled && !intercepted) {

                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {

                    // 根据Touch事件的坐标,找到触摸到了哪个子View
                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        final ArrayList<View> preorderedList = buildOrderedChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = customOrder
                                    ? getChildDrawingOrder(childrenCount, i) : i;
                            final View child = (preorderedList == null)
                                    ? children[childIndex] : preorderedList.get(childIndex);

                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            /***/

                            // 找到之后,调用dispatchTransformedTouchEvent,传入子view
                            // 子View没有消费Touch事件则该方法的返回值为false,此时mFirstTouchTarget仍为null
                            // 如果子View消费掉了Touch事件那么该方法的返回值为true,然后执行newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                if (preorderedList != null) {
                                    // childIndex points into presorted list, find original index
                                    for (int j = 0; j < childrenCount; j++) {
                                        if (children[childIndex] == mChildren[j]) {
                                            mLastTouchDownIndex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                // 如果子View消费掉了Touch事件那么该方法的返回值为true,然后执行
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                        }
                    }
                }
            }

            // mFirstTouchTarget为空,表示没有子View消耗Touch事件,需要ViewGroup自己处理
            if (mFirstTouchTarget == null) {
                // 同样也会调用dispatchTransformedTouchEvent,但是传入Null,标明由父View自己处理这次Touch事件
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                /***/
            }

            /***/
        }
    }
  • 最后会调用dispatchTransformedTouchEvent
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
                                                  View child, int desiredPointerIdBits) {
        final boolean handled;

        /***/

        // 如果child == null,表明没有子View消费这次Touch事件
        if (child == null) {
            // 所以会调用super.dispatchTouchEvent,此时,ViewGroup就化身为了普通的View,它会在自己的onTouch(),onTouchEvent()中处理Touch
            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());
            }

            // 如果child不为空,表示有子View处理这次Touch事件,直接调用child的dispatchTouchEvent
            // 当然该view可能是一个View也可能是一个ViewGroup
            handled = child.dispatchTouchEvent(transformedEvent);
        }

        // Done.
        transformedEvent.recycle();
        return handled;
    }
  • 最后会递归的执行子View的这套流程,或者被ViewGroup自身拦截掉,亲自用onTouchEvent处理这次事件

  • onInterceptTouchEvent()表示是否拦截此次事件,ViewGroup的默认实现是不拦截,return false;所以我们往往可以在自定义控件中重写这个方法,来决定什么情况下拦截事件

  • 总结下来就是:

Touch事件的传递顺序为 :

Activity–>外层ViewGroup–>内层ViewGroup–>View

如果Touch事件在中间某一层被拦截了,DOWN事件将不会再传递给更底层的View

Touch事件的消费顺序为 :

View–>内层ViewGroup–>外层ViewGroup–>Activity

如果Touch事件在中间某一层被消费了,将不会再通知更上层的View,只有当所有子View都不消费Touch事件,顶层ViewGroup才会自己处理这次Touch事件。


三、滑动冲突处理

  • 引发原因:两个可以滑动的View互相嵌套,且滑动方向相同,则会产生滑动冲突。

  • 有两个比较常见的解决方案:

  • 1、在父View中准确地进行事件分发和拦截

    • 比如重写onInterceptTouchEvent()和onTouchEvent(),对事件进行正确的分配,保证在合适的时候Touch时间可以传递给子View
  • 2、使用Google在support.v4包提供的两个支持嵌套滚动的接口:onNestedScrollChild、onNestedScrollParent。(有一个例子,在我的Github上一个快速开发框架里的下拉刷新SwipeLayout中有用到,贴上地址:https://github.com/miomin/Shareward


四、需要注意的地方

  • 一个事件序列指的是从手指按下的一刻起直到手指放开,通常情况下,一个事件序列只能被一个View拦截或消耗,拦截和消耗通常都是在DOWN事件进行,如果不对DOWN时间进行消费,则不会有机会消耗后续的MOVE事件,如果消耗了DOWN事件,后续的MOVE和UP事件同样由这个View消费。(support.v4包中的NestedScrolling接口可以打破这个原则,允许多个View同时处理同一个事件序列)

  • ViewGroup的onInterceptTouchEvent默认返回false,也就是默认不会拦截事件,交给下层的View来处理。

  • View没有onInterceptTouchEvent方法,一旦有事件到达,就会调用onTouchEvent。

  • 可点击的View的onTouchEvent默认返回true,也就是消耗事件。

【自定义View系列】04--谈谈事件分发

标签:

原文地址:http://blog.csdn.net/mxm691292118/article/details/51815519

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!