【自定义View系列】事件分发(带代码逻辑图)

事件分发机制代码逻辑图

看大图

  本部分介绍View的一个核心知识点:事件分发机制。事件分发机制不仅仅是核心知识点更是难点,不少初学者甚至中级开发者面对这个问题都会觉得困惑。另外,View的另一大难题滑动冲突,它的解决方法的理论基础就是事件分发机制,因此掌握好View的事件分发机制是十分重要的。

一.为什么需要事件分发机制

http://blog.csdn.net/aigestudio/article/details/44260301
http://blog.csdn.net/aigestudio/article/details/44746625

  aige已经写过两篇相关的博客,非常清晰的解释了为何有事件分发机制,这里就不再赘述了。其实,不止android,一切有界面的系统都有自己的事件分发机制,如windows系统、osx系统,ios系统等等。

二.事件分发机制的类比案例

  假设点击事件是一个难题,经理把这个难题分给组长去处理,组长又分给程序员处理,程序员解决不了,只能交给组长,组长来解决,组长解决不了,那只能继续往上交给经理,经理来解决。从这个角度看,事件分发机制还是很贴近现实的。

三.MotionEvent

常量:
public static final int ACTION_DOWN = 0;单点触摸按下动作
public static final int ACTION_UP = 1;单点触摸离开动作
public static final int ACTION_MOVE = 2;触摸点移动动作
public static final int ACTION_CANCEL = 3;触摸动作取消
ACTION_MASK = 0X000000ff 动作掩码 为了得到action

方法:
getAction() 返回值:int 使用掩码后可获得上面的对应常量值
getX(),getY() 相对当前view的坐标
getRawX(),getRawY() 相对屏幕的坐标

四.依照惯例,我们仍然从栈帧分析入手

  栈帧清晰的帮我们列出了方法的调用流程,是阅读源码非常主要的工具。

1.栈帧

【自定义View系列】事件分发(带代码逻辑图)_第1张图片

布局:


<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.ht.androidstudy.view.DebugTextView
        android:id="@+id/debugView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="dddd"
        />


LinearLayout>

DebugTextView:

public class DebugTextView extends TextView {

    public DebugTextView(Context context) {
        super(context);
    }

    public DebugTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        Log.d("dd", "");
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }


    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        Log.d("dd", "");
        super.onLayout(changed, left, top, right, bottom);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        Log.d("dd", "");
        super.onDraw(canvas);
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d("dd", "");
        return super.onTouchEvent(event);
    }
}

  onTouchEvent方法的 Log.d(“dd”, “”);加上断点debug后的栈帧如上图,我们抛开系统对UI的影响,只分析LinearLayout和DebugTextView。

2.Activity对点击事件的分发过程

  点击事件用MotionEvent来表示,当一个点击操作发生时,事件最先传递给当前Activity,由Activity的dispatchtouchevent来进行事件派发,具体的工作是由Activity内部的Window完成的。Window会将事件传递给DecorView,DecorView一般就是当前界面的底层容器,即SetContentView所设置的View的父容器),通过Activity.getWindow().getDecorView()可以获得。我们从Activity的dispatchtouchevent开始分析。源码如下:

    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        // 主要是走的这个分支,getWindow就是PhoneWindow的一个对象
        // PhoneWindow里的superDispatchTouchEvent走的是
        // mDecor.superDispatchTouchEvent(event);
        // 所以最终走到了mDecor的superDispatchTouchEvent方法
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

mDecor的superDispatchTouchEvent源码如下:

        public boolean superDispatchTouchEvent(MotionEvent event) {
            return super.dispatchTouchEvent(event);
        }

因为mDecor继承自FrameLayout,所以走的是FrameLayout的dispatchTouchEvent方法。

3.顶级View(即LinearLayout)对点击事件的分发过程

我们跳过mDecor的分发看LinearLayout
LinearLayout的dispatchTouchEvent源码如下:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ......
    ......
    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.

        //viewgroup在两种情况下会判断是否拦截当前事件:事件类型
        //ACTION_DOWN或者mFirstTouchTarget!=null。mFirstTouchTarget为
        //viewgroup成功处理事件的元素。这里可以验证一条:当viewgroup拦截了
        //action_down事件,则mFirstTouchTarget==null,那么
        //actionMasked == MotionEvent.ACTION_DOWN||mFirstTouchTarget != null
        //为false,则以后的move和up事件都不会再调用onInterceptTouchEvent方法

        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {

                //FLAG_DISALLOW_INTERCEPT标记位,这个标记位是通过
                //requestDisallowInterceptTouchEvent方法来设置的,
                //一般用于子view中,FLAG_DISALLOW_INTERCEPT一旦设置了,
                //ViewGroup将无法拦截除了ACTION_DOWN以外的其他点击事件,
                //为什么说对ACTION_DOWN无效呢,因为viewroup在分发事件的时候,
                //如果是ACTION_DOWN,就会重置这个FLAG_DISALLOW_INTERCEPT标记位
                //(在resetTouchState重置的),当面对ACTION_DOWN事件的时候,
                //viewgroup总会调用自己的onInterceptTouchEvent方法来询问自己
                //是否拦截事件。因此,子view调用requestDisallowInterceptTouchEvent
                //方法并不能viewgroup对ACTION_DOWN事件的处理。

            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 split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        if (!canceled && !intercepted) {

            // If the event is targeting accessiiblity 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);

                //viewgroup不拦截事件的时候,事件会向下分发交由它的子view进行处理。
                //逻辑:首先会遍历ViewGroup的所有子元素,然后判断子元素是否能够接受
                //到点击事件。是否能接收到点击事件主要由两点来衡量:子元素是否在
                //播动画和点击事件的坐标是否落在子元素的区域内。如果某个子元素满足
                //这两个条件,那么事件就会传递给它处理。可以看到,
                //dispatchTransformedTouchEvent实际上调用的就是子元素的
                //disPatchTouchEvent方法,在它内部有如下一段内容,因为这里传递的
                //child不为null,所以他会直接调用子元素的disPatchTouchEvent方法,
                //这样事件就交由子元素处理了,从而完成一轮事件分发。

                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    final float x = ev.getX(actionIndex);
                    final float y = ev.getY(actionIndex);
                    // Find a child that can receive the event.
                    // Scan children from front to back.
                    final ArrayList 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 - 1;
                        }

                        if (!canViewReceivePointerEvents(child)
                                || !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();
                            //如果dispatchTransformedTouchEvent返回TRUE,
                            //即子元素的dispatchEvent返回true,这时我们不用
                            //考虑事件在子元素内部是怎么分发的,那么
                            //mFirstTouchTarget就会被赋值同时跳出for循环。
                            //如果dispatchTransformedTouchEvent返回false,
                            //那么就会继续去遍历下一个元素。
                            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;
}

具体的解释看代码注释即可。

3.LinearLayout分发就到了DebugTextView的dispatchTouchEvent方法

走的是View的dispatchTouchEvent方法

    public boolean dispatchTouchEvent(MotionEvent event) {
        // View对点击事件的处理过程就比较简单了,因为View是一个单独的元素,他没有子元素
        // 因此无法向下传递事件,所以只能自己处理事件。
        // 从下面可见,View对点击事件的处理过程,它会首先判断有没有设置onTouchListener,
        // 如果onTouchListener中的onTouch方法返回true,那么onTouchEvent就不会被调用,可见
        // onTouchListener中的onTouch方法优先级高于onTouchEvent。
        // 这样做的好处是方便我们在外界处理点击事件,就像自定义view总会留出setxxx接口一样
        // 其实这里是同样的道理
        if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
                mOnTouchListener.onTouch(this, event)) {
            return true;
        }
        return onTouchEvent(event);
    }

4.继续看DebugTextView 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();

        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return (((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
        }
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

        //只要view的CLICKABLE和LONG_CLICKABLE是true,那么它就会消耗这个事件,即
        //onTouchEvent返回true,不管它是不是disable状态(看上面return代码)。
        //然后就是当ACTION_UP事件发生的时候,就会触发performClick方法,如果view
        //设置了onclicklistener,那么performClick方法内部就会调用它的onClick方法。
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            setPressed(true, x, y);
                       }

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }

                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }

                        if (prepressed) {
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }

                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_DOWN:
                    mHasPerformedLongPress = false;

                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

                    // Walk up the hierarchy to determine if we're inside a scrolling container.
                    boolean isInScrollingContainer = isInScrollingContainer();

                    // For views inside a scrolling container, delay the pressed feedback for
                    // a short period in case this is a scroll.
                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        setPressed(true, x, y);
                        checkForLongClick(0, x, y);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    setPressed(false);
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_MOVE:
                    drawableHotspotChanged(x, y);

                    // Be lenient about moving outside of buttons
                    if (!pointInView(x, y, mTouchSlop)) {
                        // Outside button
                        removeTapCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            // Remove any future long press/tap checks
                            removeLongPressCallback();

                            setPressed(false);
                        }
                    }
                    break;
            }

            return true;
        }

        return false;
    }

至此,对这个栈帧的分析已经结束,下面会做提取结论,做以总结。

五.总结

1.隧道式向下分发,然后冒泡式向上处理。

  当一个点击事件发生的时候,它的传递过程遵循如下顺序:Activity-Window-ViewGroup-View。
  处理按照是否消费的返回值,从下到上返回,即如果View的onTouchEvent返回false,将会向上传给它的parent的ViewGroup,如果ViewGroup的onTouchEvent也返回false,,将会一直向上返回到Activity,即activity的onTouchEvent方法会被调用。
  activity和window的拦截方法我们一般不修改,因为不具备应用价值。

2.ViewGRoup的事件传递机制

1)dispatchTouchEvent

 @Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ......
    ......
    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.

        //viewgroup在两种情况下会判断是否拦截当前事件:事件类型
        //ACTION_DOWN或者mFirstTouchTarget!=null。mFirstTouchTarget为
        //viewgroup成功处理事件的元素。这里可以验证一条:当viewgroup拦截了
        //action_down事件,则mFirstTouchTarget==null,那么
        //actionMasked == MotionEvent.ACTION_DOWN||mFirstTouchTarget != null
        //为false,则以后的move和up事件都不会再调用onInterceptTouchEvent方法

        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {

                //FLAG_DISALLOW_INTERCEPT标记位,这个标记位是通过
                //requestDisallowInterceptTouchEvent方法来设置的,
                //一般用于子view中,FLAG_DISALLOW_INTERCEPT一旦设置了,
                //ViewGroup将无法拦截除了ACTION_DOWN以外的其他点击事件,
                //为什么说对ACTION_DOWN无效呢,因为viewroup在分发事件的时候,
                //如果是ACTION_DOWN,就会重置这个FLAG_DISALLOW_INTERCEPT标记位
                //(在resetTouchState重置的),当面对ACTION_DOWN事件的时候,
                //viewgroup总会调用自己的onInterceptTouchEvent方法来询问自己
                //是否拦截事件。因此,子view调用requestDisallowInterceptTouchEvent
                //方法并不能viewgroup对ACTION_DOWN事件的处理。

            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 split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        if (!canceled && !intercepted) {

            // If the event is targeting accessiiblity 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);

                //viewgroup不拦截事件的时候,事件会向下分发交由它的子view进行处理。
                //逻辑:首先会遍历ViewGroup的所有子元素,然后判断子元素是否能够接受
                //到点击事件。是否能接收到点击事件主要由两点来衡量:子元素是否在
                //播动画和点击事件的坐标是否落在子元素的区域内。如果某个子元素满足
                //这两个条件,那么事件就会传递给它处理。可以看到,
                //dispatchTransformedTouchEvent实际上调用的就是子元素的
                //disPatchTouchEvent方法,在它内部有如下一段内容,因为这里传递的
                //child不为null,所以他会直接调用子元素的disPatchTouchEvent方法,
                //这样事件就交由子元素处理了,从而完成一轮事件分发。

                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    final float x = ev.getX(actionIndex);
                    final float y = ev.getY(actionIndex);
                    // Find a child that can receive the event.
                    // Scan children from front to back.
                    final ArrayList 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 - 1;
                        }

                        if (!canViewReceivePointerEvents(child)
                                || !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();
                            //如果dispatchTransformedTouchEvent返回TRUE,
                            //即子元素的dispatchEvent返回true,这时我们不用
                            //考虑事件在子元素内部是怎么分发的,那么
                            //mFirstTouchTarget就会被赋值同时跳出for循环。
                            //如果dispatchTransformedTouchEvent返回false,
                            //那么就会继续去遍历下一个元素。
                            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;
}

2)onInterceptTouchEvent:是否拦截

如果父布局在拦截方法中重写了down\move\up这些事件的拦截,那么每次都会走down\move\up的拦截,然后才会走ontouchevent。

3)onTouchEvent

采用的是View的onEvent方法。长按事件的响应,点击事件的响应都在这个方法中。

3.View的事件传递机制

1)dispatchTouchEvent

    public boolean dispatchTouchEvent(MotionEvent event) {
        // View对点击事件的处理过程就比较简单了,因为View是一个单独的元素,他没有子元素
        // 因此无法向下传递事件,所以只能自己处理事件。
        // 从下面可见,View对点击事件的处理过程,它会首先判断有没有设置onTouchListener,
        // 如果onTouchListener中的onTouch方法返回true,那么onTouchEvent就不会被调用,可见
        // onTouchListener中的onTouch方法优先级高于onTouchEvent。
        // 这样做的好处是方便我们在外界处理点击事件,就像自定义view总会留出setxxx接口一样
        // 其实这里是同样的道理
        if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
                mOnTouchListener.onTouch(this, event)) {
            return true;
        }
        return onTouchEvent(event);
    }

可以看到dispatchTouchEvent方法的返回值是受onInterceptTouchEvent和onTouchEvent影响的,并不是彼此不相干的。

2)无这个方法onInterceptTouchEvent

3)onTouchEvent,上面已经分析过

长按事件的响应,点击事件的响应都在这个方法中。

4.处理滑动冲突

1)外部拦截法

重写ViewGroup的onInterceptTouchEvent方法.
在onDown不拦截(否则无法再向下传递),需要拦截其他事件时返回true
伪代码:

public boolean onInterceptTouchEvent(MotionEvent ev){
boolean intercepted=false;
switch(ev.getAction()){
case MotionEvent.ACTION_DOWN:
intercepted=false;
break;
case MotionEvent.ACTION_MOVE:
if(xxx) intercepted=true;  //拦截
else intercepted=false; //不拦截
break;
case MotionEvent.ACTION_UP:
intercepted=false;
break;
}
return intercepted;
}

2)内部拦截法.

在view中控制viewGroup来实现,view通过改变FLAG_DISALLOW_INTERCEPT的状态来控制ViewGroup是否拦截后续事件
伪代码:

public boolean dispatchTouchEvent(MotionEvent event){
    switch(event.getAction()){
        case MotionEvent.ACTION_DOWN:
            parent.requestDisallowInterceptTouchEvent(true);
            break;
        case MotionEvent.ACTION_MOVE:
            if(需要上级拦截){
                parent.requestDisallowInterceptTouchEvent(false);
            }
            break;
        default:
            break;
    }
    return super.dispatchTouchEvent(event);
}

5.涉及类和方法的类图结构

6.应用场合

  • 自定义view
    一般也是改变返回值,具体的代码实现几乎不会改动

  • 滑动冲突(事件分发机制是它的理论基础)
    产生场合
    如何解决

你可能感兴趣的:(自定义view与动画)