AndroidTouch事件

记录一下Touch事件的分析- -

什么是事件:当用户触摸屏幕时,将产生的触摸行为(Touch事件)

事件的类型:

  • MotionEvent.ACTION_DOWN 手指刚接触屏幕
  • MotionEvent.ACTION_UP 手指从屏幕上松开
  • MotionEvent.ACTION_MOVE 手指在屏幕上滑动
  • MotionEvent.ACTION_CANCEL 非人为因素取消

正常情况下,一次手指触摸屏幕的行为会触发一系列点击事件

  • 点击屏幕后立即松开,事件序列为DOWN—>UP
  • 点击屏幕滑动一会再松开,事件序列为DOWN——>MOVE——>……——>MOVE—>UP


    image.png

事件分发对象

Activity ——> ViewGroup——>View

主要方法

  • dispatchTouchEvent(MotionEvent ev) 用来进行事件分发
  • onInterceptTouchEvent(MotionEvent ev) 判断是否拦截事件(只存在于ViewGroup中)
  • onTouchEvent(MotionEvent ev) 处理点击事件

Activity

当一个事件发生时首先会调用Activity的dispatchTouchEvent()事件
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction(); //这是一个空方法子类可以实现来获取到当前用户触摸屏幕的监听  
    }
    //getWindow 返回的是PhoneWindow 实际上就是调用PhoneWindow的superDispatchTouchEvent的方法
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    //如果没有View消费事件,就会执行Activity的onTouchEvent事件
    return onTouchEvent(ev);
}
然后在PhoneWindow中的superDispatchTouchEvent()方法
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    //mDecor 是继承自FrameLayout的一个子类
    return mDecor.superDispatchTouchEvent(event);
}
然后再看一下DecorView的superDispatchTouchEvent()方法
public boolean superDispatchTouchEvent(MotionEvent event) {
    //它调用了父类的dispatchTouchEvent()方法
    return super.dispatchTouchEvent(event);
}
由于DecorView是FrameLayout的子类所以就到了ViewGroup的dispatchTouchEvent()方法

流程就是

image.png

Activity收到事件后会一层一层的往下派发到ViewGroup到了ViewGroup之后就涉及到了ViewGroup的dispatchTouchEvent方法。

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

    boolean handled = false;
    //mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0&& (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED!=0
    //就是检查上述两个标志上述两个标志均为true时onFilterTouchEventForSecurity会返回false 
    //mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0 是View有设置被遮挡时不处理触摸事件的flag
    //event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED!=0 检查受到该事件的窗口是被其它窗口遮挡
    //FILTER_TOUCHES_WHEN_OBSCURED可以通过在xml文件中的android:filterTouchesWhenObscured来设置,或者在Java中通过setFilterTouchesWhenObscured()来添加或移除
    //DecorView默认是没有这个标志位的,而其他View基本上默认都是有的
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;

        // Handle an initial down.
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            //当时down事件时,是一个新的事件的开始,会进行一系列的reset操作,对上一次的事件一些状态进行重置&mFirstTouchTarget设置成null
            //清除TouchTarget的缓存
            cancelAndClearTouchTargets(ev);
            //对mGroupFlags的标志进行重置为~FLAG_DISALLOW_INTERCEPT
            resetTouchState();
        }

        // Check for interception.
        //记录当前事件能否被拦截
        final boolean intercepted;
        //如果当前为down事件  或者mFirstTouchTarget为null
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            //FLAG_DISALLOW_INTERCEPT这个标志很重要 当子View调用requestDisallowInterceptTouchEvent(boolean)方法时就是设置的mGroupFlags的属性
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            //这个disallowIntercept就是判断是否允许拦截除了down之外的事件 为什么是down事件 是因为在上一个判断中对mGroupFlags的标志进行重置所以在down事件中disallowIntercept永远为false
            //所以就会执行onInterceptTouchEvent()方法
            if (!disallowIntercept) {
                intercepted = onInterceptTouchEvent(ev);
                //onInterceptTouchEvent这个方法默认为false就是viewgroup默认不会拦截事件 
                ev.setAction(action); // restore action in case it was changed
            } else {
                //如果子View调用getParent().requestDisallowInterceptTouchEvent(true)就会执行下边语句,不在拦截事件
                intercepted = false;
            }
        } else {
            //这里默认就是mFirstTouchTarget为null,可以理解为没有子View能够分发此事件 所以 intercepted标志就变为true,
            intercepted = true;
        }
        if (intercepted || mFirstTouchTarget != null) {
            ev.setTargetAccessibilityFocus(false);
        }

        // 标识本次事件需不需要取消
        final boolean canceled = resetCancelNextUpFlag(this)
                || actionMasked == MotionEvent.ACTION_CANCEL;

        // 检查父View是否支持多点触控,即将多个TouchEvent分发给子View,
        // 通过setMotionEventSplittingEnabled()可以修改这个值。
        // FLAG_SPLIT_MOTION_EVENTS在3.0是默认为true的,即支持多点触控的分发
        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        //默认不拦截    
        if (!canceled && !intercepted) {
            // 检查TouchEvent是否可以触发View获取焦点,如果可以,查找本View中有没有获得焦点的子View,
            // 有就获取它,没有就为null
            View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                    ? findChildWithAccessibilityFocus() : null;
            //如果当前为down事件
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE    //鼠标移动) {
                // 获取当前触摸手指在多点触控中的排序
                // 这个值可能因为有手指发生Down或Up而发生改变
                final int actionIndex = ev.getActionIndex(); // always 0 for down
                // 标识当前是那一个点的触摸事件
                //ev.getPointerId()此时获取到手指的Id,这个值在Down到Up这个过程中是不会改变的
                final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)        
                        : TouchTarget.ALL_POINTER_IDS;
                // 清理之前触摸事件中的目标
                removePointersFromTouchTargets(idBitsToAssign);

                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    final float x = ev.getX(actionIndex);
                    final float y = ev.getY(actionIndex);
                
                    //5.0加入的 将所有子View放到集合中,按照添加顺序排序,但是受到Z轴影响 
                    //只有子View数量大于1,并且其中至少有一个子View的Z轴不为0,其实就是elevation属性大于0,它才不为null
                    // 7.0中,View的elevation默认为0
                    final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                    final boolean customOrder = preorderedList == null
                            && isChildrenDrawingOrderEnabled();
                    final View[] children = mChildren;
                    //对Viewgroup的所有子View进行倒序遍历(为什么是倒序  是因为 默认最后添加的View在最上层,应该最优先得到事件)
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        final int childIndex = getAndVerifyPreorderedIndex(
                                childrenCount, i, customOrder);
                        final View child = getAndVerifyPreorderedView(
                                preorderedList, children, childIndex);
                        // 如果当前已经有View获得焦点了,找到它。后面的触摸事件会优先传给它。
                        // 应该主要影响后面触摸点的Down事件
                        if (childWithAccessibilityFocus != null) {
                            if (childWithAccessibilityFocus != child) {
                                continue;
                            }
                            childWithAccessibilityFocus = null;
                            //// 找到后i设为最后一个View,强制结束for循环不再继续查找
                            i = childrenCount - 1;
                        }
                        //此方法判断子View是否可见&没有在执行动画
                        if (!canViewReceivePointerEvents(child)
                                    //判断触摸区域是否在View中
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }
                        //找到了可以分发的View
                        //根据当前child查找是否已经记录在mFirstTouchTarget这个单链表中
                        newTouchTarget = getTouchTarget(child);
                        if (newTouchTarget != null) {
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                            break;
                        }
                        // 再次重置View
                        resetCancelNextUpFlag(child);
                        //将child传递到dispatchTransformedTouchEvent方法中如果传入的child为null会调用super.dispatchTouchEvent
                        // 否则会对想X,Y计算根据当前的View进行偏移然后调用child.dispatchTouchEvent方法将事件传递到child中
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            // 记录这次TouchEvent按下的时间
                            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();
                            //将mFirstTouchTarget指向消费该事件的View
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            // 标记已经把事件分发给了newTouchTarget,退出子View遍历
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }
                        ev.setTargetAccessibilityFocus(false);
                    }
                    if (preorderedList != null) preorderedList.clear();
                }
                //这个只有在多点触控才会执行
                // newTouchTarget在不是Down事件,或者没有找到处理事件的View时是null
                // mFirstTouchTarget在Down事件时,如果找到了处理的View就不为null
                if (newTouchTarget == null && mFirstTouchTarget != null) {
                    // 直接让上次处理的View继续处理
                    newTouchTarget = mFirstTouchTarget;
                    while (newTouchTarget.next != null) {
                        newTouchTarget = newTouchTarget.next;
                    }
                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                }
            //到这里 ACTION_DOWN的事件处理完毕
            }
        }

        //mFirstTouchTarget == null 表示没有能相应该事件的child,那么就调用父类(也就是View)的dispatchTouchEvent,如果在down事件中intercepted为true,则newTouchTarget也为null也会执行此方法
        if (mFirstTouchTarget == null) {
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;
                //表示在Down事件处理中,已经将这个事件交给newTouchTarget处理过了,就不重复处理了
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                //对Move Up事件的处理
                } else {
                    //cancelChild 为true 如果事件被父View拦截了,或者child原本被打上了暂时不可接收TouchEvent的标记PFLAG_CANCEL_NEXT_UP_EVENT
                    // 就给他给它发送取消事件
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    //将Move Up等事件的分发给子View
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    //cancelChild为true 就会清空事件队列,这样后续事件就会被ViewGroup本身拦截掉
                    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;
}

因为代码注释太多了,仔细分析可以看注释即可,简单分析就是

public boolean dispatchTouchEvent(MotionEvent ev){
    boolean concume = fasle ;
    //判断是否要拦截,
    if(onInterceptTouchEvent(ev)){
        //如果拦截就调用自身的onTouchEvent()事件。 ViewGroup的onTouchEvent是用的父类View的,自身并没有实现这个方法
        consume = onTouchEvent(ev)
     }else{
        //如果不拦截就调用child.dispatchTouchEvent再对事件分发给子View
        Consume = child.dispatchTouchEvent(ev);
    }
    return consume;
}

还需要注意一下下边的代码

            //FLAG_DISALLOW_INTERCEPT这个标志很重要 当子View调用requestDisallowInterceptTouchEvent(boolean)方法时就是设置的mGroupFlags的属性
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            //这个disallowIntercept就是判断是否允许拦截除了down之外的事件 为什么是down事件 是因为在上一个判断中对mGroupFlags的标志进行重置所以在down事件中disallowIntercept永远为false
            //所以就会执行onInterceptTouchEvent()方法
            if (!disallowIntercept) {
                intercepted = onInterceptTouchEvent(ev);
                //onInterceptTouchEvent这个方法默认为false就是viewgroup默认不会拦截事件 
                ev.setAction(action); // restore action in case it was changed
            } else {
                //如果子View调用getParent().requestDisallowInterceptTouchEvent(true)就会执行下边语句,不在拦截事件
                intercepted = false;
            }

再看一下dispatchTransformedTouchEvent这个方法

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;
     // 先记录原本的Action
    //如果cancel为true就会发送cancel事件,实际传递进来的事件就会被覆盖掉
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        // 可能过来的事件没有ACTION_CANCEL,如果希望取消的话,那么为事件添加取消标志。
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            // 如果没有子View了,调用View中的dispatchTouchEvent
            // 进而调用View的onTouch或者onTouchEvent方法,触发ACTION_CANCEL逻辑
            handled = super.dispatchTouchEvent(event);
        } else {
            // 如果有子View,将这个取消事件传递给子View
            handled = child.dispatchTouchEvent(event);
        }
        // 在设置回原本的Action
        // 此时TouchEvent的行为相当于没变
        // 但是却把该ViewGroup的
        event.setAction(oldAction);
        return handled;
    }
    // 获取触摸事件的触摸点id
    final int oldPointerIdBits = event.getPointerIdBits();
    // 看和期望的触摸点是不是一个
    final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
       if (newPointerIdBits == 0) {
        //不是
        return false;
    }
    final MotionEvent transformedEvent;
    if (newPointerIdBits == oldPointerIdBits) {
        if (child == null || child.hasIdentityMatrix()) {
            //这里边如果child == null 就会调用自身的super.dispatchTouchEvent方法
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                //否则就会计算偏移量然后调用 就会调用child.dispatchTouchEvent方法进行分发
                final float offsetX = mScrollX - child.mLeft;
                final float offsetY = mScrollY - child.mTop;
                event.offsetLocation(offsetX, offsetY);
                handled = child.dispatchTouchEvent(event);
   
             // 恢复TouchEvent坐标到原来位置,避免影响后面的流
                event.offsetLocation(-offsetX, -offsetY);
            }
            return handled;
        }
        transformedEvent = MotionEvent.obtain(event);
    } else {
        transformedEvent = event.split(newPointerIdBits);
    }
    // Perform any necessary transformations and dispatch.
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        transformedEvent.offsetLocation(offsetX, offsetY);
        if (! child.hasIdentityMatrix()) {
            transformedEvent.transform(child.getInverseMatrix());
        }
        handled = child.dispatchTouchEvent(transformedEvent);
    }


    // Done.
    transformedEvent.recycle();
    return handle;
}

再记录一下cancelAndClearTouchTargets这个方法

private void cancelAndClearTouchTargets(MotionEvent event) {
    // 如果触摸事件目标队列不为空才执行后面的逻辑
    if (mFirstTouchTarget != null) {
        boolean syntheticEvent = false;
        if (event == null) {
            final long now = SystemClock.uptimeMillis();
            //自己创建一个ACTION_CANCEL事件
            event = MotionEvent.obtain(now, now,
                    MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
            // 设置事件源类型为触摸屏幕
            event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
            // 标记一下,这是一个合成事件
            syntheticEvent = true;
        }
        // TouchTarget是一个链表结构,保存了事件传递的子一系列目标View
        for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
            // 检查View是否设置了暂时不在接收事件的标志位,如果有清除该标志位
            // 这样该View就能够接收下一次事件了。
            //这个标志位是PFLAG_CANCEL_NEXT_UP_EVENT 一个View如果有PFLAG_CANCEL_NEXT_UP_EVENT标志,表示它跟ViewGroup解除了绑定,通常会在调用ViewGroup#detachViewFromParent(View),很少用
            resetCancelNextUpFlag(target.child);
            // 将这个取消事件传给子View,给上一次接收事件流的子View发送模拟的ACTION_CANCEL事件,可以重置这些子View的触摸状态。比如取消它们的点击或者长按事件
            dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
        }
        // 清空触摸事件目标队列
        clearTouchTargets();
        if (syntheticEvent) {
            // 如果是合成事件,需要回收它
            event.recycle();
        }
    }
}

再学习一个类

TouchTarget是一个内部类 他是一个单向的链表,mFirstTouchTarget表示的就是头,他记录的就是能够处理child的TouchTarget。它内部有一个链表组成的TouchTarget对象池能够起到复用的机制
private static final class TouchTarget {
    //这个值表示最大个数,这个值也就决定了事件最多传递32层,所以当写一个layout的层级超过32的时候,子View就会收不到事件。
    private static final int MAX_RECYCLED = 32;
    private static final Object sRecycleLock = new Object[0];
    //这一个static的值。所有的ViewGroup对象共用一个这点很重要
    private static TouchTarget sRecycleBin;
    private static int sRecycledCount;

    public static final int ALL_POINTER_IDS = -1; // all ones

    // The touched child view.
    public View child;

    // The combined bit mask of pointer ids for all pointers captured by the target.
    public int pointerIdBits;

    // The next target in the target list.
    public TouchTarget next;

    private TouchTarget() {
    }
    //获取一个可以复用的target
    public static TouchTarget obtain(@NonNull View child, int pointerIdBits) {
        if (child == null) {
            throw new IllegalArgumentException("child must be non-null");
        }

        final TouchTarget target;
        synchronized (sRecycleLock) {
            //如果第一次为null就会创建一个新的
            if (sRecycleBin == null) {
                target = new TouchTarget();
            } else {
                //这基本就是链表操作
                //会在sRecycleBin取一个出来,将对象传递出去
                target = sRecycleBin;
                sRecycleBin = target.next;
                //sRecycleBin 的count-1 ,因为移出去了一个
                 sRecycledCount--;
                target.next = null;
            }
        }
        target.child = child;
        target.pointerIdBits = pointerIdBits;
        return target;
}
    //对this进行回收操作,将它放进sRecycleBin的链表里
    public void recycle() {
        if (child == null) {
            throw new IllegalStateException("already recycled once");
        }
        //基本链表操作
        synchronized (sRecycleLock) {
            if (sRecycledCount < MAX_RECYCLED) {
                next = sRecycleBin;
                sRecycleBin = this;
                sRecycledCount += 1;
            } else {
                next = null;
            }
            child = null;
    }
}

总结:

  • ViewGroup的事件会先判断是否被拦截,如果true就调用super.dispatchTouchEvent也就是自己的View父类的dispatchTouchEven否则就会下发child.dispatchTouchEvent
  • Down事件是一系列事件的开端,所以很重要,花费的事件也最长
  • 子View可以通过调用getParent().requestDisallowInterceptTouchEvent(true)通知ViewGroup不拦截事件
  • ViewGroup的onTouchEvent()没有实现,是用的View的
  • ViewGroup通过TouchTarget来缓存down事件接收的View,然后其他事件通过TouchTarget来分发事件,防止再次循环遍历,提高效率
  • TouchTarget是一个单链表实现的对象池,这个思路值得学习。Android源码内部有很多类似的设计
    下边看一下View的,View的事件非常简单,因为它不会再继续往下分发了。所以只要判断处不处理就可以了
public boolean dispatchTouchEvent(MotionEvent event) {
    if (event.isTargetAccessibilityFocus()) {
        if (!isAccessibilityFocusedViewOrHost()) {
            return false;
        }
        event.setTargetAccessibilityFocus(false);
    }

    boolean result = false;

    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(event, 0);
    }

    final int actionMasked = event.getActionMasked();
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        // Defensive cleanup for new gesture
        //5.0内嵌滑动的处理
        stopNestedScroll();
    }
    //跟ViewGroup一样的判断
    if (onFilterTouchEventForSecurity(event)) {
        if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
            result = true;
        }
  
        ListenerInfo li = mListenerInfo;
        //优先执行li.mOnTouchListener.onTouch方法。
        if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
        }
        //如果执行li.mOnTouchListener.onTouch方法返回为true就不会再次执行onTouchEvent了这个需要注意
        if (!result && onTouchEvent(event)) {
            result = true;
        }
    }

    if (!result && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
    }
    if (actionMasked == MotionEvent.ACTION_UP ||
            actionMasked == MotionEvent.ACTION_CANCEL ||
            (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
        stopNestedScroll();
    }

    return result;
}

可以看到

  • View的dispatchTouchEvent方法很简单,并没有再次分发事件的逻辑了
  • onTouch优先于onTouchEvent执行,并且返回true之后onTouchEvent就不再执行
    所以事件分发的总体流程就是


    image.png

    最后再分析一下View的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();

    final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
            || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
            || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
    //如果ENABLED为false 则直接return
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
        // A disabled view that is clickable still consumes the touch
        // events, it just doesn't respond to them.
        //如果ENABLED为false 则直接return 不在继续后续操作
        return clickable;
    }
    if (mTouchDelegate != null) {
        //如果有TouchDelegate则直接返回true
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }

    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                if ((viewFlags & TOOLTIP) == TOOLTIP) {
                    handleTooltipUp();
                }
                if (!clickable) {
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    break;
                }
                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) {
                     
                        setPressed(true, x, y);
                    }
                    //mHasPerformedLongPress 会有1中情况会true  只有当触摸了超过500ms并且onLongClick回调返回了true mHasPerformedLongPress就会true 可查看源码比较清晰,
                    //这种情况下就不会出发点击事件
                    //所以就会又2种情况 低于500ms会移除长按事件,并触发点击事件,超过500ms之后并且设置了长按监听会优先处理长按事件然后根据返回值来处理是否执行下边代码
                    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                        // This is a tap, so remove the longpress check
                        //移除长按事件
                        removeLongPressCallback();

                        if (!focusTaken) {
                             //点击事件
                            if (mPerformClick == null) {
                                mPerformClick = new PerformClick();
                            }
                            if (!post(mPerformClick)) {
                                performClick();
                            }
                        }
                    }

                   // 省去若干代码

                    removeTapCallback();
                }
                mIgnoreNextUpEvent = false;
                break;

            case MotionEvent.ACTION_DOWN:
                if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                    mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                }
                //mHasPerformedLongPress 改成false
                mHasPerformedLongPress = false;

                if (!clickable) {
                    checkForLongClick(0, x, y);
                    break;
                }

                if (performButtonActionOnTouchDown(event)) {
                    break;
                }

                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();
                    //发送一个100ms的点击任务
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    //mPendingCheckForTap 里边会发送一个500ms的长按事件的延迟任务 是一个CheckForLongPress的类
                } 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:
                if (clickable) {
                    setPressed(false);
                }
                removeTapCallback();
                removeLongPressCallback();
                mInContextButtonPress = false;
                mHasPerformedLongPress = false;
                mIgnoreNextUpEvent = false;
                mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                break;

            case MotionEvent.ACTION_MOVE:
                if (clickable) {
                    drawableHotspotChanged(x, y);
                }

              ////省去若干代码
                break;
        }

        return true;
    }

    return false;
}

总结

  • 一个时间序列从手指触摸屏幕到手指离开屏幕,在这个过程中产生一系列事件,以DOWN事件开始,中间含有不定数的MOVE事件,以UP事件结束
  • 正常情况下,一个事件序列只能被一个View拦截并且消耗
  • 某个View一旦决定拦截,那么这个事件序列都将由它的onTouchEvent处理,并且它的OnInterceptTouchEvent不会再调用
  • 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回false),那么同一事件序列中的其他事件都不会再交给它处理,并且重新交由它的父级元素处理(父元素onTouchEvent被调用)
  • 事件的传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子View中干预父元素的事件分发过程,ACTION_DOWN除外
  • ViewGroup默认不拦截任何事件,即onInterceptTouchEvent默认返回false。View没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent方法就会被调用
  • View的onTouchEvent默认会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时为false)View的longClickable默认都是false,clickable要分情况,比如Button的clickable默认为true,TextView的默认为false
  • View的enable属性不影响onTouchEvent的默认返回值。哪怕一个View是disable状态的,只要它的clickable或者longClickable有一个为true,那么它的onTouchEvent就会返回true
  • onClick回响应的前提是当前View是可点击的,并且收到了ACTION_DOWN和ACTION_UP的事件,并且受长按事件影响,当长按事件返回true时,onClick不会响应
  • onLongClick在ACTION_DOWN里判断是否进行响应,要想执行长按事件该View必须是longClickable的并且设置了OnLongClickListener
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350