touch事件分发、处理、冲突解决

本文基于api30, 学习源码中的触摸事件。

本文学习单点触摸,不包含多点触摸,不包含嵌套滑动

首先我们看View类,作为视图树的叶节点,View只有处理逻辑,没有分发逻辑

View

touch事件入口是dispatchTouchEvent,这里只关注主要逻辑:

public boolean dispatchTouchEvent(MotionEvent event) {
    ...
    boolean result = false;
    ...
    if (onFilterTouchEventForSecurity(event)) {
       //一般onFilterTouchEventForSecurity会返回true
        ...
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                //OnTouchListener#onTouch事件处理
                && li.mOnTouchListener.onTouch(this, event)) {
            //OnTouchListener#onTouch返回true,则最终结果为true
            result = true;
        }
        //只有OnTouchListener#onTouch返回false的情况下,才会调用onTouchEvent方法
        if (!result && onTouchEvent(event)) {
            //onTouchEvent返回true,则最终结果为true
            result = true;
        }
    ...
    return result;
}

onTouchEvent不是我们分析的重点。只需要知道,click、longClick事件是在onTouchEvent中处理的即可
也就是说,如果OnTouchListener#onTouch返回true,那么将不会产生click、longClick事件

ViewGroup

ViewGroup除了本身的事件处理外,还负责事件分发。事件分发需要区分是否是MotionEvent.ACTION_DOWN事件。下面依次分析

MotionEvent.ACTION_DOWN事件分发流程

依然从入口dispatchTouchEvent开始,该方法内的逻辑可以分为三部分:检查是否拦截、事件分发逻辑、事件处理逻辑
列出主要代码:

public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
       //一般onFilterTouchEventForSecurity会返回true
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;

        if (actionMasked == MotionEvent.ACTION_DOWN) {
            //这里清除一些标志位,如通过调用requestDisallowInterceptTouchEvent方法设置的标志位
            cancelAndClearTouchTargets(ev);
            resetTouchState();
        }


        //一、检查是否需要被拦截
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            //单点触摸,Down事件一定走这里
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                //单点Down事件一定走这里,因为上面已经清空了FLAG_DISALLOW_INTERCEPT标志位
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action); 
            } else {
                ...
            }
        } else {
            ...
        }
        ...


        //二、事件分发逻辑
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        if (!canceled && !intercepted) {
            //事件分发,如果intercepted为true,则不会分发
            ...
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                //这个if才是真正的Down事件分发
               ...
                if (newTouchTarget == null && childrenCount != 0) {
                    //有子View,进行分发
                    ...
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        //对子View进行遍历,查找第一个消费Down事件的子View
                        ...
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            //有子View消费了Down事件
                            ...
                            //保存消费Down事件的子View到mFirstTouchTarget、newTouchTarget 
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;
                            //已经找到目标,退出for循环
                            break;
                        }//if
                        ...
                    }//for
                    ...
                }//if(newTouchTarget == null && childrenCount != 0)
                ...
            }//if(actionMasked == MotionEvent.ACTION_DOWN
            ...
        }//if (!canceled && !intercepted) 


        //三、事件处理逻辑
        if (mFirstTouchTarget == null) {
           //没有需要处理事件的子View,则自己处理
           handled = dispatchTransformedTouchEvent(ev, canceled, null,
                   TouchTarget.ALL_POINTER_IDS);
        } else {
            //有处理事件的子View
            ...
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                //单点触摸,只循环一次
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    //alreadyDispatchedToNewTouchTarget 在上面被设置为true,并且mFirstTouchTarget==newTouchTarget
                    //Down事件在分发的逻辑中已经被处理,这里不再额外处理
                    handled = true;
                } else {
                    ...
                }
                ...
            }//while
        }//else
        ...
    }//if (onFilterTouchEventForSecurity(ev))
    ...
    return handled;
}

备注几个用到的方法

addTouchTarget保存消费Down事件的子View

private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    //链表是为了处理多点触摸事件
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}

事件的分发和处理逻辑都会调用到dispatchTransformedTouchEvent,Down事件中,该方法重要逻辑如下:

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;
    ...
    if (child == null) {
        //自身处理
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        //分发逻辑中,child不为空
        ...
        handled = child.dispatchTouchEvent(transformedEvent);
    }
    ...
    return handled;
}

可以看到,该方法会调用child.dispatchTouchEvent判断子VIew是否需要消费Down事件。可以理解为该方法是真正的事件处理

非Down事件处理

这里以Move事件为例(其他事件区别不大)

从上面的分析可以知道,只有Down事件才有分发逻辑。
若有子View消费了Down事件,即表示该子View对触摸事件感兴趣,那么会将该子View保存到mFirstTouchTarget
非Down事件会直接交给自身或者mFirstTouchTarget处理,不再遍历子View进行分发

这样非Down事件就只剩下了检查是否拦截、事件处理逻辑,如下:

public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
       //一般onFilterTouchEventForSecurity会返回true
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;

        //一、检查是否需要被拦截
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            //有需要处理触摸事件的子View
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                //子View没有调用requestDisallowInterceptTouchEvent
                //是否拦截取决于onInterceptTouchEvent
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action); 
            } else {
                //子View有调用requestDisallowInterceptTouchEvent
                //则不拦截
                intercepted = false;
            }
        } else {
            //没有需要处理触摸事件的子View
            //则拦截
            intercepted = true;
        }

        ...//不再分发

        //二、事件处理逻辑
        if (mFirstTouchTarget == null) {
           //没有需要处理事件的子View,则自己处理
           handled = dispatchTransformedTouchEvent(ev, canceled, null,
                   TouchTarget.ALL_POINTER_IDS);
        } else {
            //有处理事件的子View
           ...
           TouchTarget target = mFirstTouchTarget;
           while (target != null) {
               //单点触摸,只循环一次
               final TouchTarget next = target.next;
               if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                   //alreadyDispatchedToNewTouchTarget始终为false,不再走这里
                   ...
               } else {
                   //若拦截,则cancelChild 为true
                   final boolean cancelChild = resetCancelNextUpFlag(target.child)
                           || intercepted;
                   if (dispatchTransformedTouchEvent(ev, cancelChild,
                           target.child, target.pointerIdBits)) {
                       //后面分析这个方法
                       handled = true;
                   }
                    if (cancelChild) {
                        //若被拦截,走这里
                        if (predecessor == null) {
                            //将mFirstTouchTarget置为空
                            //单点触摸,next为空
                            mFirstTouchTarget = next;
                        } else {
                            ...
                        }
                        ...
                    }
                }
                   ...
               }//else
               ...
            }//while
        }//else
        ...
    }//if (onFilterTouchEventForSecurity(ev)) 
    ...
    return handled;
}

非Down事件的事件处理dispatchTransformedTouchEvent如下:

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        //被拦截时,cancel为true
        //将事件类型修改为ACTION_CANCEL后下发处理
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        //还原事件的原本类型
        event.setAction(oldAction);
        return handled;
    }
    
    //未被拦截时,cancel为false,继续执行
    ...

    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        ...
        handled = child.dispatchTouchEvent(transformedEvent);
    }
    ...

    return handled
}

以上便是触摸事件的分发、处理逻辑。总结如下:

  • Down事件,有拦截、分发、处理逻辑
    1. 拦截逻辑:一般不拦截Down事件,否则子View始终收不到触摸事件
    2. 分发逻辑:查找对触摸事件感兴趣的子View,若找到,保存在mFirstTouchTarget
    3. 处理逻辑:
      1. mFirstTouchTarget不为空:已经在分发时通过dispatchTransformedTouchEvent交给子View处理过了;
      2. 否则通过dispatchTransformedTouchEvent交给自身的super.dispatchTouchEvent处理
  • 非Down事件,有拦截、处理逻辑
    1. 拦截逻辑:
      1. mFirstTouchTarget为空:拦截
      2. mFirstTouchTarget不为空,且子View调用parent#requestDisallowInterceptTouchEvent(true):不拦截
      3. 否则由自身的onInterceptTouchEvent决定是否拦截
    2. 处理逻辑:
      1. mFirstTouchTarget为空:通过dispatchTransformedTouchEvent交给自身的super.dispatchTouchEvent处理
      2. mFirstTouchTarget不为空,且不拦截:通过dispatchTransformedTouchEvent交给子View处理
      3. mFirstTouchTarget不为空,且拦截:将事件类型临时修改为Cancel,交给子View处理,同时mFirstTouchTarget被置为空!
  • 如果子View处理了Down事件,那么可能不会收到Move事件(比如被parent拦截)。但一定会收到Up和Cancel其中一个事件。

冲突解决

这里的冲突一般是指Move事件

View的OnTouchListener#onTouch返回true时,onClick不会执行,也可以算作是事件冲突

解决Move事件的冲突,可以分为两种方式:

  • 子View处理
  • 父View处理

子View处理

即通过调用parent#requestDisallowInterceptTouchEvent(true)方法,来允许或者禁止parent拦截Move事件
android原生widget,基本都是子View处理

父View处理

即在onInterceptTouchEvent方法中,决定何时拦截或不拦截Move事件

以上就是两种解决Move事件冲突的方式,不论哪一种,一旦Move事件被父View处理,那么子View就再也收不到了
想要解决这个问题,就需要使用android提供的嵌套滑动方案了

你可能感兴趣的:(touch事件分发、处理、冲突解决)