Android事件分发机制浅析

来源:互联网 发布:淘宝上的汉斯格雅花洒 编辑:程序博客网 时间:2024/04/30 03:18

       事件机制是Android中一个比较复杂且重要的知识点,比如你想自定义拦截事件,或者某系组件中嵌套了其他布局,往往会出现这样那样的事件冲突,坑爹啊!!事件主要涵盖onTouch,onClick,onTouchEvent,dispatchTouchEvent,onInterceptTouchEvent等等一系列事件,并且事件间还相互交互耦合,甚至有的事件还有返回值,一会true,一会false,什么情况下返回true,什么情况下返回false,为什么要有返回值,想想这些就感觉整个人都不好了。

这里写图片描述

       但是(万恶的但是),该知识点还是必须要掌握的,知识的深度与广度决定了你走的远度,鉴于此我们就来捅一捅该知识点。

准备工作

       俗话说工欲善其事必先利其器,为了看他的执行流程,我们还是先写个样例,打几个日志看看执行流程吧!

       首先自定义一个外层布局的Layout,自定义Layout继承了LinearLayout,复写了相应的函数,在调用之前输入日志。如下:

public class Layout extends LinearLayout {    public Layout(Context context) {        super(context);        init();    }    public Layout(Context context, AttributeSet attrs) {        super(context, attrs);        init();    }    public Layout(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        init();    }    private void init() {        //requestDisallowInterceptTouchEvent(false);    }    @TargetApi(Build.VERSION_CODES.KITKAT)    @Override    public boolean onInterceptTouchEvent(MotionEvent ev) {        Log.e("Event", "Layout onInterceptTouchEvent " + MotionEvent.actionToString(ev.getAction()));        return super.onInterceptTouchEvent(ev);    }    @TargetApi(Build.VERSION_CODES.KITKAT)    @Override    public boolean onTouchEvent(MotionEvent event) {        Log.e("Event", "Layout onTouchEvent " + MotionEvent.actionToString(event.getAction()));        return super.onTouchEvent(event);    }    @TargetApi(Build.VERSION_CODES.KITKAT)    @Override    public boolean dispatchTouchEvent(MotionEvent event) {        Log.e("Event", "Layout dispatchTouchEvent " + MotionEvent.actionToString(event.getAction()));        return super.dispatchTouchEvent(event);    }}

       我们还自定义了一个LogTextView,继承自TextView,也是为了输出日志,代码如下:

public class LogTextView extends TextView {    public LogTextView(Context context) {        super(context);    }    public LogTextView(Context context, AttributeSet attrs) {        super(context, attrs);    }    public LogTextView(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);    }    @TargetApi(Build.VERSION_CODES.KITKAT)    @Override    public boolean onTouchEvent(MotionEvent event) {        Log.e("Event", "TextView  onTouchEvent " + MotionEvent.actionToString(event.getAction()));        return super.onTouchEvent(event);    }    @Override    public void setOnTouchListener(OnTouchListener l) {        super.setOnTouchListener(l);    }    @Override    public void setOnClickListener(OnClickListener l) {        super.setOnClickListener(l);    }}

       接下来是布局文件了:

<?xml version="1.0" encoding="utf-8"?><com.sunny.event.wigdet.Layout    xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:orientation="vertical"    android:paddingBottom="@dimen/activity_vertical_margin"    android:paddingLeft="@dimen/activity_horizontal_margin"    android:paddingRight="@dimen/activity_horizontal_margin"    android:paddingTop="@dimen/activity_vertical_margin">    <com.sunny.event.wigdet.LogTextView        android:id="@+id/textView"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_gravity="center_horizontal"        android:layout_marginTop="40dp"        android:background="#999999"        android:padding="20dp"        android:text="Hello World!"/>    <ImageView        android:id="@+id/image"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_gravity="center_horizontal"        android:layout_marginTop="20dp"        android:src="@mipmap/ic_launcher"/></com.sunny.event.wigdet.Layout>

       布局中嵌套了两个view,一个TextView,一个ImageView。最后就是主界面了。

public class MainActivity extends AppCompatActivity {    private LogTextView tv;    private ImageView imageView;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        findViews();        setViewListener();    }    private void findViews() {        tv = (LogTextView) findViewById(R.id.textView);        imageView = (ImageView) findViewById(R.id.image);    }    private void setViewListener() {        tv.setOnTouchListener(new View.OnTouchListener() {            @TargetApi(Build.VERSION_CODES.KITKAT)            @Override            public boolean onTouch(View v, MotionEvent event) {                Log.e("Event", "TextView  onTouch " + MotionEvent.actionToString(event.getAction()));                return true;            }        });        tv.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                Log.e("Event", "TextView  onClick ");            }        });        imageView.setOnTouchListener(new View.OnTouchListener() {            @TargetApi(Build.VERSION_CODES.KITKAT)            @Override            public boolean onTouch(View v, MotionEvent event) {                Log.e("Event", "ImageView onTouch " + MotionEvent.actionToString(event.getAction()));                return false;            }        });        imageView.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                Log.e("Event", "ImageView onClick ");            }        });    }}

执行结果

       round 1

       TextView的onTouch返回为false,点击TextView,日志如下:

05-05 14:04:44.091 27644-27644/com.sunny.event E/Event: Layout    dispatchTouchEvent ACTION_DOWN05-05 14:04:44.091 27644-27644/com.sunny.event E/Event: Layout    onInterceptTouchEvent ACTION_DOWN05-05 14:04:44.091 27644-27644/com.sunny.event E/Event: TextView  onTouch ACTION_DOWN05-05 14:04:44.091 27644-27644/com.sunny.event E/Event: TextView  onTouchEvent ACTION_DOWN05-05 14:04:44.158 27644-27644/com.sunny.event E/Event: Layout    dispatchTouchEvent ACTION_MOVE05-05 14:04:44.158 27644-27644/com.sunny.event E/Event: Layout    onInterceptTouchEvent ACTION_MOVE05-05 14:04:44.158 27644-27644/com.sunny.event E/Event: TextView  onTouch ACTION_MOVE05-05 14:04:44.158 27644-27644/com.sunny.event E/Event: TextView  onTouchEvent ACTION_MOVE05-05 14:04:44.158 27644-27644/com.sunny.event E/Event: Layout    dispatchTouchEvent ACTION_UP05-05 14:04:44.159 27644-27644/com.sunny.event E/Event: Layout    onInterceptTouchEvent ACTION_UP05-05 14:04:44.159 27644-27644/com.sunny.event E/Event: TextView  onTouch ACTION_UP05-05 14:04:44.159 27644-27644/com.sunny.event E/Event: TextView  onTouchEvent ACTION_UP05-05 14:04:44.159 27644-27644/com.sunny.event E/Event: TextView  onClick 

       根据日志我们可以看到首先有一个ACTION_DOWN事件,执行的顺序是Layout的dispatchTouchEvent→onInterceptTouchEvent→(TextView)onTouch要→onTouchEvent,之后的我帕金森发生了,产生了ACTION_MOVE事件,传递的顺序与Down是一致的,最后一个事件是UP事件,正常点击不滑动是不会产生MOVE事件的,在这个这个三个事件最后调用了TextView的onClick事件。

小结:

1 . 事件的传递顺序是先外层容器,之后再是具体的View。
2. onTouch事件先于onTouchEvent事件,onTouchEvent先于onClick事件

       round 2

       我们将TextView的onTouch事件返回true。重新执行。执行顺序如下:

05-05 14:10:20.556 27644-27644/com.sunny.event E/Event: Layout    dispatchTouchEvent ACTION_DOWN05-05 14:10:20.556 27644-27644/com.sunny.event E/Event: Layout    onInterceptTouchEvent ACTION_DOWN05-05 14:10:20.556 27644-27644/com.sunny.event E/Event: TextView  onTouch ACTION_DOWN05-05 14:10:20.616 27644-27644/com.sunny.event E/Event: Layout    dispatchTouchEvent ACTION_MOVE05-05 14:10:20.616 27644-27644/com.sunny.event E/Event: Layout    onInterceptTouchEvent ACTION_MOVE05-05 14:10:20.616 27644-27644/com.sunny.event E/Event: TextView  onTouch ACTION_MOVE05-05 14:10:20.622 27644-27644/com.sunny.event E/Event: Layout    dispatchTouchEvent ACTION_UP05-05 14:10:20.622 27644-27644/com.sunny.event E/Event: Layout    onInterceptTouchEvent ACTION_UP05-05 14:10:20.622 27644-27644/com.sunny.event E/Event: TextView  onTouch ACTION_UP

       从日志可以看出如果onTouch返回为true,执行顺序变成了如下:
       首先还是ACTION_DOWN事件(Layout)dispatchTouchEvent→onInterceptTouchEvent→(TextView)onTouch,ACTION_MOVE与ACTION_UP执行顺序同ACTION_DOWN,可以发现的是TextView的onTouchEvent事件没有了,并且onClick事件也没有了。

小结

1,onTouch事件的返回值为true会拦截onTouchEvent事件
2,onTouchEvent与onClick有关联

       上面的两次执行中每次都调用了onInterceptTouchEvent事件,这个到底又是啥?我们去看看他的返回值是什么?

public boolean onInterceptTouchEvent(MotionEvent ev) {    return false;}

       可以看到默认返回false,注释长的吓人,那我们就来改写一下他的返回值,这个函数是ViewGroup才有的,说明与布局容器有关.
       round 3

       我们将Layout的onInterceptTouchEvent的返回值改为true。重新执行。执行顺序如下:

05-05 14:59:17.829 15157-15157/com.sunny.event E/Event: Layout    dispatchTouchEvent ACTION_DOWN05-05 14:59:17.830 15157-15157/com.sunny.event E/Event: Layout    onInterceptTouchEvent ACTION_DOWN05-05 14:59:17.830 15157-15157/com.sunny.event E/Event: Layout    onTouchEvent ACTION_DOWN

       从日志可以发现,只有最外层的控件能够执行事件,TextView已经收不到任何事件。

小结

父控件onInterceptTouchEvent返回true会拦截子控件的事件

追根溯源

       我们从代码的层面来看看他是怎么执行的,当屏幕接收到点击事件时会首先传递到Activity的dispatchTouchEvent:

/** * Called to process touch screen events.  You can override this to * intercept all touch screen events before they are dispatched to the * window.  Be sure to call this implementation for touch screen events * that should be handled normally. * * @param ev The touch screen event. * * @return boolean Return true if this event was consumed. */public boolean dispatchTouchEvent(MotionEvent ev) {    if (ev.getAction() == MotionEvent.ACTION_DOWN) {        onUserInteraction();    }    if (getWindow().superDispatchTouchEvent(ev)) {        return true;    }    return onTouchEvent(ev);}

       在这里执行了三步,

1.第一告诉用户ACTION_DOWN,用户可以复写onUserInteraction来处理点击开始
2.调用了getWindow().superDispatchTouchEvent(ev),这里的getWindow得到是PhoneWindow对象,因此执行的PhoneWindow的superDispatchTouchEvent函数,
3.调用了Activity的onTouchEvent事件

       PhoneWindow的superDispatchTouchEvent又调用了DecorView的superDispatchTouchEvent函数,每一个Activity都有一个PhoneWindow,每一个PhoneWindow都有一个DecorView,DecoView继承自FrameLayout,这里又调用了super.dispatchTouchEvent(event),FrameLayout里面是没有改函数的,所以最终执行的是ViewGroup的dispatchTouchEvent函数。

       这里我们先穿插一点界面的知识,以我测试手机为例,DecorView中有两个child,分别是ViewStub和LinerLayout,LinerLayout中又包含了FrameLayout,FrameLayout中包含了一个ActionBarOverlayLayout,ActionBarOverlayLayout里又包含了两个child,分别是ActionBarContainer与ContentFrameLayout。

       接下来我们去看看ViewGroup的dispatchTouchEvent函数:

@Overridepublic 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) {            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;        }        ............        // 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 (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 = 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<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);                        .......                        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();                            newTouchTarget = addTouchTarget(child, idBitsToAssign);                            alreadyDispatchedToNewTouchTarget = true;                            break;                        }                    }                    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;}

       我们只看最重要的部分

1: 事件为ACTION_DOWN时,执行了cancelAndClearTouchTargets函数,该函数主要清除上一次点击传递的路径,之后执行了resetTouchState,重置了touch状态,其中执行了 mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;就是拦截状态为false,这个与requestDisallowInterceptTouchEvent函数相关。

2: 获取intercepted的值,首先判断了disallowIntercept状态,是否拦截子控件的事件执行。从代码可以看到当disallowIntercept为false时,该状态主要取决于onInterceptTouchEvent函数的返回值,这就是前面我们拦截的函数,如果为true,这时intercepted为true标识拦截。

3: 接着判断了!canceled && !intercepted的值,canceled这里为false,如果intercepted为false,则会进入判断条件,这里假设不拦截,进入后继续判断如果是ACTION_DOWN事件,则会继续进入判断,遍历所有子控件,isTransformedTouchPointInView会判断当前点击区域是否在控件内,如果不在则遍历下一个,之后调用dispatchTransformedTouchEvent函数。最后在调用addTouchTarget函数,将当前选中的控件,挂载到当前点击目标链表。alreadyDispatchedToNewTouchTarget赋值为true。

接着判断mFirstTouchTarget是否为空,经过上一步的addTouchTarget的执行,这里mFirstTouchTarget不为空。第一个事件alreadyDispatchedToNewTouchTarget为true,且target == newTouchTarget,因此handled值为true,如果是后续的事件,则会进入dispatchTransformedTouchEvent中。

        我们接着看看第三部中的dispatchTransformedTouchEvent函数:

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,        View child, int desiredPointerIdBits) {    final boolean handled;    // Calculate the number of pointers to deliver.    final int oldPointerIdBits = event.getPointerIdBits();    final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;    .......    final MotionEvent transformedEvent;    if (newPointerIdBits == oldPointerIdBits) {        if (child == null || child.hasIdentityMatrix()) {            if (child == null) {                handled = super.dispatchTouchEvent(event);            } else {                final float offsetX = mScrollX - child.mLeft;                final float offsetY = mScrollY - child.mTop;                event.offsetLocation(offsetX, offsetY);                handled = child.dispatchTouchEvent(event);                event.offsetLocation(-offsetX, -offsetY);            }            return handled;        }        transformedEvent = MotionEvent.obtain(event);    } else {        transformedEvent = event.split(newPointerIdBits);    }    .......    // Done.    transformedEvent.recycle();    return handled;}

       这里会进入到第10行,且传递过来的child不为空,因此会继续执行child.dispatchTouchEvent,这里继续执行ViewGroup的dispatchTouchEvent,一直递归执行,直到真正接受点击的控件,到最后child会为空,这里要么是一个View控件,要么是未包含任何子控件的ViewGroup,这时这里会执行View的dispatchTouchEvent。

       从上述执行逻辑可以直到,先从DecorView一直递归到Layout,最后再到TextView,这里我们去看看View的dispatchTouchEvent:

public boolean dispatchTouchEvent(MotionEvent event) {    final int actionMasked = event.getActionMasked();    if (actionMasked == MotionEvent.ACTION_DOWN) {        // Defensive cleanup for new gesture        stopNestedScroll();    }    if (onFilterTouchEventForSecurity(event)) {        //noinspection SimplifiableIfStatement        ListenerInfo li = mListenerInfo;        if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED                && li.mOnTouchListener.onTouch(this, event)) {            result = true;        }        if (!result && onTouchEvent(event)) {            result = true;        }    }    if (!result && mInputEventConsistencyVerifier != null) {        mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);    }    // Clean up after nested scrolls if this is the end of a gesture;    // also cancel it if we tried an ACTION_DOWN but we didn't want the rest    // of the gesture.    if (actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_CANCEL ||            (actionMasked == MotionEvent.ACTION_DOWN && !result)) {        stopNestedScroll();    }    return result;}

       首先会停止掉嵌套滑动,之后先判断了ListenerInfo不为空,这里只要是设置了onTouch,onKey,onHover,onDrag等等中的任何一个这里就不为空,具体可以去看看ListenerInfo包含的listenerInfo类型。其次判断了mOnTouchListener不为空,只要设置了onTouchListener这里就不为空,再之后判断了该控件是否是enabled,一般都会enabled,可以代码设置为false,再之后调用了mOnTouchListener的onTouch事件,这里就是外面传进来的onTouchListener,从这里可以看到无论onTouch返回任何值,onTouch事件都会执行,但是如果返回为true,则会导致result为true,!result && onTouchEvent(event)因为短路,不会执行到onTouchEvent事件。

小结

1:onTouch返回为true导致onTouchEvent不能执行
2:如果enable为false,因为短路onTouch不会执行

        到此还没有看到任何onClick事件的执行,我们继续去看看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 & 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);                }                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;}

       我们首先看ACTION_DOWN事件,这里主要看checkForLongClick,CheckForTap中也调用了该函数,这里就是添加一个长按事件,如果达到长按标准且长按listener不为空,则执行长按事件,接着我们看ACTION_UP,这里看到如果不是长按事件,则调用了performClick,performClick里面执行了onClick事件。

小结

1:onClick事件与onLongClick事件是在onTouchEvent中执行的
2:如果执行了长按事件则onClick不执行
3:就api 23代码,长按的时间间隔为500毫秒

        上面解析了intercepted为false的情况,那intercepted为true,它到底是怎么拦截的?

       如果intercepted为true,则!canceled && !intercepted为false,不能进入该判断,mFirstTouchTarget为空,会继续执行如下分支:

if (mFirstTouchTarget == null) {    // No touch targets so treat this as an ordinary view.    handled = dispatchTransformedTouchEvent(ev, canceled, null,            TouchTarget.ALL_POINTER_IDS);} 

       这里第三参数传递的child为null,因此就会执行该控件onTouch与onTouchEvent函数,不会继续递归传递,因此也就拦截了子控件的执行。

总结

  1. 事件接收先从父控件到子控件,如果父控件onInterceptTouchEvent为true,则表示拦截事件。
  2. dispatchTouchEvent的ACTION_DOWN事件中,会清除上一次的点击目标列表,且重置disallowIntercept状态为false,表示拦截,但是真正的拦截状态还是靠onInterceptTouchEvent函数的返回值决定。
  3. 如果为自定义控件,自定义控件与父控件有事件冲突,比如说滑动等,还需要重写onInterceptTouchEvent。
  4. 如果onLongClick执行,api 23 默认时间为500毫秒,则onClick不执行。
  5. 如果onTouch事件返回为true,则会拦截onTouchEvent事件,onClick,onLongClick事件均不在执行。
2 0