Android View事件分发机制

最近在开发中遇到view滑动冲突的问题,由于一开始就知道这个问题与view事件分发有关,之后在网上看了几篇关于事件分发的资料后,开发中遇到的问题很快便得到解决。
在这里总结一下我对view事件分发的理解。

首先,看下事件分发流程图:
Android View事件分发机制_第1张图片

Button事件演示

在对view的事件分发机制进行分析前,我们可以通过一个demo看看Button的事件处理的流程。
在布局文件中添加一个button控件,然后在代码中实现Button的setOnClickListener和setOnTouchListener方法,注册Click监听和Touch监听。

/** * button事件 */
private void showButtonTouch() {
    mBtn = (Button) findViewById(R.id.btn);
    mBtn.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Log.e(TAG, "Button onClick");
        }
    });
    mBtn.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_UP:
                    Log.e(TAG, "Button onTouch " + "ACTION_UP");
                    break;
                case MotionEvent.ACTION_MOVE:
                    Log.e(TAG, "Button onTouch " + "ACTION_MOVE");
                    break;
                case MotionEvent.ACTION_DOWN:
                    Log.e(TAG, "Button onTouch " + "ACTION_DOWN");
                    break;
            }

            return false;
        }
    });
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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">

    <Button  android:id="@+id/btn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button事件测试"/>
</LinearLayout>

demo运行起来之后点击button,看下log日志(在点击时移动一下保证ACTION_MOVE事件能被触发):
这里写图片描述

在这里通过日志可以看出事件处理的流程。

View事件分发源码解析

在View的源码中,我们可以看到dispatchTouchEvent方法,这个方法可以理解为是View事件分发的入口。(代码基于4.0.3即API 15,建议大家在理解源码时不要看太高的版本,高版本源码会有过多的优化,会妨碍我们对于代码主要功能逻辑的理解

dispatchTouchEvent方法解析

以下是View中dispatchTouchEvent方法代码:

public boolean dispatchTouchEvent(MotionEvent event) {
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(event, 0);
    }

    if (onFilterTouchEventForSecurity(event)) {
        //noinspection SimplifiableIfStatement

        //这个是View事件分发的主要逻辑判断
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            return true;
        }

        if (onTouchEvent(event)) {
            return true;
        }
    }

    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
    }
    return false;
}

在这整个方法中,事件分发的关键就在这个判断中 (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event))

如果这个判断为true,那么该方法返回true;否则,执行onTouchEvent()方法并根据onTouchEvent方法的返回值返回具体结果。

  • li != null:代码中ListenerInfo对象li,是View内定义的一个静态类,这个类内部定义了View中所有Listener相关的类,一般情况下这个类不为空,在这里不做过多解释(2.3版本源码中没有该对象)。

  • li.mOnTouchListener != null:是否为空,mOnTouchListener 对象就是我们通过mBtn.setOnTouchListener(new View.OnTouchListener() {})设置的,所以这里不为空。

  • (mViewFlags & ENABLED_MASK) == ENABLED:判断控件是否是enable,很显然为true。

  • li.mOnTouchListener.onTouch(this, event):最后就是判断onTouch方法中返回值是否为true,onTouch方法就是我们在Button控件注册Touch监听时@Override的onTouch方法。

在前面的demo中,因为mOnTouchListener.onTouch(this, event)方法的返回值为false,我们在log中看到button先执行touch事件在执行click事件。在这里我们将onTouch方法的返回值改为true,可以看到以下log:

在log中可以看到click方法没有被执行,这又是为什么呢?

其实在这里大家通过阅读dispatchTouchEvent方法代码可以想到,因为onTouch返回值为true,所以这个判断条件成立即dispatchTouchEvent方法返回true。if (onTouchEvent(event))->onTouchEvent方法没有被执行,会不会是由于这个原因导致click方法没有被执行呢? 很显然我们要看看onTouchEvent方法的源码。

onTouchEvent源码解析:

以下是onTouchEvent方法的源码:

public boolean onTouchEvent(MotionEvent event) {
    final int viewFlags = mViewFlags;

    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PRESSED) != 0) {
            mPrivateFlags &= ~PRESSED;
            refreshDrawableState();
        }
        // 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));
    }

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


    //这是onTouchEvent代码主要逻辑功能
    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
                boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
                if ((mPrivateFlags & 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.
                        mPrivateFlags |= PRESSED;
                        refreshDrawableState();
                   }

                    if (!mHasPerformedLongPress) {
                        // 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();
                }
                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 |= PREPRESSED;
                    if (mPendingCheckForTap == null) {
                        mPendingCheckForTap = new CheckForTap();
                    }
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                } else {
                    // Not inside a scrolling container, so show the feedback right away
                    mPrivateFlags |= PRESSED;
                    refreshDrawableState();
                    checkForLongClick(0);
                }
                break;

            case MotionEvent.ACTION_CANCEL:
                mPrivateFlags &= ~PRESSED;
                refreshDrawableState();
                removeTapCallback();
                break;

            case MotionEvent.ACTION_MOVE:
                final int x = (int) event.getX();
                final int y = (int) event.getY();

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

                        // Need to switch from pressed to not pressed
                        mPrivateFlags &= ~PRESSED;
                        refreshDrawableState();
                    }
                }
                break;
        }
        return true;
    }

    return false;
}

对于onTouchEvent方法,主要的功能逻辑我们只需要从第23行代码开始阅读便可。
首先判断View是否是可点击的,因为Button默认是可点击的,所以这个条件成立,我们可以进入到switch分支判断中。在这里给大家留下一个疑问,如果这个条件不成立即就是控件是不可点击的,会出现什么样的情况呢?

MotionEvent.ACTION_UP:在这个分支判断中我们可以看到在58行有个performClick()方法,我们进入到这个方法体中可以看到:

public boolean performClick() {
    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

    ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        li.mOnClickListener.onClick(this);
        return true;
    }

    return false;
}

在该方法中可以看到li.mOnClickListener.onClick(this)被调用了,到此处我们可以确认Button的click事件就是在onTouchEvent方法中调用的

TextView和Button比较

在onTouchEvent中曾留下一个疑问,如果判断控件是否可点击为false,会出现什么样的情况呢?

为了验证这个问题我们可以通过TextView和Button进行比较一下,因为TextView默认不可点击,Button默认可点击的。

首先在xml中添加TextView和Button,在代码中分别为他们注册setOnTouchListener监听,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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">

    <Button  android:id="@+id/btn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button事件测试"/>

    <TextView  android:id="@+id/tv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@android:color/holo_green_light" android:text="TextView事件测试" />
</LinearLayout>
/** * button事件 */
private void showButtonTouch() {
    mBtn = (Button) findViewById(R.id.btn);
    mBtn.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_UP:
                    Log.e(TAG, "Button onTouch " + "ACTION_UP");
                    break;
                case MotionEvent.ACTION_MOVE:
                    Log.e(TAG, "Button onTouch " + "ACTION_MOVE");
                    break;
                case MotionEvent.ACTION_DOWN:
                    Log.e(TAG, "Button onTouch " + "ACTION_DOWN");
                    break;
            }

            return false;
        }
    });
}
/** * TextView事件 */
private void showTextTouch() {
    mTv = (TextView) findViewById(R.id.tv);
    mTv.setOnTouchListener(new View.OnTouchListener() {
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_UP:
                    Log.e(TAG, "TextView onTouch " + "ACTION_UP");
                    break;
                case MotionEvent.ACTION_MOVE:
                    Log.e(TAG, "TextView onTouch " + "ACTION_MOVE");
                    break;
                case MotionEvent.ACTION_DOWN:
                    Log.e(TAG, "TextView onTouch " + "ACTION_DOWN");
                    break;
            }
            return false;
        }
    });
}

因为我们需要进入到onTouchEvent方法中,所以必须让setOnTouchListener.onTouch()方法的返回值为false,在这里我们看看demo运行起来后分别点击Button和TextView打印的log:

发现Button的log和之前演示的一样,但是TextView在点击->移动->抬起的过程中只打印一次ACTION_DOWN的log,这是什么原因呢?
在之前的onTouchEvent方法中,我们可以看到:

  • 当控件可点击时,方法会进入到switch分支判断中,当switch执行完成后在129行代码会 return true
  • 而当控件不可点击时,方法会直接到132行,return false

    这里需要指明onTouchEvent方法的返回值是touch事件层级传递的关键,说明如下:

  • return true,那么表示该方法消费了此次事件,可以开始下一个事件
  • return false,那么表示该方法并未处理完全,该事件仍然需要以某种方式传递下去继续等待处理。

现在就能解释上面TextView为什么只打印一句log了,因为onTouchEvent返回值为false,系统会认为ACTION_DOWN事件没有被执行完成,那么其他的touch事件就不能被触发了。

现在大家再去看事件分发流程图中的View部分想必就能看懂了吧!!!

你可能感兴趣的:(android,view,事件分发)