【UI篇7】关于MotionEvent事件分发

460 阅读9分钟

带着百分之百的热情去做你正在做的事,久而久之也能成为一部分天赋

1. 简介

MotionEvent继承自InputEvent,通常一次完整的MotionEvent有ACTION_DOWN、ACTION_MOVE、ACTION_UP,对于单点操作通过getX()获取坐标,多点操作通过getX(pointerIndex)获取坐标

操作事件
单指操作ACTION_DOWN->ACTION_MOVE->ACTION_UP
多指操作ACTION_DOWN->ACTION_POINTER_DOWN->ACTION_MOVE->ACTION_POINTER_UP->ACTION_UP

image.png

/**
 * Constant for {@link #getActionMasked}: A pressed gesture has started, the
 * motion contains the initial starting location.
 * <p>
 * This is also a good time to check the button state to distinguish
 * secondary and tertiary button clicks and handle them appropriately.
 * Use {@link #getButtonState} to retrieve the button state.
 * </p>
 */
public static final int ACTION_DOWN             = 0;

/**
 * Constant for {@link #getActionMasked}: A pressed gesture has finished, the
 * motion contains the final release location as well as any intermediate
 * points since the last down or move event.
 */
public static final int ACTION_UP               = 1;

/**
 * Constant for {@link #getActionMasked}: A change has happened during a
 * press gesture (between {@link #ACTION_DOWN} and {@link #ACTION_UP}).
 * The motion contains the most recent point, as well as any intermediate
 * points since the last down or move event.
 */
public static final int ACTION_MOVE             = 2;

/**
 * Constant for {@link #getActionMasked}: The current gesture has been aborted.
 * You will not receive any more points in it.  You should treat this as
 * an up event, but not perform any action that you normally would.
 */
public static final int ACTION_CANCEL           = 3;

/**
 * Constant for {@link #getActionMasked}: A movement has happened outside of the
 * normal bounds of the UI element.  This does not provide a full gesture,
 * but only the initial location of the movement/touch.
 * <p>
 * Note: Because the location of any event will be outside the
 * bounds of the view hierarchy, it will not get dispatched to
 * any children of a ViewGroup by default. Therefore,
 * movements with ACTION_OUTSIDE should be handled in either the
 * root {@link View} or in the appropriate {@link Window.Callback}
 * (e.g. {@link android.app.Activity} or {@link android.app.Dialog}).
 * </p>
 */
public static final int ACTION_OUTSIDE          = 4;

/**
 * Constant for {@link #getActionMasked}: A non-primary pointer has gone down.
 * <p>
 * Use {@link #getActionIndex} to retrieve the index of the pointer that changed.
 * </p><p>
 * The index is encoded in the {@link #ACTION_POINTER_INDEX_MASK} bits of the
 * unmasked action returned by {@link #getAction}.
 * </p>
 */
public static final int ACTION_POINTER_DOWN     = 5;

/**
 * Constant for {@link #getActionMasked}: A non-primary pointer has gone up.
 * <p>
 * Use {@link #getActionIndex} to retrieve the index of the pointer that changed.
 * </p><p>
 * The index is encoded in the {@link #ACTION_POINTER_INDEX_MASK} bits of the
 * unmasked action returned by {@link #getAction}.
 * </p>
 */
public static final int ACTION_POINTER_UP       = 6;

/**
 * Constant for {@link #getActionMasked}: A change happened but the pointer
 * is not down (unlike {@link #ACTION_MOVE}).  The motion contains the most
 * recent point, as well as any intermediate points since the last
 * hover move event.
 * <p>
 * This action is always delivered to the window or view under the pointer.
 * </p><p>
 * This action is not a touch event so it is delivered to
 * {@link View#onGenericMotionEvent(MotionEvent)} rather than
 * {@link View#onTouchEvent(MotionEvent)}.
 * </p>
 */
public static final int ACTION_HOVER_MOVE       = 7;

/**
 * Constant for {@link #getActionMasked}: The motion event contains relative
 * vertical and/or horizontal scroll offsets.  Use {@link #getAxisValue(int)}
 * to retrieve the information from {@link #AXIS_VSCROLL} and {@link #AXIS_HSCROLL}.
 * The pointer may or may not be down when this event is dispatched.
 * <p>
 * This action is always delivered to the window or view under the pointer, which
 * may not be the window or view currently touched.
 * </p><p>
 * This action is not a touch event so it is delivered to
 * {@link View#onGenericMotionEvent(MotionEvent)} rather than
 * {@link View#onTouchEvent(MotionEvent)}.
 * </p>
 */
public static final int ACTION_SCROLL           = 8;

/**
 * Constant for {@link #getActionMasked}: The pointer is not down but has entered the
 * boundaries of a window or view.
 * <p>
 * This action is always delivered to the window or view under the pointer.
 * </p><p>
 * This action is not a touch event so it is delivered to
 * {@link View#onGenericMotionEvent(MotionEvent)} rather than
 * {@link View#onTouchEvent(MotionEvent)}.
 * </p>
 */
public static final int ACTION_HOVER_ENTER      = 9;

/**
 * Constant for {@link #getActionMasked}: The pointer is not down but has exited the
 * boundaries of a window or view.
 * <p>
 * This action is always delivered to the window or view that was previously under the pointer.
 * </p><p>
 * This action is not a touch event so it is delivered to
 * {@link View#onGenericMotionEvent(MotionEvent)} rather than
 * {@link View#onTouchEvent(MotionEvent)}.
 * </p>
 */
public static final int ACTION_HOVER_EXIT       = 10;

/**
 * Constant for {@link #getActionMasked}: A button has been pressed.
 *
 * <p>
 * Use {@link #getActionButton()} to get which button was pressed.
 * </p><p>
 * This action is not a touch event so it is delivered to
 * {@link View#onGenericMotionEvent(MotionEvent)} rather than
 * {@link View#onTouchEvent(MotionEvent)}.
 * </p>
 */
public static final int ACTION_BUTTON_PRESS   = 11;

/**
 * Constant for {@link #getActionMasked}: A button has been released.
 *
 * <p>
 * Use {@link #getActionButton()} to get which button was released.
 * </p><p>
 * This action is not a touch event so it is delivered to
 * {@link View#onGenericMotionEvent(MotionEvent)} rather than
 * {@link View#onTouchEvent(MotionEvent)}.
 * </p>
 */
public static final int ACTION_BUTTON_RELEASE  = 12;

2. 概要点

2.1 Touch事件对比

onTouchEvent方法ViewViewGroupActivity
dispatchTouchEvent
onInterceptTouchEvent
onTouchEvent

方法说明

方法说明
dispatchTouchEventreturn true:表示该View内部消化掉了所有事件
return false:表示事件在本层不再继续进行分发,并交由上层控件的onTouchEvent方法进行消费
return super.dispatchTouchEvent(ev):默认事件将分发给本层的事件拦截onInterceptTouchEvent方法进行处理
onInterceptTouchEventreturn true:表示将事件进行拦截,并将拦截到的事件交由本层控件的onTouchEvent进行处理
return false:表示不对事件进行拦截,事件得以成功分发到子View
return super.onInterceptTouchEvent(ev):默认表示不拦截该事件,并将事件传递给下一层View的dispatchTouchEvent
onTouchEventreturn true:表示onTouchEvent处理完事件后消费了此次事件
return fasle:表示不响应事件,那么该事件将会不断向上层View的onTouchEvent方法传递,直到某个View的onTouchEvent方法返回true
return super.dispatchTouchEvent(ev):表示不响应事件,结果与return false一样

从以上过程中可以看出,dispatchTouchEvent无论返回true还是false,事件都不再进行分发,只有当其返回super.dispatchTouchEvent(ev),才表明其具有向下层分发的愿望,但是是否能够分发成功,则需要经过事件拦截onInterceptTouchEvent的审核。事件是否向上传递处理是由onTouchEvent的返回值决定的。

1. dispatchTouchEvent:

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

2. onTouchEvent:

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

3. onInterceptTouchEvent:

//ViewGroup.java
/**
 * Implement this method to intercept all touch screen motion events.  This
 * allows you to watch events as they are dispatched to your children, and
 * take ownership of the current gesture at any point.
 *
 * <p>Using this function takes some care, as it has a fairly complicated
 * interaction with {@link View#onTouchEvent(MotionEvent)
 * View.onTouchEvent(MotionEvent)}, and using it requires implementing
 * that method as well as this one in the correct way.  Events will be
 * received in the following order:
 *
 * <ol>
 * <li> You will receive the down event here.
 * <li> The down event will be handled either by a child of this view
 * group, or given to your own onTouchEvent() method to handle; this means
 * you should implement onTouchEvent() to return true, so you will
 * continue to see the rest of the gesture (instead of looking for
 * a parent view to handle it).  Also, by returning true from
 * onTouchEvent(), you will not receive any following
 * events in onInterceptTouchEvent() and all touch processing must
 * happen in onTouchEvent() like normal.
 * <li> For as long as you return false from this function, each following
 * event (up to and including the final up) will be delivered first here
 * and then to the target's onTouchEvent().
 * <li> If you return true from here, you will not receive any
 * following events: the target view will receive the same event but
 * with the action {@link MotionEvent#ACTION_CANCEL}, and all further
 * events will be delivered to your onTouchEvent() method and no longer
 * appear here.
 * </ol>
 *
 * @param ev The motion event being dispatched down the hierarchy.
 * @return Return true to steal motion events from the children and have
 * them dispatched to this ViewGroup through onTouchEvent().
 * The current target will receive an ACTION_CANCEL event, and no further
 * messages will be delivered here.
 */
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
            && ev.getAction() == MotionEvent.ACTION_DOWN
            && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
            && isOnScrollbarThumb(ev.getX(), ev.getY())) {
        return true;
    }
    return false;
}

4. dispatchTouchEvent

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
    }

    // If the event targets the accessibility focused view and this is it, start
    // normal event dispatch. Maybe a descendant is what will handle the click.
    if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
        ev.setTargetAccessibilityFocus(false);
    }

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

        // 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();
        }

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

        // If intercepted, start normal event dispatch. Also if there is already
        // a view that is handling the gesture, do normal event dispatch.
        if (intercepted || mFirstTouchTarget != null) {
            ev.setTargetAccessibilityFocus(false);
        }

        // Check for cancelation.
        final boolean canceled = resetCancelNextUpFlag(this)
                || actionMasked == MotionEvent.ACTION_CANCEL;

        // Update list of touch targets for pointer down, if needed.
        final boolean isMouseEvent = ev.getSource() == InputDevice.SOURCE_MOUSE;
        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0
                && !isMouseEvent;
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        if (!canceled && !intercepted) {
            // If the event is targeting accessibility focus we give it to the
            // view that has accessibility focus and if it does not handle it
            // we clear the flag and dispatch the event to all children as usual.
            // We are looking up the accessibility focused host to avoid keeping
            // state since these events are very rare.
            View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                    ? findChildWithAccessibilityFocus() : null;

            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                final int actionIndex = ev.getActionIndex(); // always 0 for down
                final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                        : TouchTarget.ALL_POINTER_IDS;

                // Clean up earlier touch targets for this pointer id in case they
                // have become out of sync.
                removePointersFromTouchTargets(idBitsToAssign);

                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    final float x =
                            isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
                    final float y =
                            isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
                    // 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);

                        // If there is a view that has accessibility focus we want it
                        // to get the event first and if not handled we will perform a
                        // normal dispatch. We may do a double iteration but this is
                        // safer given the timeframe.
                        if (childWithAccessibilityFocus != null) {
                            if (childWithAccessibilityFocus != child) {
                                continue;
                            }
                            childWithAccessibilityFocus = null;
                            i = childrenCount;
                        }

                        if (!child.canReceivePointerEvents()
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }

                        newTouchTarget = getTouchTarget(child);
                        if (newTouchTarget != null) {
                            // Child is already receiving touch within its bounds.
                            // Give it the new pointer in addition to the ones it is handling.
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                            break;
                        }

                        resetCancelNextUpFlag(child);
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            // Child wants to receive touch within its bounds.
                            mLastTouchDownTime = ev.getDownTime();
                            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();
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }

                        // The accessibility focus didn't handle the event, so clear
                        // the flag and do a normal dispatch to all children.
                        ev.setTargetAccessibilityFocus(false);
                    }
                    if (preorderedList != null) preorderedList.clear();
                }

                if (newTouchTarget == null && mFirstTouchTarget != null) {
                    // Did not find a child to receive the event.
                    // Assign the pointer to the least recently added target.
                    newTouchTarget = mFirstTouchTarget;
                    while (newTouchTarget.next != null) {
                        newTouchTarget = newTouchTarget.next;
                    }
                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                }
            }
        }

        // 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;
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    if (cancelChild) {
                        if (predecessor == null) {
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
        }

        // 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);
        }
    }

    if (!handled && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
    }
    return handled;
}
  • 可以通过复写onInterceptTouchEvent(ev)方法,拦截子View的事件(即return true),把事件交给自己处理,则会执行自己对应的onTouchEvent方法。
  • 子View可以通过调用getParent().requestDisallowInterceptTouchEvent(true); 阻止ViewGroup对其MOVE或者UP事件进行拦截;
    • 一个点击事件产生后,它的传递过程如下: 
      Activity->Window->View。顶级View接收到事件之后,就会按相应规则去分发事件。如果一个View的onTouchEvent方法返回false,那么将会交给父容器的onTouchEvent方法进行处理,逐级往上,如果所有的View都不处理该事件,则交由Activity的onTouchEvent进行处理。 
  • 如果某一个View开始处理事件,如果他不消耗ACTION_DOWN事件(也就是onTouchEvent返回false),则同一事件序列比如接下来进行ACTION_MOVE,则不会再交给该View处理。
  • ViewGroup默认不拦截任何事件。 
  • 诸如TextView、ImageView这些不作为容器的View,一旦接受到事件,就调用onTouchEvent方法,它们本身没有onInterceptTouchEvent方法。正常情况下,它们都会消耗事件(返回true),除非它们是不可点击的(clickable和longClickable都为false),那么就会交由父容器的onTouchEvent处理。 
  • 点击事件分发过程如下 dispatchTouchEvent—->OnTouchListener的onTouch方法—->onTouchEvent–>OnClickListener的onClick方法。也就是说,我们平时调用的setOnClickListener,优先级是最低的,所以,onTouchEvent或OnTouchListener的onTouch方法如果返回true,则不响应onClick方法…

2.2 事件消费流程

image.png

3. 常见问题

3.1 纵向RecyclerView嵌套横向RecyclerView 划动冲突

【前置原理】

public boolean onInterceptTouchEvent(MotionEvent e) {
    if (mLayoutSuppressed) {
        // When layout is suppressed,  RV does not intercept the motion event.
        // A child view e.g. a button may still get the click.
        return false;
    }

    // Clear the active onInterceptTouchListener.  None should be set at this time, and if one
    // is, it's because some other code didn't follow the standard contract.
    mInterceptingOnItemTouchListener = null;
    if (findInterceptingOnItemTouchListener(e)) {
        cancelScroll();
        return true;
    }

    if (mLayout == null) {
        return false;
    }

    final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
    final boolean canScrollVertically = mLayout.canScrollVertically();

    if (mVelocityTracker == null) {
        mVelocityTracker = VelocityTracker.obtain();
    }
    mVelocityTracker.addMovement(e);

    final int action = e.getActionMasked();
    final int actionIndex = e.getActionIndex();

    switch (action) {
        case MotionEvent.ACTION_DOWN:
            if (mIgnoreMotionEventTillDown) {
                mIgnoreMotionEventTillDown = false;
            }
            mScrollPointerId = e.getPointerId(0);
            mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
            mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);

            if (mScrollState == SCROLL_STATE_SETTLING) {
                getParent().requestDisallowInterceptTouchEvent(true);
                setScrollState(SCROLL_STATE_DRAGGING);
                stopNestedScroll(TYPE_NON_TOUCH);
            }

            // Clear the nested offsets
            mNestedOffsets[0] = mNestedOffsets[1] = 0;

            int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
            if (canScrollHorizontally) {
                nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
            }
            if (canScrollVertically) {
                nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
            }
            startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
            break;

        case MotionEvent.ACTION_POINTER_DOWN:
            mScrollPointerId = e.getPointerId(actionIndex);
            mInitialTouchX = mLastTouchX = (int) (e.getX(actionIndex) + 0.5f);
            mInitialTouchY = mLastTouchY = (int) (e.getY(actionIndex) + 0.5f);
            break;

        case MotionEvent.ACTION_MOVE: {
            final int index = e.findPointerIndex(mScrollPointerId);
            if (index < 0) {
                Log.e(TAG, "Error processing scroll; pointer index for id "
                        + mScrollPointerId + " not found. Did any MotionEvents get skipped?");
                return false;
            }

            final int x = (int) (e.getX(index) + 0.5f);
            final int y = (int) (e.getY(index) + 0.5f);
            if (mScrollState != SCROLL_STATE_DRAGGING) {
                final int dx = x - mInitialTouchX;
                final int dy = y - mInitialTouchY;
                boolean startScroll = false;
                if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
                    mLastTouchX = x;
                    startScroll = true;
                }
                if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
                    mLastTouchY = y;
                    startScroll = true;
                }
                if (startScroll) {
                    setScrollState(SCROLL_STATE_DRAGGING);
                }
            }
        }
        break;

        case MotionEvent.ACTION_POINTER_UP: {
            onPointerUp(e);
        }
        break;

        case MotionEvent.ACTION_UP: {
            mVelocityTracker.clear();
            stopNestedScroll(TYPE_TOUCH);
        }
        break;

        case MotionEvent.ACTION_CANCEL: {
            cancelScroll();
        }
    }
    return mScrollState == SCROLL_STATE_DRAGGING;
}

RecyclerView接收到的滑动只要滑动的距离绝对值大于阈值mTouchSlop,就会触发拦截

【分析方法】 此处比较重要的是弄清楚滑动的需求,根据事件分发流程的逻辑,在dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent中对事件进行拦截分发处理,使之满足需求按照设想的逻辑运行。

【解决方法】

  1. 纵向RecyclerView
/**
 * 纵向RecyclerView
 */
class ParentRecyclerView(context: Context, attrs: AttributeSet) : RecyclerView(context, attrs){
    companion object {
        private const val TAG = "ParentRecyclerView"
    }
    private var mInitialTouchX = 0
    private var mInitialTouchY = 0
    private var mScrollPointerId = -1
    private var mTouchSlop = 0

    init {
        mTouchSlop = ViewConfiguration.get(getContext()).scaledTouchSlop
    }

    override fun setScrollingTouchSlop(slopConstant: Int) {
        val vc: ViewConfiguration = ViewConfiguration.get(this.context)
        when (slopConstant) {
            TOUCH_SLOP_DEFAULT -> {
                this.mTouchSlop = vc.scaledTouchSlop
            }
            TOUCH_SLOP_PAGING -> {
                this.mTouchSlop = vc.scaledPagingTouchSlop
            }
            else -> Log.w(
                TAG,
                "setScrollingTouchSlop(): bad argument constant $slopConstant; using default value"
            )
        }
        super.setScrollingTouchSlop(slopConstant)
    }

    override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
        when (e.actionMasked) {
            MotionEvent.ACTION_DOWN -> {
                mScrollPointerId = e.getPointerId(0)
                this.mInitialTouchX = (e.x + 0.5f).toInt()
                this.mInitialTouchY = (e.y + 0.5f).toInt()
                return super.onInterceptTouchEvent(e)
            }
            MotionEvent.ACTION_MOVE -> {
                val index = e.findPointerIndex(this.mScrollPointerId)
                if (index < 0) {
                    Log.e(
                        TAG,
                        "Error processing scroll; pointer index for id " + this.mScrollPointerId + " not found. Did any MotionEvents get skipped?"
                    )
                    return false
                }
                val x = (e.getX(index) + 0.5f).toInt()
                val y = (e.getY(index) + 0.5f).toInt()
                val dx = x - this.mInitialTouchX
                val dy = y - this.mInitialTouchY
                
                //横向划动, 停止滚动,不拦截事件,将事件传给子View
                if (abs(dx) > mTouchSlop && abs(dx) > abs(dy)) {
                    stopScroll()
                    return false
                }

                if (scrollState != SCROLL_STATE_DRAGGING) {
                    var startScroll = false

                    if (abs(dy) > this.mTouchSlop && abs(dy) > abs(dx)) {
                        startScroll = true
                    }
                    return startScroll && super.onInterceptTouchEvent(e)
                }
            }

            MotionEvent.ACTION_UP -> {
            }
            MotionEvent.ACTION_CANCEL -> {
            }
        }

        return super.onInterceptTouchEvent(e)
    }

    override fun onTouchEvent(e: MotionEvent): Boolean {
        when (e.actionMasked) {
            MotionEvent.ACTION_DOWN -> {
                mScrollPointerId = e.getPointerId(0)
                this.mInitialTouchX = (e.x + 0.5f).toInt()
                this.mInitialTouchY = (e.y + 0.5f).toInt()
            }

            MotionEvent.ACTION_MOVE -> {
                val index = e.findPointerIndex(this.mScrollPointerId)
                if (index < 0) {
                    Log.e(
                        TAG,
                        "Error processing scroll; pointer index for id " + this.mScrollPointerId + " not found. Did any MotionEvents get skipped?"
                    )
                    return false
                }
                val x = (e.getX(index) + 0.5f).toInt()
                val y = (e.getY(index) + 0.5f).toInt()
                val dx = x - this.mInitialTouchX
                val dy = y - this.mInitialTouchY
                
                //横向划动则不拦截
                if (abs(dx) > mTouchSlop && abs(dx) > abs(dy)) {
                    stopScroll()
                    return false
                }

                if (scrollState != SCROLL_STATE_DRAGGING) {
                    var startScroll = false

                    if (abs(dy) > this.mTouchSlop && abs(dy) > abs(dx)) {
                        startScroll = true
                    }
                    return startScroll && super.onInterceptTouchEvent(e)
                }
            }

            MotionEvent.ACTION_UP -> {
            }
            MotionEvent.ACTION_CANCEL -> {
            }
        }
        return super.onTouchEvent(e)
    }
}
  1. 横向RecyclerView
/**
 * RecyclerView中嵌套的横向RecyclerView
 */
class ChildRecyclerView(context: Context, attrs: AttributeSet) : RecyclerView(context, attrs) {
    companion object {
        private const val TAG = "ChildRecyclerView"
    }
    private var mInitialTouchX = 0
    private var mInitialTouchY = 0
    private var mScrollPointerId = -1
    private var mTouchSlop = 0

    init {
        val vc = ViewConfiguration.get(context)
        mTouchSlop = vc.scaledTouchSlop
    }

    override fun setScrollingTouchSlop(slopConstant: Int) {
        val vc: ViewConfiguration = ViewConfiguration.get(this.context)
        when (slopConstant) {
            TOUCH_SLOP_DEFAULT -> {
                this.mTouchSlop = vc.scaledTouchSlop
            }
            TOUCH_SLOP_PAGING -> {
                this.mTouchSlop = vc.scaledPagingTouchSlop
            }
            else -> Log.w(
                TAG,
                "setScrollingTouchSlop(): bad argument constant $slopConstant; using default value"
            )
        }
        super.setScrollingTouchSlop(slopConstant)
    }

    override fun dispatchTouchEvent(e: MotionEvent): Boolean {
        when (e.actionMasked) {
            MotionEvent.ACTION_DOWN -> {
                mScrollPointerId = e.getPointerId(0)
                this.mInitialTouchX = (e.x + 0.5f).toInt()
                this.mInitialTouchY = (e.y + 0.5f).toInt()
                Log.i(
                    TAG,
                    "dispatchTouchEvent ACTION_DOWN mInitialTouchX:$mInitialTouchX, mInitialTouchY:$mInitialTouchY"
                )
            }
            MotionEvent.ACTION_MOVE -> {
                val index = e.findPointerIndex(this.mScrollPointerId)
                if (index < 0) {
                    Log.e(
                        TAG,
                        "Error processing scroll; pointer index for id " + this.mScrollPointerId + " not found. Did any MotionEvents get skipped?"
                    )
                    return false
                }
                val x = (e.getX(index) + 0.5f).toInt()
                val y = (e.getY(index) + 0.5f).toInt()

                val dx = x - this.mInitialTouchX
                val dy = y - this.mInitialTouchY

                Log.i(
                    TAG,
                    "dispatchTouchEvent ACTION_MOVE, dx:$dx, dy:$dy, mTouchSlop:$mTouchSlop"
                )
                //如果纵向划动,则停止滚动,不再分发
                if (abs(dy) > this.mTouchSlop && abs(dy) > abs(dx)) {
                    stopScroll()
                    return false
                }
            }
        }
        return super.dispatchTouchEvent(e)
    }
}