当前位置: 代码迷 >> Android >> Android View 抚摸事件传递机制
  详细解决方案

Android View 抚摸事件传递机制

热度:51   发布时间:2016-04-27 23:44:29.0
Android View 触摸事件传递机制

PS:以现在的眼光看以前写的博客感觉写的很烂,或许或一段时间再看现在的博客会有同样的感觉。所以每时每刻都去学习,去发现和理解新的东西。

引言

由于之前写的一篇关于Android事件传递顺序的博客质量太差,可能是理解不到位的原因,故最近又花了许多时间再次去看Android源码,看完之后有了新的理解,所以打算重新整理这篇博客。理解Android触摸事件传递机制有助于日后的开发以及自定义一些手势效果等。注意:这篇博客是基于Android2.0源码来分析的,不管老版本还是新版本的Android,其内部触摸事件传递机制是不变的。只是说Android2.0的源码相对比较少,便于读者理解。

示例

自定义一个MyCustomView

public class MyCustomView extends View {    private String TAG = "MyButton";    public MyCustomView(Context context) {        super(context);    }    public MyCustomView(Context context, AttributeSet attrs) {        super(context, attrs);    }    public MyCustomView(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);    }    @Override    public boolean dispatchTouchEvent(MotionEvent event) {        switch (event.getAction()) {            case MotionEvent.ACTION_DOWN:                Log.e(TAG, "dispatchTouchEvent----->>ACTION_DOWN");                break;            case MotionEvent.ACTION_MOVE:                Log.e(TAG, "dispatchTouchEvent----->>ACTION_MOVE");                break;            case MotionEvent.ACTION_UP:                Log.e(TAG, "dispatchTouchEvent----->>ACTION_UP");                break;        }        return super.dispatchTouchEvent(event);    }    @Override    public boolean onTouchEvent(MotionEvent event) {        switch (event.getAction()) {            case MotionEvent.ACTION_DOWN:                Log.e(TAG, "onTouchEvent----->>ACTION_DOWN");                break;            case MotionEvent.ACTION_MOVE:                Log.e(TAG, "onTouchEvent----->>ACTION_MOVE");                break;            case MotionEvent.ACTION_UP:                Log.e(TAG, "onTouchEvent----->>ACTION_UP");                break;        }        return super.onTouchEvent(event);    }}

重写dispatchTouchEvent和onTouchEvent方法,添加三种触摸事件的打印日志。

MainActivity调用如下:

public class MainActivity extends Activity {    private MyCustomView button;    private String TAG = "MainActivity";    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        button = (MyCustomView) findViewById(R.id.button);        Log.e(TAG, "the view is clickable " + button.isClickable());        button.setClickable(true);        button.setOnTouchListener(new View.OnTouchListener() {            @Override            public boolean onTouch(View v, MotionEvent event) {                switch (event.getAction()) {                    case MotionEvent.ACTION_DOWN:                        Log.e(TAG, "onTouch------->>ACTION_DOWN");                        break;                    case MotionEvent.ACTION_MOVE:                        Log.e(TAG, "onTouch------->>ACTION_MOVE");                        break;                    case MotionEvent.ACTION_UP:                        Log.e(TAG, "onTouch------->>ACTION_UP");                        break;                }                return false;            }        });        button.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                Log.e(TAG, "onClick------->>onClick");            }        });    }}

点击自定义MyCustomView,结果打印如下:

07-29 11:03:51.714  20278-20278/com.xjp.toucheventdemo E/MainActivity﹕ the view is clickable false07-29 11:03:54.877  20278-20278/com.xjp.toucheventdemo E/MyButton﹕ dispatchTouchEvent----->>ACTION_DOWN07-29 11:03:54.878  20278-20278/com.xjp.toucheventdemo E/MainActivity﹕ onTouch------->>ACTION_DOWN07-29 11:03:54.878  20278-20278/com.xjp.toucheventdemo E/MyButton﹕ onTouchEvent----->>ACTION_DOWN07-29 11:03:54.929  20278-20278/com.xjp.toucheventdemo E/MyButton﹕ dispatchTouchEvent----->>ACTION_MOVE07-29 11:03:54.929  20278-20278/com.xjp.toucheventdemo E/MainActivity﹕ onTouch------->>ACTION_MOVE07-29 11:03:54.929  20278-20278/com.xjp.toucheventdemo E/MyButton﹕ onTouchEvent----->>ACTION_MOVE07-29 11:03:54.929  20278-20278/com.xjp.toucheventdemo E/MyButton﹕ dispatchTouchEvent----->>ACTION_UP07-29 11:03:54.931  20278-20278/com.xjp.toucheventdemo E/MainActivity﹕ onTouch------->>ACTION_UP07-29 11:03:54.931  20278-20278/com.xjp.toucheventdemo E/MyButton﹕ onTouchEvent----->>ACTION_UP07-29 11:03:54.936  20278-20278/com.xjp.toucheventdemo E/MainActivity﹕ onClick------->>onClick

分析:有上面的打印可以看出

  • 触摸事件主要有三种且执行顺序为:ACTION_DOWN,ACTION_MOVE,ACTION_UP。也就是先执行ACTION_DOWN按下的行为,按下之后手指可能会移动,移动时就出发了ACTION_MOVE行为,当手指抬起时,触发了ACTION_UP行为,至此触摸事件顺序执行结束。当然触摸事件不止这三种行为,但是我们这里主要分析这三种。
  • 触摸事件过程执行的方法顺序为:dispatchTouchEvent,onTouch,onTouchEvent。最后执行了onClick点击事件。也就是顺序应该为:dispatchTouchEvent–>>onTouch–>>onTouchEvent–>>onClick
  • onClick点击事件是在触摸事件ACTION_UP执行完之后才执行。

为什么会有以上三种现象和情况出现?现在我们只能从打印日志中看到结果,但是并不知道其内部原因,为了窥探其内部原因,read the fuck code 。基于Android2.0源码分析View。View的触摸事件分发是从View#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条件就返回true退出方法,条件不满足时,才去执行onTouchEvent方法且返回该方法的返回值。
1.那么什么情况下满足mOnTouchListener != null条件呢?查看View源码发现调用如下方法时:

public void setOnTouchListener(OnTouchListener l) {        mOnTouchListener = l;    }

当开发者给相应的View设置了View#setOnTouchListener触摸事件之后,mOnTouchListener != null条件就成立。
2.View默认都是enabled状态,所以第二个条件成立。
3.当前两个条件都成立了,执行第三个条件接口方法mOnTouchListener.onTouch(this, event)。根据该方法的返回值来决定if条件是否成立。该方法在开发者设置View#setOnTouchListener触摸事件实现,当onTouch方法返回false时,dispatchTouchEvent方法就会执行onTouchEvent方法,否则不执行onTouchEvent方法。

总结:
1.onTouch接口方法的返回值决定是否执行onTouchEvent方法。
2.只要onTouch接口方法返回值为true,dispatchTouchEvent方法一定返回true,否则根据onTouchEvent方法返回值决定dispatchTouchEvent返回值。

View#onTouchEvent

进入onTouchEvent方法:

public boolean onTouchEvent(MotionEvent event) {        final int viewFlags = mViewFlags;     ................        if (((viewFlags & CLICKABLE) == CLICKABLE ||                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {            switch (event.getAction()) {                case MotionEvent.ACTION_UP:                    if ((mPrivateFlags & PRESSED) != 0) {                        // 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                            if (mPendingCheckForLongPress != null) {                                removeCallbacks(mPendingCheckForLongPress);                            }                            // Only perform take click actions if we were in the pressed state                            if (!focusTaken) {                                performClick();                            }                        }                        if (mUnsetPressedState == null) {                            mUnsetPressedState = new UnsetPressedState();                        }                        if (!post(mUnsetPressedState)) {                            // If the post failed, unpress right now                            mUnsetPressedState.run();                        }                    }                    break;                case MotionEvent.ACTION_DOWN:                    mPrivateFlags |= PRESSED;                    refreshDrawableState();                    if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {                        postCheckForLongClick();                    }                    break;                case MotionEvent.ACTION_CANCEL:                    mPrivateFlags &= ~PRESSED;                    refreshDrawableState();                    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 = ViewConfiguration.get(mContext).getScaledTouchSlop();                    if ((x < 0 - slop) || (x >= getWidth() + slop) ||                            (y < 0 - slop) || (y >= getHeight() + slop)) {                        // Outside button                        if ((mPrivateFlags & PRESSED) != 0) {                            // Remove any future long press checks                            if (mPendingCheckForLongPress != null) {                                removeCallbacks(mPendingCheckForLongPress);                            }                            // Need to switch from pressed to not pressed                            mPrivateFlags &= ~PRESSED;                            refreshDrawableState();                        }                    } else {                        // Inside button                        if ((mPrivateFlags & PRESSED) == 0) {                            // Need to switch from not pressed to pressed                            mPrivateFlags |= PRESSED;                            refreshDrawableState();                        }                    }                   break;            }            return true;        }        return false;    }

分析:代码有点长,首先进入该方法判断if条件是否成立?如果该View是可点击的或者是可以长按点击,则if条件成立,进入if判断,执行ACTION_UP分支。
1.代码第26行,调用了performClick方法来执行View的点击事件

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

该方法判断如果mOnClickListener!=null条件成立,则执行mOnClickListener.onClick(this);接口方法。什么时候条件成立呢?当给当前View设置了点击监听事件之后,条件成立,因此调用接口onClick方法。在View类中有如下方法:

public void setOnClickListener(OnClickListener l) {        if (!isClickable()) {            setClickable(true);        }        mOnClickListener = l;    }

先判断当前View是否可点击的状态?如果不可点击的话,先设置成可点击,之后对mOnClickListener赋值操作。总结:只要给任何一个View设置了setOnClickListener点击监听事件,不管这个View是否是可点击的状态,最后都设置为了可点击的状态了。

2.只有当前View是可点击或者长按的状态,才进入if条件判断,然后执行相应的手势操作,最后返回true。也就是说,只要View是可点击的,onTouchEvent方法返回的就是true,从而dispatchTouchEvent方法返回的也是true。

3.只要是当前View是不可点击或者长按的状态,if条件不成立,不执行任何操作,直接返回false。也就是说,View不可点击的时候,onTouchEvent方法返回的就是false,从而dispatchTouchEvent方法返回的也是false。

4.onClick方法是在ACTION_UP手势里面执行的,也就是当手势抬起时才去执行onClick方法。

到此,Android View触摸事件传递已经分析结束。如果条件都满足,则整个触摸事件传递过程就是:dispatchTouchEvent–>>onTouch–>>onTouchEvent–>>onClick。现在我们来验证一下如下几种情况:

onTouch方法返回值为true

改成如下

 button.setOnTouchListener(new View.OnTouchListener() {            @Override            public boolean onTouch(View v, MotionEvent event) {                switch (event.getAction()) {                    case MotionEvent.ACTION_DOWN:                        Log.e(TAG, "onTouch------->>ACTION_DOWN");                        break;                    case MotionEvent.ACTION_MOVE:                        Log.e(TAG, "onTouch------->>ACTION_MOVE");                        break;                    case MotionEvent.ACTION_UP:                        Log.e(TAG, "onTouch------->>ACTION_UP");                        break;                }                return true;            }        });

此时点击View的打印如下:

07-29 14:42:22.969    2964-2964/com.xjp.toucheventdemo E/MyButton﹕ dispatchTouchEvent----->>ACTION_DOWN07-29 14:42:22.970    2964-2964/com.xjp.toucheventdemo E/MainActivity﹕ onTouch------->>ACTION_DOWN07-29 14:42:22.987    2964-2964/com.xjp.toucheventdemo E/MyButton﹕ dispatchTouchEvent----->>ACTION_MOVE07-29 14:42:22.987    2964-2964/com.xjp.toucheventdemo E/MainActivity﹕ onTouch------->>ACTION_MOVE07-29 14:42:22.987    2964-2964/com.xjp.toucheventdemo E/MyButton﹕ dispatchTouchEvent----->>ACTION_UP07-29 14:42:22.988    2964-2964/com.xjp.toucheventdemo E/MainActivity﹕ onTouch------->>ACTION_UP

当onTouch方法返回true时,就不执行onTouchEvnet方法,因此也就不执行onClick点击事件。可以理解成此时onTouch把触摸事件已经消费掉了,也就不会继续往下传递触摸事件。所以如果你不想自己的View执行onTouchEvent方法,你可以设置onTouch事件,且返回值为true即可。

View不可点击情况

在文章开头的 MainActivity里面,我是添加了一行代码 button.setClickable(true); 目的是让当前View可点击,但是默认情况下除了Button,TextView少数控件外,其他大部分View控件默认都是不可点击的状态,除非你设置了View#setClickable(true)或者View#setOnClickListener。现在我将MainActivity中的button.setClickable(true);这一行代码去掉且不设置setOnClickListener事件,

public class MainActivity extends Activity {    private MyCustomView button;    private String TAG = "MainActivity";    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        button = (MyCustomView) findViewById(R.id.button);        Log.e(TAG, "the view is clickable " + button.isClickable());//        button.setClickable(true);        button.setOnTouchListener(new View.OnTouchListener() {            @Override            public boolean onTouch(View v, MotionEvent event) {                switch (event.getAction()) {                    case MotionEvent.ACTION_DOWN:                        Log.e(TAG, "onTouch------->>ACTION_DOWN");                        break;                    case MotionEvent.ACTION_MOVE:                        Log.e(TAG, "onTouch------->>ACTION_MOVE");                        break;                    case MotionEvent.ACTION_UP:                        Log.e(TAG, "onTouch------->>ACTION_UP");                        break;                }                return false;            }        });//        button.setOnClickListener(new View.OnClickListener() {//            @Override//            public void onClick(View v) {//                Log.e(TAG, "onClick------->>onClick");//            }//        });    }}

Log打印日志如下:

07-29 14:57:03.656    4896-4896/com.xjp.toucheventdemo E/MyButton﹕ dispatchTouchEvent----->>ACTION_DOWN07-29 14:57:03.658    4896-4896/com.xjp.toucheventdemo E/MainActivity﹕ onTouch------->>ACTION_DOWN07-29 14:57:03.658    4896-4896/com.xjp.toucheventdemo E/MyButton﹕ onTouchEvent----->>ACTION_DOWN

不知道你发现木有?此处打印看出只执行了ACTION_DOWN手指操作,其他的手势操作呢?没有执行,为什么呢?
情况是这样的:当onTouch方法返回false,则dispatchTouchEvent方法就会执行onTouchEvent方法,但是由于View不可点击,所以onTouchEvent是不执行if条件体的,也就是onTouchEvent方法返回false,从而导致dispatchTouchEvent方法返回false,由于dispatchTouchEvent方法返回false,导致后面的手势操作ACTION_MOVE,ACTION_UP得不到执行。

总结:如果我们将手势操作分为三个过程的话:ACTION_DOWN,ACTION_MOVE,ACTION_UP。只有当dispatchTouchEvent方法返回true时,系统才会执行对应过程后面的手势操作。

总结

至此Android View 触摸事件传递机制已经分析结束,现在用一个流程图来体现:

这里写图片描述

  1. 触摸事件传递顺序:dispatchTouchEvent–>>onTouch–>>onTouchEvent–>>onClick。
  2. onTouch和onTouchEvent区别:两个方法先后在dispatchTouchEvent中调用,只有给View设置了触摸事件View#setOnTouchListener才会执行onTouch方法;onTouch方法的返回值决定是否执行onTouchEvent方法。
  3. 手势操作执行的顺序为ACTION_DOWN,ACTION_MOVE,ACTION_UP,只有dispatchTouchEvent方法返回true值时后面的手势才会被执行。
  4. onClick方法的调用是在onTouchEvent的ACTION_UP手势里面执行的,也就是当手势抬起时,手势操作结束才会触发onClick方法的调用。

【转载请注明出处:http://blog.csdn.net/feiduclear_up CSDN 废墟的树】

版权声明:本文为博主原创文章,未经博主允许不得转载。

  相关解决方案