Android-View事件分发机制

前言

Android开发中一些简单的UI开发其实用不到事件分发,但是要做一些特殊的功能的时候会用到触摸事件的处理,那么我们就需要了解View和ViewGoup的事件分发机制,这样才能在触摸屏幕的时候在不同的View和ViewGroup直之间切换事件处理。下面将从源码入手分析View的的事件分发机制。

View事件分发源码分析

事件的分发机制由dispatchTouchEvent进行控制,根据里面的逻辑进行判断是执行OnTouchListener还是OnClickListener;所以要了解事件的分发原理,就需要了解dispatchTouchEvent的源码。以下代码片段展示了dispatchTouchEvent中控制OnTouchListener和TouchEvent的核心代码:

    /**
     * Pass the touch screen motion event down to the target view, or this
     * view if it is the target.
     *
     * @param event The motion event to be dispatched.
     * @return True if the event was handled by the view, false otherwise.
     */
    public boolean dispatchTouchEvent(MotionEvent event) {
        ......
        boolean result = false;
    
        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;
            }
        }
        ...
        return result;
    }

从dispathcTouchEvent的方法注释中可以看出,该方法的作用是将触摸屏运动事件传递到目标视图或者自身,针对View传递的对象就是自身,而针对ViewGroup,就可能会存在目标对象。

if (li != null && li.mOnTouchListener != null
                && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            result = true;
}
 if (!result && onTouchEvent(event)) {
                result = true;
            }

从这段代码中可以看出,如果我们设置了OnTouchListener并且返回true,那么result为true,后续的onToucheEvent就不能执行。接下来看一下onTouchEvent的源码处理流程:

/**
 *
 * @param event The motion event.
 * @return True if the event was handled, false otherwise.
 */
 public boolean onTouchEvent(MotionEvent event) {
 final float x = event.getX();
        //获取点击坐标和事件
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();
        //判断该View是否可以点击
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

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

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

                        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:
                    if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                        mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                    }
                    mHasPerformedLongPress = false;

                    if (!clickable) {
                        checkForLongClick(
                                ViewConfiguration.getLongPressTimeout(),
                                x,
                                y,
                                TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                        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.
                    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(
                                ViewConfiguration.getLongPressTimeout(),
                                x,
                                y,
                                TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                    }
                    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);
                    }

                    final int motionClassification = event.getClassification();
                    final boolean ambiguousGesture =
                            motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE;
                    int touchSlop = mTouchSlop;
                    if (ambiguousGesture && hasPendingLongPressCallback()) {
                        if (!pointInView(x, y, touchSlop)) {
                            // The default action here is to cancel long press. But instead, we
                            // just extend the timeout here, in case the classification
                            // stays ambiguous.
                            removeLongPressCallback();
                            long delay = (long) (ViewConfiguration.getLongPressTimeout()
                                    * mAmbiguousGestureMultiplier);
                            // Subtract the time already spent
                            delay -= event.getEventTime() - event.getDownTime();
                            checkForLongClick(
                                    delay,
                                    x,
                                    y,
                                    TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                        }
                        touchSlop *= mAmbiguousGestureMultiplier;
                    }

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

                    final boolean deepPress =
                            motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS;
                    if (deepPress && hasPendingLongPressCallback()) {
                        // process the long click action immediately
                        removeLongPressCallback();
                        checkForLongClick(
                                0 /* send immediately */,
                                x,
                                y,
                                TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS);
                    }

                    break;
            }

            return true;
        }

        return false;
     
     
 }

该方法的处理较复杂,我们简化操作,只分析简单流程和clickListener的执行,进入到该方法后会获取点击的坐标、view是否可用、点击事件是否可用。满足条件后会在MotionEvent.ACTION_UP中进入下面方法:

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

该逻辑会在UI线程红执行PerformClick

 private final class PerformClick implements Runnable {
        @Override
        public void run() {
            recordGestureClassification(TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SINGLE_TAP);
            performClickInternal();
        }
    }

最后再执行:

public boolean performClick() {
    // We still need to call this method to handle the cases where performClick() was called
    // externally, instead of through performClickInternal()
    notifyAutofillManagerOnClick();

    final boolean result;
    final ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);
        result = true;
    } else {
        result = false;
    }

    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

    notifyEnterOrExitForAutoFillIfNeeded(true);

    return result;
}

如果我们设置了OnClickListener,最终会在这里消费事件。

分析了相关的源码后我们可以得到下面的结论:

  1. 针对View的事件分发,都是由dispatchTouchEvent进行控制是否像后传递
  2. dispatchTouchEvent首先执行OnTouchListener,如果该方法中有对应的事件被消费,那么不会执行后续的onTouchEvent()的对应事件
  3. 我们平时开发设置的OnClickListener和OnLongClickListener的执行在OnTouchListener之后。

了解MotionEvent中的action

了解View事件分发的基本流程后,我们平时开发中更多的是需要根据业务场景处理Down、Move、Up、Cancel等事件,那么下面通过在OnTouchListener中通过action的DOWN、MOVE、UP、Cancel进行处理验证事件的传递流程:

//设置OnTouchListener,输出acition信息
this.waterView.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        Log.d("waterView","onTouch:"+event.getAction());
        int action = event.getAction();
        if(action == MotionEvent.ACTION_DOWN){
            Log.d("EventTag","onTouchListener:"+"ACTION_DOWN");
        }else if(action == MotionEvent.ACTION_MOVE){
            Log.d("EventTag","onTouchListener:"+"ACTION_MOVE");
        }else if(action == MotionEvent.ACTION_UP){
            Log.d("EventTag","onTouchListener:"+"ACTION_UP");
        }
        return false;
    }
});

//重写View的onTouchEvent,输出事件的传递流程
@Override
public boolean onTouchEvent(MotionEvent event) {
    int action = event.getAction();
    if(action == MotionEvent.ACTION_DOWN){
        Log.d("EventTag","onTouchEvent:"+"ACTION_DOWN");
    }else if(action == MotionEvent.ACTION_MOVE){
        Log.d("EventTag","onTouchEvent:"+"ACTION_MOVE");
    }else if(action == MotionEvent.ACTION_UP){
        Log.d("EventTag","onTouchEvent:"+"ACTION_UP");
    }
    return super.onTouchEvent(event);
}

1. 测试一:OnTouchListener返回false,onTouchEvent调用return super.onTouchEvent(event),不设置OnClickListener;得到以下日志信息:

17631-17631/com.water.view.demo D/EventTag: onTouchListener:ACTION_DOWN
17631-17631/com.water.view.demo D/EventTag: onTouchEvent:ACTION_DOWN

通过日志发现值输出的ACTION_DOWN其他的action并没有输出,这里主要是有ViewGroup的事件分发决定的,子View不处理Down事件,那么后续的事件将不会收到。

2. 测试二:将onTouchEvent的ACTION_DWON返回true:

18130-18130/com.water.view.demo D/EventTag: onTouchListener:ACTION_DOWN
18130-18130/com.water.view.demo D/EventTag: onTouchEvent:ACTION_DOWN
18130-18130/com.water.view.demo D/EventTag: onTouchListener:ACTION_MOVE
18130-18130/com.water.view.demo D/EventTag: onTouchEvent:ACTION_MOVE
18130-18130/com.water.view.demo D/EventTag: onTouchListener:ACTION_UP
18130-18130/com.water.view.demo D/EventTag: onTouchEvent:ACTION_UP

通过上面日志,将onTouchEvent的ACTION_DWON返回true,后面的事件能够正常接收, 所以:在开发中需要在子view中处理事件,那么一定要消费DOWN,否则后续的Action将不能接受

3. 测试三:将OnTouchListener的ACTION_DWON返回true:

19016-19016/com.water.view.demo D/EventTag: onTouchListener:ACTION_DOWN
19016-19016/com.water.view.demo D/EventTag: onTouchListener:ACTION_MOVE
19016-19016/com.water.view.demo D/EventTag: onTouchEvent:ACTION_MOVE
19016-19016/com.water.view.demo D/EventTag: onTouchListener:ACTION_UP
19016-19016/com.water.view.demo D/EventTag: onTouchEvent:ACTION_UP

通过上面的日志,我们可以看到View的onTouchEvent中没有收到ACTION_DOWN事件。这里的控制逻辑是由dispatchTouchEvent控制的,判断依据是result,如果OnToucheListern中对应的Action返回了true,那么该值为true,那么onTouchEvent()方法就不会执行了。

    public boolean dispatchTouchEvent(MotionEvent event) {
        ......
        //判断事件的后续分发
        boolean result = false;
        if (onFilterTouchEventForSecurity(event)) {
            ......
            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;
            }
        }
        ...
        return result;
    }

4. 测试四:将OnTouchListener的ACTION_MOVE和ACTION_DWON返回true:

20211-20211/com.water.view.demo D/EventTag: onTouchListener:ACTION_DOWN
20211-20211/com.water.view.demo D/EventTag: onTouchListener:ACTION_MOVE
20211-20211/com.water.view.demo D/EventTag: onTouchListener:ACTION_MOVE
20211-20211/com.water.view.demo D/EventTag: onTouchListener:ACTION_UP
20211-20211/com.water.view.demo D/EventTag: onTouchEvent:ACTION_UP

从上面的输出日志中可以看出,onTouchEvent接受不到ACTION_DOWN和ACTION_UP事件,其他的事件的处理这里不再列举,流程和上面的类似。

最后

分析了源码,demo进行测试,写了这么多的文字进行说明,那么我们可以收获什么呢?

  1. View屏幕触摸事件由dispatchToucheEvent()进行控制,由其内部逻辑决定后续执行的方法
  2. OnTouchListener的处理优先级要高于onTouchEvent
  3. 如果在OnTouchListener中消费了ACTION_DOWN、ACTION_MOVE、ACTION_UP等事件,对应的事件则不能传递到onTouchEvent中
  4. 如果我们自定义的View需要处理事件,那么ACTION_DOWN一定要消费,否则不能接受到后续的其他事件
  5. 要对一个知识点进行熟练掌握,查看底层的实现原理,更容易掌握得精准和牢固

在实际的项目的开发中,事件的分发的复杂处理在于ViewGroup和View之间的嵌套使用,后续将进一步进行解析。

事件分发的相关文章可参考:

Android-ViewGrop事件分发机制

Android-事件分发-嵌套滑动

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值