结合实例,一篇文章彻底理清OnTouchListener、onT

2020-02-03  本文已影响0人  Ronnie_火老师

本文将结合具体实例:通过微信聊天页面的交互方式,分析实现方法,进而搞清 OnTouchListener、onTouchEvent、onClick、clickable的关系。

说明 1:本文默认读者已经基本了解事件分发机制,主要是 dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent 流程。

说明 2:文中代码以 Android SDK 23 为参考,如果想要亲自调试一下,可以将 compileSdkVersion 设置为 23,并且安装 Nexus 模拟器,模拟器系统版本要与 compileSdkVersion 一致。最好不要使用真机调试,即使系统版本对应,一般也会因为手机厂商对原生系统的改动,导致调试时代码行数不对应。

微信聊天页面示例

交互分析

image

实现方法

View 的事件传递

View.dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent event) {

        // 省略部分代码
        
        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)) { // 注释 1
                result = true;
            }

            if (!result && onTouchEvent(event)) { // 注释 2
                result = true;
            }
        }

        // 省略部分代码

        return result;
    }

ViewGroup.dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent ev) {
        
        // 省略代码 ...

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

            // 当为 ACTION_DOWN 时,说明是一个事件序列的开始,会调用 resetTouchState 方法重置状态
            // Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

            // mFirstTouchTarget 是用来记录接收该事件的子 View 的,当为 null 时,说明还没有子 View 接收该事件序列,不为空时,说明已经有子 View 接收了该事件,事件序列的其他事件就可以直接传给该 View。
            // 这一段代码主要是检查要不要对事件进行拦截:onInterceptTouchEvent
            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }

            // 省略代码 ...
            
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            if (!canceled && !intercepted) {

               // 省略代码 ...

                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                     
                     // 省略代码 ...

                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        // 下面会循环遍历子 View,找到可以接收事件的那个
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);

                            // 省略代码 ...
                            newTouchTarget = getTouchTarget(child);
                            // 省略代码 ...
                            // dispatchTransformedTouchEvent 可以看成将事件传递给参数 child,即调用了 child 的 dispatchTouchEvent 方法
                            // 如果child 是 ViewGroup,这个过程相当于递归调用;如果 child 是 View,则调用我们上一小节分析的方法。
                            // 最终的返回值也即 child 的 dispatchTouchEvent 的返回值,如果是 true,说明该 child (或其子 View)消费了事件
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                // 省略代码 ...
                                // 调用 addTouchTarget 方法,将找到的接收事件的子 View 保存起来,也会给 mFirstTouchTarget 赋值
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                            // 省略代码 ...
                        }
                    }
                     // 省略代码 ...
                }
            }

            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) { // 注释 3
                // mFirstTouchTarget 为空,说明没找到接收事件的子 View
                // 此时调用 dispatchTransformedTouchEvent 方法,传入 View 参数为 null 时,会调用 super.dispatchTouchEvent
                // 即调用到上面分析的 View 的 dispatchTouchEvent,以确定是否由当前 View 消费事件
                // 这就是事件分发中常见的结论:“事件由父 View 向下传递,如果没有子 View 消费事件,事件又会依次向上传递”
                // 实际上并不是向上传递(也就是不是直接调用的 parent.dispatchTouchEvent)而是ViewGroup 先调子 View 的 dispatchTouchEvent 方法,如果没有接收的,再调用自己的 dispatchTouchEvent 方法,以达到“向上传递”的效果
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                // 注释 4 后面讲解
            }

            // 当为 ACTION_UP 事件时,说明事件序列结束,也会调用 resetTouchState 方法重置状态
            // Update list of touch targets for pointer up or cancel, if needed.
            if (canceled
                    || actionMasked == MotionEvent.ACTION_UP
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                resetTouchState();
            } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
                final int actionIndex = ev.getActionIndex();
                final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
                removePointersFromTouchTargets(idBitsToRemove);
            }
        }

        // 省略代码 ...
        return handled;
    }

将理论应用到需求中

View.onTouchEvent

public boolean onTouchEvent(MotionEvent event) {
    // 省略代码 ...
    final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
    // 省略代码 ...
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    // // 省略代码 ...
                    break;

                case MotionEvent.ACTION_DOWN:
                    // 省略代码 ...
                    break;

                case MotionEvent.ACTION_CANCEL:
                    // 省略代码 ...
                    break;

                case MotionEvent.ACTION_MOVE:
                    // 省略代码 ...
                    break;
            }

            return true;
        }

    return false;
}

最终方案

知识拓展

          // Dispatch to touch targets.
          if (mFirstTouchTarget == null) {
              // No touch targets so treat this as an ordinary view.
              handled = dispatchTransformedTouchEvent(ev, canceled, null,
                      TouchTarget.ALL_POINTER_IDS);
          } else {
              // Dispatch to touch targets, excluding the new touch target if we already
              // dispatched to it.  Cancel touch targets if necessary.
              TouchTarget predecessor = null;
              TouchTarget target = mFirstTouchTarget;
              while (target != null) {
                  final TouchTarget next = target.next;
                  if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                      handled = true;
                  } else {
                      final boolean cancelChild = resetCancelNextUpFlag(target.child)
                              || intercepted; // 注释 4
                      if (dispatchTransformedTouchEvent(ev, cancelChild,
                              target.child, target.pointerIdBits)) {
                          handled = true;
                      }

参考

上一篇下一篇

猜你喜欢

热点阅读