美文网首页
从源码看android触摸事件分发

从源码看android触摸事件分发

作者: 冬冬269 | 来源:发表于2018-08-25 20:09 被阅读0次

android事件分发在android相当重要,我们有必要从源码来看。

android中有view,和viewgroup,可以简单把viewgroup理解为view组,viewgroup也是继承view的。

事件分发有三个重要方法 dispatchTouchEvent(MotionEvent event)、onInterceptTouchEvent(MotionEvent event)、onTouchEvent(MotionEvent event)。我们可以看到事件传递分发的都是MotionEvent。在view中是没有onInterceptTouchEvent的,dispatchTouchevent直接调用onTouchEvent。

1.dispatchTouchEvent(MotionEvent event)

当一个点击操作发生时,事件最先发送到activity中,由activity的dispatchTouchEvent进行分发,调用activity的dispatchTouchEvent();


image.png

可以看到,接下来会调用

getWindow().superDIspatchTouchEvent(ev);

我们再看window中的

public abstract boolean superDispatchTouchEvent(MotionEvent event);

是一个抽象方法,window是一个抽象类,具体实现类是phoneWindow,其中的方法

mDecor.superdispatchTouchEvent()

其中mDecor是DecorView,是activity布局文件的跟布局,这里会另外学习。
这是decorview的dispatchTouchEvent的方法。


    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        final Window.Callback cb = mWindow.getCallback();
        return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
                ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
    }

super.dispatchTouchEvent(),继承framelayout,是一个viewgroup。下面看viewgroup的源码。

我们分成四部分来看。

第一部分

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

如果触发事件是down,初始化一些参数。其中重要参数有
mFirstTouchEvent = null
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT。一个是处理本次事件的子view,一个是view是否请求viewgroup,不要拦截事件。子类调用的方法为parent.requestDisallowInterceptTouchEvent。

第二部分

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

创建了一个变量intercepted,用final修饰,变量变常量,只能赋值一次,这里是一个判断,如果event是DOWN 或者mFirstTouchTarget不为null,进入判断,都为null,直接设置intercepted为true,这个常量标识viewgroup是否拦截本次事件。判断中我们可以看到,先判断mGroupFlags&FLAGS_DISALLOW_INTERCEPT标识符,为1说明调用了requestDisallowInterceptTouchEvent,如果为1,intercept常量赋值为false,如果为0,调用onInterceptTouchEvent方法并把返回值赋值给常量intercept。

第三部分(假设不拦截)

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

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

循环viewgroup中的view,判断这个view是否是可见的并且点击事件的坐标是否落在view区域内并且view是否getAnimation()。如果返回为true,调用viewgroup的方法dispatchTransfromedTouchEvent(MotionEvent event,boolean cancel,View child,int ...);我们来看一下这个方法有一个是判断是否是取消,滑动事件是否是cancle,这个暂时不看。关键看下面这个。

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

如果view 参数为null,调用super.dispatchTouchEvent方法,我们知道viewgroup的这个方法意思就是调用view.dispatchTouchEvent方法(),不为null,调用这个view的dispatchTouchEvent()方法并把返回值返回。

如果返回值为true,表示onTouchEvent返回为true,意思是消费了。那么就给mFirstTouchTarget赋值,TouchTarget(view,id)当前子view和本次事件id,单链表结构,并跳出循环。为false。继续循环。

第四部分,判断mFirstTouchTarget为null,直接调用dispatchTransformedTouchEvent,传入view为null,调用super.dispatchTouchEvent。就是view的dispatchTouchEvent。什么时候mFirstTouxhTarget为null,第一从down就拦截了,第二,没有拦截但是所有的子view的dispatchTouchEvent返回都是false。这时viewgroup就是把自己当作一个view来处理。

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

下面分析view.dispatchTouchEvent

//核心代码
if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

可以看到view.dispatchTouchEvent中,先调用onTouchListener.onTouch方法,返回为false则调用自己的onTouchEvent方法,说明了优先级顺序,好处是方便外部处理点击事件。

下面分析view.onTouchEvent()方法。

第一部分

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.
            return clickable;
        }
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

判断view是否设置了enable 如果为disable,那么看有没有给view设置点击事件,如果设置了,那么仍然会消费调这次事件,并且返回为true。虽然什么都没做。

第二部分

if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

这里的TouchDelegate是扩大view触摸范围的一个类。

判断是否可点击。clickable longclickable 或者Context_Clickable
进入下面方法,一旦进去onTouchEvent就会返回true。

今天具体看看onTouchEvent到底干嘛了。

一共有四个DOWN、UP、MOVE、CANCLE
先从最简单的cancle 和 up来分析好了。
clickable 设置了点击事件 设置了长点击

final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;


case MotionEvent.ACTION_CANCEL:
//简单一句话,初始化view的状态为未点击状态。
                    if (clickable) {
                        setPressed(false);
                    }
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    break;
case MotionEvent.ACTION_UP:
//乍一看挺长的,根据两部分来看。是否在scrollview中。
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    if ((viewFlags & TOOLTIP) == TOOLTIP) {
                        handleTooltipUp();
                    }
//不能点击,初始化状态,但是都进up方法了,一般都是可点击的。
                    if (!clickable) {
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        break;
                    }
//这里是up的第二部分,下面用@2表示。
//prepress是在scroolview中,并且按下事件不超过100ms
                    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();
                        }
//在up里设置setPress为true,这是什么操作?不急,看下翻译,也就是让用户看到你调到button了。在下面一会儿一会儿的地方就会设置为false。
                        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);
                        }

//第一部分,不在scroolview中,关键参数。mHasPerformedLongPress,这个参数值为false,触发了并且perfromlongclick为true,值为true。赋值是在down里,一会儿就能看到。这里就是说设置了点击长事件方法,没有执行到,或者执行到了但返回值为false。就会往下走,换到代码里说啊,如果你同时给一个view设置长点击事件和点击事件,如果长点击事件触发了并返回为false,点击事件也会触发了。如果为true呢,点击事件就不触发了。下面有个小demo,也验证了一下。
                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // This is a tap, so remove the longpress check
//如果mHasPerformedLongPress未false,那就remove掉longpress的check,
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
//英语翻译,只执行按下状态的click事件。然后执行onclick事件。如果你要弹出一个Toast等等。
                            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();
                        }
//第二部分,就是我说的,为嘛up还要setPress(true)呢,别急,这不就针对prepress给设置为false了。UnsetPressedState里的run方法就是调用setPress,并传入false方法。
                        if (prepressed) {
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }
//删除掉100ms的callback。
                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;
                    break;

IKGH0`DY5W2EWJAKR]Y`Y2X.png
Button btnTest = findViewById(R.id.btn_ontouch);

        btnTest.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                Log.i("dongdong","点击了长点击事件");
                return false;
            }
        });
        btnTest.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.i("dongdong","点击了点击事件");
            }
        });

这部分看完可能还有疑问,有些参数,或者什么什么scrollview啊,哪跟哪啊,我们看下down就全明白了。


case MotionEvent.ACTION_DOWN:
                    if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                        mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                    }
//设置了初始值为false,在up的时候用到了哦。
                    mHasPerformedLongPress = false;
//不是点击过来的,那就是tooltip过来的,可以设置长点击事件的那种,类似popupwindow,好像过时了。
                    if (!clickable) {
                        checkForLongClick(0, x, y);
                        break;
                    }

                    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.
//这个view在scrollview中。
                    if (isInScrollingContainer) {
//初始化press状态为prepress,这个东西在up的时候会用到。
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
//检查prepress点击状态,taotimeout一般就是100ms
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
//不在,最舒服,直接就设置setPresstrue ,检查长点击状态。
                        // Not inside a scrolling container, so show the feedback right away
                        setPressed(true, x, y);
                        checkForLongClick(0, x, y);
                    }
                    break;
    /**
     * <p>Indicates this view can display a tooltip on hover or long press.</p>
     * {@hide}
     */
    static final int TOOLTIP = 0x40000000;
private final class CheckForTap implements Runnable {
        public float x;
        public float y;

        @Override
        public void run() {
//修改prepress状态不为prepress
            mPrivateFlags &= ~PFLAG_PREPRESSED;
//修改prepress状态为press。
            setPressed(true, x, y);
            checkForLongClick(ViewConfiguration.getTapTimeout(), x, y);
        }
    }
private void checkForLongClick(int delayOffset, float x, float y) {
        if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
//设置长点击状态为false
            mHasPerformedLongPress = false;

            if (mPendingCheckForLongPress == null) {
                mPendingCheckForLongPress = new CheckForLongPress();
            }
            mPendingCheckForLongPress.setAnchor(x, y);
            mPendingCheckForLongPress.rememberWindowAttachCount();
            mPendingCheckForLongPress.rememberPressedState();
            postDelayed(mPendingCheckForLongPress,
                    ViewConfiguration.getLongPressTimeout() - delayOffset);
        }
    }
 private final class CheckForLongPress implements Runnable {
        private int mOriginalWindowAttachCount;
        private float mX;
        private float mY;
        private boolean mOriginalPressedState;

//主要就是看run方法。
        @Override
        public void run() {
            if ((mOriginalPressedState == isPressed()) && (mParent != null)
                    && mOriginalWindowAttachCount == mWindowAttachCount) {
//调用longclick 返回为true,修改参数,为false。没操作。
                if (performLongClick(mX, mY)) {
                    mHasPerformedLongPress = true;
                }
            }
        }

        public void setAnchor(float x, float y) {
            mX = x;
            mY = y;
        }

        public void rememberWindowAttachCount() {
            mOriginalWindowAttachCount = mWindowAttachCount;
        }

        public void rememberPressedState() {
            mOriginalPressedState = isPressed();
        }
    }
case MotionEvent.ACTION_MOVE:
//判断 如果手势移出屏幕
                    if (clickable) {
                        drawableHotspotChanged(x, y);
                    }

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

CANCLE 方法触发条件。。父类突然拦截事件。或者手机移出view范围。

大概就是这样。不足之处或疑问等,感谢指出。

相关文章

网友评论

      本文标题:从源码看android触摸事件分发

      本文链接:https://www.haomeiwen.com/subject/ueytiftx.html