android事件传递机制---源码分析(上)

前言

在android开发中经常会碰到各种View,ViewGroup嵌套等问题。在这种嵌套问题中经常就会碰到滑动,点击等的冲突问题,要想正确的处理这种问题,对android的事件传递机制的理解尤其重要 。平时看书自己测试各种方法都是一知半解,感觉最好的方式还是打开源码看看内部的实现方能理清这些思路。于是参考书籍和郭神的博客,开启了android事件传递源码的分析。

分析思路

其实最主要的,自己对自己准备理清的内容要有一定的思路,也就是看源码要有针对性,因为android的源码太多了,不适合一点一点的精读,所以就要先理清思路,顺藤摸瓜地去读源码,才能搞清楚自己想理清的内容。我们分析android的事件传递机制,首先就要理清,从哪开始分析。我们主要分析的是View和ViewGroup的事件传递,ViewGroup又是View的子类,而且View只是一个控件,比ViewGroup简单,所以我们先理清View的事件传递机制,其实整块80%的内容就解决了。

View的事件传递机制

我们先做一个简单的例子,一个activity,一个可点击的button

btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.i(TAG, "onClick: 按钮被点击了--");
            }
        });
  • 1

我们通常都是这样子添加监听的方式,按钮点击里面的内容就会被执行。那么我们再给它添加上onTouch方法看一看效果

        btn.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.d(TAG, "onTouch: " + event.getAction());

                return false;
            }
        });
  • 1

控制台log结果如下:


可以看到onTouch方法被调用了两次,onClick被调用了一次,并且是先执行onTouch再执行onClick的。我们这里onTouch方法的返回值是false,我们改为true再试一下,结果如下:

可以看到,onTouch仍然是被执行了两次,但是没有执行onClick了,这是为什么呢?我们先理清楚一些内容。

MotionEvent:触摸事件的抽象,事件类型主要有如下几种:
1. ACTION_DOWN:
用户手指按下操作。
2. ACTION_MOVE:
用户手指按压屏幕之后,在松开之前,如果移动的距离超过一定的阀值,就会被判定为ACTION_MOVE操作,这个阀值很小,所以一般手指在屏幕上轻微地滑动都会触发一系列的移动事件。
3. ACTION_UP:
当用户手指离开屏幕的操作。

public boolean dispatchTouchEvent(MotionEvent ev)
这是所有事件分发的总线,每次的MotionEvent操作都会调用到这个方法,而其它诸如onTouch,onTouchEvent等方法的调用等逻辑都在这里面。比如一次按钮的点击操作:
按下会触发一次,移动也会触发,抬起也会触发一次,这就是为什么上面我们点击一次按钮onTouch被调用了两次的原因。

所以我们就从这个方法的源码入手,来分析事件传递机制。

View的dispatchTouchEvent源码

 public boolean dispatchTouchEvent(MotionEvent event) {
        // If the event should be handled by accessibility focus first.
        if (event.isTargetAccessibilityFocus()) {
            // We don't have focus or no virtual descendant has it, do not handle the event.
            if (!isAccessibilityFocusedViewOrHost()) {
                return false;
            }
            // We have focus and got the event, then use normal event dispatch.
            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
            stopNestedScroll();
        }

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

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

可以看到,view的dispatchTouchEvent源码不是很多,我们找到关键代码:

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

我们可以看到,当4个条件完全为真的时候,result=true,那么前两个条件在哪赋值的呢:我们顺着源码找到 :

public void setOnTouchListener(OnTouchListener l) {
        getListenerInfo().mOnTouchListener = l;
    }
  • 1
ListenerInfo getListenerInfo() {
        if (mListenerInfo != null) {
            return mListenerInfo;
        }
        mListenerInfo = new ListenerInfo();
        return mListenerInfo;
    }
  • 1

也就是在setOntouchListener的时候,前两个变量都被赋值了,而mViewFlags & ENABLED_MASK判断当前控件是否是enable的,按钮默认是的,所以为true,所以,内部的赋值操作是否会执行就取决于onTouch的返回值了。可以看到我们上面的例子里,返回false的时候,会进入下面的if判断:

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

因为result初始化为false,如果没进入上面的语句块赋值则不会更改,所以如果onTouch返回为true,判断条件不成立,则onTouchEvent就肯定不会执行的。我们还记的上面的测试,在onTouch方法里,如果返回false,则onTouch和onClick都会执行,如果返回true,那么onClick就不会执行,看到这里我们肯定会猜想onClick就是在OnTouchEvent方法内部执行的,既然有了这个猜想我们就再进去OnTouchEvent方法里看看内部如何实现调用关系的。

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

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

        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

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

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

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

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

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

                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_DOWN:
                    mHasPerformedLongPress = false;

                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

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

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

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

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

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

                            setPressed(false);
                        }
                    }
                    break;
            }

            return true;
        }

        return false;
    }
  • 1

这个方法里代码可不少,但是没关系,我们仍然是抓住关键点来看:
从这个判断语句可以知道,如果控件可以点击,那么会进入下面的switch语句。

 if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
  • 1

然后在ACTION_UP中,往下看

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

也就是当我们处于按下状态的时候,也就是手指按下以后还在接触屏幕,进入大的语句块,在没有执行该click的时候,会调用performClick方法,我们在进入performClick方法看一下

public boolean performClick() {
        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);
        return result;
    }
  • 1

其实看到这里,要求li != null && li.mOnClickListener != null,那么这两个变量在哪里赋值的呢:

public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }
  • 1
ListenerInfo getListenerInfo() {
        if (mListenerInfo != null) {
            return mListenerInfo;
        }
        mListenerInfo = new ListenerInfo();
        return mListenerInfo;
    }
  • 1

看到了吧,跟onTouch类似,这边是在设置监听的时候,为这两个变量赋值的,那么这两个变量都不为空的情况下,就会调用ClickListener的onClick方法,也就是我们自己实现的监听器逻辑。
到这里,验证了我们的猜想,也就是先调用onTouch,根据其返回值会直接决定onTouchEvent方法的执行与否,从而决定onClick方法是否会被执行。

注意,还有一点关键的地方:就是dispatchTouch的返回值会决定一些内容,我们做一个实验:声明一个自定义的layout

package pg.com.mylibrary;

import android.content.Context;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.LinearLayout;

import static pg.com.mylibrary.MyActivity.TAG;

/**
 * 作者:潘浩
 * 学校:南华大学
 * 时间:17-7-31
 */
public class MyLayout extends LinearLayout {
    public MyLayout(Context context) {
        super(context);
    }

    public MyLayout(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public MyLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.d(TAG, "dispatchTouchEvent: down");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d(TAG, "dispatchTouchEvent: move");
                break;
            case MotionEvent.ACTION_UP:
                Log.d(TAG, "dispatchTouchEvent: up");
                break;
            default:
                break;
        }
        return true;
    }



}
  • 1

重写了它的dispatchTouchEvent方法,并且返回值没有super。当我们返回true时,我们测试按下,拖动和抬起操作,控制台打印结果如下:


然后我们把返回值修改为false,再执行上述操作,控制台打印结果如下:

注意,这里我是连续执行了按下,移动和抬起操作两次,而每次只会打印down事件,也就是说,当该方法返回true时,下次的除了down其它的事件都不会被接受了。这里和dispatchTouchEvent的源码无关,因为没有super默认也是这么执行的,这是touch事件层级传递的机制,具体肯定是更高层的代码调用了dispatchTouchEvent方法,在哪里调用的?我还没找到~~

也就是我们如果不重写dispatchTouchEvent方法的情况下,能影响该方法返回值的将在onTouch和onTouchEvent方法里,我们看到,其实不管down还是up,如果控件是可点击的,直接会返回true,否则没进入语句块直接在外层返回false,如果我们这里测试的是imageView,那么这里的onTouch方法也就只会执行一次,因为它默认是不可点击的,所以如果进入onTouchEvent的话,会直接进入不了语句块返回false,那么后续的除了down的操作也就都接受不到了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

apple_51426592

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值