当前位置: 代码迷 >> Android >> Android View事件散发机制
  详细解决方案

Android View事件散发机制

热度:57   发布时间:2016-04-24 11:29:22.0
Android View事件分发机制

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

首先,看下事件分发流程图:
这里写图片描述

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事件分发的入口。(代码基于2.2即API 8,建议大家在理解源码时不要看太高的版本,高版本源码会有过多的优化,会妨碍我们对于代码主要功能逻辑的理解

dispatchTouchEvent方法解析

以下是View中dispatchTouchEvent方法代码:

    public boolean dispatchTouchEvent(MotionEvent event) {        if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&                mOnTouchListener.onTouch(this, event)) {            return true;        }        return onTouchEvent(event);    }

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

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

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

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

  • 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) {            // 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;            }        }        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 (!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) {                            mPrivateFlags |= PRESSED;                            refreshDrawableState();                            postDelayed(mUnsetPressedState,                                    ViewConfiguration.getPressedStateDuration());                        } else if (!post(mUnsetPressedState)) {                            // If the post failed, unpress right now                            mUnsetPressedState.run();                        }                        removeTapCallback();                    }                    break;                case MotionEvent.ACTION_DOWN:                    if (mPendingCheckForTap == null) {                        mPendingCheckForTap = new CheckForTap();                    }                    mPrivateFlags |= PREPRESSED;                    mHasPerformedLongPress = false;                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());                    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                    int slop = mTouchSlop;                    if ((x < 0 - slop) || (x >= getWidth() + slop) ||                            (y < 0 - slop) || (y >= getHeight() + slop)) {                        // 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方法,主要的功能逻辑我们只需要从第17行代码开始阅读便可。
首先判断View是否是可点击的,因为Button默认是可点击的,所以这个条件成立,我们可以进入到switch分支判断中。在这里给大家留下一个疑问,如果这个条件不成立即就是控件是不可点击的,会出现什么样的情况呢?

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

    public boolean performClick() {        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);        if (mOnClickListener != null) {            playSoundEffect(SoundEffectConstants.CLICK);            mOnClickListener.onClick(this);            return true;        }        return false;    }

在该方法中可以看到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部分想必就能看懂了吧!!!

  相关解决方案