Android 触摸事件传递机制

2015/6/14 posted in  Android

之前在设置监听事件遇到了一些问题,今天参考学习了一些文档和博客,记录总结view和viewgroup的触摸事件传递

View

常用监听方法

view表示没有子空间的布局,例如textview、button等,MotionEvent分为三种

  • MotionEvent.ACTION_DOWN:按下时候
  • MotionEvent.ACTION_MOVE:滑动时
  • MotionEvent.ACTION_UP:抬起时

常用两种监听事件

******.setOnClickListener(new View.OnClickListener() {      
            @Override
            public void onClick(View v) {
                // TODO Auto-generated method stub
            }
        });

*******.setOnTouchListener(new View.OnTouchListener() {
            
            @Override
            public boolean onTouch(View v, MotionEvent event){
                // TODO Auto-generated method stub
    
                return false;
            }
        });

注:setOnTouchListener中最后返回的是false

通常一个按键触发的顺序是:

  1. onClick->MotionEvent.ACTION_DOWN
  2. onClick->MotionEvent.ACTION_UP
  3. OnClickListener

若在setOnTouchListener返回true,则不会执行OnClickListener方法

分析源码

在Button中并未发现dispatchTouchEvent代码,向上寻找,Button->TextVIew->View,在view中发现dispatchTouchEvent函数

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

注意 if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) {

mOnTouchListener即注册的touch监听事件。

当li.mOnTouchListener 返回true时,result = true

在onTouchEvent中依次调用performClick()->li.mOnClickListener.onClick(this);

最终会回调用户注册的onclick函数

d

测试代码

主Activity

package io.github.xuyushi.toucheventtest;

import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;


public class MainActivity extends ActionBarActivity {

    private static final String TAG = "TouchEventTest";

    private MyButton button;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button = (MyButton) findViewById(R.id.button_id);

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

        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d(TAG, "MainActivity->" + "OnClickListener()->");

            }
        });
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.d(TAG, "MainActivity->" + "dispatchTouchEvent()->"+"ACTION_DOWN");
                break;
            case MotionEvent.ACTION_UP:
                Log.d(TAG, "MainActivity->" + "dispatchTouchEvent()->"+ "ACTION_UP");
                break;
            default:
                break;
        }


        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.d(TAG, "MainActivity->" + "onTouchEvent()->"+"ACTION_DOWN");
                break;
            case MotionEvent.ACTION_UP:
                Log.d(TAG, "MainActivity->" + "onTouchEvent()->"+ "ACTION_UP");
                break;
            default:
                break;
        }
        return super.onTouchEvent(event);
    }

 
}

自定义Button

package io.github.xuyushi.toucheventtest;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.Button;

/**
 * Created by xuyushi on 15/8/2.
 */
public class MyButton extends Button {
    private static final String TAG = "TouchEventTest";


    //注意 这里第二个参数不能省略,否则会导致崩溃,之后有时间学习研究
    public MyButton(Context context, AttributeSet attrs) {
        super(context, attrs);
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.d(TAG, "MyButton->" + "onTouchEvent->" + "ACTION_DOWN");
                break;
            case MotionEvent.ACTION_UP:
                Log.d(TAG, "MyButton->" + "onTouchEvent->" + "ACTION_UP");
                break;
            default:
                break;
        }
        return true;
    }


    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.d(TAG, "MyButton->" + "dispatchTouchEvent->" + "ACTION_DOWN");
                break;
            case MotionEvent.ACTION_UP:
                Log.d(TAG, "MyButton->" + "dispatchTouchEvent->" + "ACTION_UP");
                break;
            default:
                break;
        }
        return super.dispatchTouchEvent(event);
    }

}

布局文件

<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:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">


    <io.github.xuyushi.toucheventtest.MyButton
        android:id="@+id/button_id"
        android:text="button"
        android:textSize="30dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>

log

D/TouchEventTest﹕ MainActivity->dispatchTouchEvent()->ACTION_DOWN
D/TouchEventTest﹕ MyButton->dispatchTouchEvent->ACTION_DOWN
D/TouchEventTest﹕ MainActivity->OnTouchListener()->ACTION_DOWN
D/TouchEventTest﹕ MyButton->onTouchEvent->ACTION_DOWN
D/TouchEventTest﹕ MainActivity->dispatchTouchEvent()->ACTION_UP
D/TouchEventTest﹕ MyButton->dispatchTouchEvent->ACTION_UP
D/TouchEventTest﹕ MainActivity->OnTouchListener()->ACTION_UP
D/TouchEventTest﹕ MyButton->onTouchEvent->ACTION_UP
D/TouchEventTest﹕ MainActivity->OnClickListener()->

若修改MyButton dispatchTouchEvent返回值为true时

D/TouchEventTest﹕ MainActivity->dispatchTouchEvent()->ACTION_DOWN
D/TouchEventTest﹕ MyButton->dispatchTouchEvent->ACTION_DOWN
D/TouchEventTest﹕ MainActivity->dispatchTouchEvent()->ACTION_UP
D/TouchEventTest﹕ MyButton->dispatchTouchEvent->ACTION_UP

事件先由Activity的dispatchTouchEvent进行分发,然后TestButton的dispatchTouchEvent进行分发,接着执行onTouch监听,然后执行onTouchEvent。第二次UP动作的时候,在onTouchEvent里又执行了onClick监听

若修改MyButton onTouchEvent返回值为true时

D/TouchEventTest﹕ MainActivity->dispatchTouchEvent()->ACTION_DOWN
D/TouchEventTest﹕ MyButton->dispatchTouchEvent->ACTION_DOWN
D/TouchEventTest﹕ MainActivity->OnTouchListener()->ACTION_DOWN
D/TouchEventTest﹕ MyButton->onTouchEvent->ACTION_DOWN
D/TouchEventTest﹕ MainActivity->dispatchTouchEvent()->ACTION_UP
D/TouchEventTest﹕ MyButton->dispatchTouchEvent->ACTION_UP
D/TouchEventTest﹕ MainActivity->OnTouchListener()->ACTION_UP
D/TouchEventTest﹕ MyButton->onTouchEvent->ACTION_UP

注:这里和参考博客的结果不一样,因为在dispatchTouchEvent源码中,进行之前做** if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) {**判断后,执行的是result = true;并不是 return true; 所以onTouchEvent 依然会执行

所以只少一个 OnClickListener未执行

ViewGroup

ViewGroup比View多一个onInterceptTouchEvent方法,此方法是用来拦截事件,拦截之后ViewGroup中的子布局是接受不到事件了。

源码修改

自定义布局

package io.github.xuyushi.toucheventtest;

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

/**
 * Created by xuyushi on 15/8/2.
 */
public class MyLinearLayout extends LinearLayout {

    private static final String TAG = "TouchEventTest";

    public MyLinearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }


    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        // TODO Auto-generated method stub
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.i(TAG, "MyLinearLayout->"+"dispatchTouchEvent->ACTION_DOWN");
                break;
            case MotionEvent.ACTION_UP:
                Log.i(TAG, "MyLinearLayout->"+"dispatchTouchEvent->ACTION_UP");

                break;
            default:
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        // TODO Auto-generated method stub
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.i(TAG, "MyLinearLayout->"+"onInterceptTouchEvent->ACTION_DOWN");

                break;
            case MotionEvent.ACTION_UP:
                Log.i(TAG, "MyLinearLayout->"+"onInterceptTouchEvent->ACTION_UP");

                break;
            default:
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // TODO Auto-generated method stub
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.i(TAG, "MyLinearLayout->"+"onTouchEvent->ACTION_DOWN");
                break;
            case MotionEvent.ACTION_UP:
                Log.i(TAG, "MyLinearLayout->"+"onTouchEvent->ACTION_UP");
                break;
            default:
                break;
        }
        return super.onTouchEvent(event);
    }

}

主activity增加监听

mylayout.setOnTouchListener(new View.OnTouchListener() {

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        // TODO Auto-generated method stub
        switch(event.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.d(TAG, "MainActivity->" + "setOnTouchListener()->" + "ACTION_DOWN");
                break;
            case MotionEvent.ACTION_UP:
                Log.d(TAG, "MainActivity->" + "setOnTouchListener()->" + "ACTION_UP");
                break;
            default:break;

        }
        return false;
    }
});

mylayout.setOnClickListener(new View.OnClickListener() {

    @Override
    public void onClick(View v) {
        // TODO Auto-generated method stub
        Log.i(TAG, "MainActivity--mylayout-onClick...");
    }
});

LOG

MainActivity->dispatchTouchEvent()->ACTION_DOWN
MyLinearLayout->dispatchTouchEvent->ACTION_DOWN
MyLinearLayout->onInterceptTouchEvent->ACTION_DOWN
MyButton->dispatchTouchEvent->ACTION_DOWN
MainActivity->OnTouchListener()->ACTION_DOWN
MyButton->onTouchEvent->ACTION_DOWN
MainActivity->dispatchTouchEvent()->ACTION_UP
MyLinearLayout->dispatchTouchEvent->ACTION_UP
MyLinearLayout->onInterceptTouchEvent->ACTION_UP
MyButton->dispatchTouchEvent->ACTION_UP
MainActivity->OnTouchListener()->ACTION_UP
MyButton->onTouchEvent->ACTION_UP
MyButton->testBtn---onClick
MainActivity->OnClickListener()

如果将Linearlayout的onInterceptTouchEvent 改成return true

MainActivity->dispatchTouchEvent()->ACTION_DOWN
MyLinearLayout->dispatchTouchEvent->ACTION_DOWN
MyLinearLayout->onInterceptTouchEvent->ACTION_DOWN
MainActivity->OnTouchListener()->ACTION_DOWN
MainActivity->dispatchTouchEvent()->ACTION_UP
MyLinearLayout->dispatchTouchEvent->ACTION_UP
MyLinearLayout->onInterceptTouchEvent->ACTION_UP
MainActivity->OnTouchListener()->ACTION_UP
MainActivity--mylayout-onClick...

由此可见,当onInterceptTouchEvent 返回ture之后,button接受不到事件了,只执行MyLinearLayout中的OnTouch 和OnClick了

总结

1、如果是自定义复合控件,如图片+文字,我再Activity里给你注册了onClick监听,期望点击它执行。那么最简单的方法就是将图片+文字的父布局,也即让其容器ViewGroup的秘书将事件拦下,这样父亲就可以执行onClick了。这时候的父亲就像一个独立的孩子一样了(View),再也不用管它的孩子了,可以正常onClick onTouch.

2、如果希望一个View只onTouch而不onClick,在onTouch里return true就ok了。

3、dispatch是为了onTouch监听,onTouchEvent是为了onClick监听。

4、自定义布局时,一般情况下:

@Override

public boolean onTouchEvent(MotionEvent event) {return super.onTouchEvent(event);}  

@Override

public boolean dispatchTouchEvent(MotionEvent event) {return super.dispatchTouchEvent(event);

我们可以复写,但是最后的super.***是万万不能少滴。如果少了,表示连dispatch*** onTouchEvent压根就不调用了,事件就此打住。

Android 事件分发机制结论

View

不管是DOWN,MOVE,UP都会按照下面的顺序执行:

  1. dispatchTouchEvent (view中)
  2. setOnTouchListener的onTouch (监听事件设置 activity 中)
  3. onTouchEvent (view中)

在dispatchTouchEvent中会进行OnTouchListener的判断,如果OnTouchListener不为null且返回true,则表示事件被消费,onTouchEvent不会被执行;否则执行onTouchEvent。

ViewGroup

可以看到大体的事件流程为:
MyLinearLayout的dispatchTouchEvent -> MyLinearLayout的onInterceptTouchEvent -> MyButton的dispatchTouchEvent ->Mybutton的onTouchEvent

可以看出,在View上触发事件,最先捕获到事件的为View所在的ViewGroup,然后才会到View自身~

事件拦截

ViewGroup 与 View 不同的是,可以对事件进行拦截,使子 view 是否能接受到事件。
复写ViewGroup的onInterceptTouchEvent方法:

@Override  
public boolean onInterceptTouchEvent(MotionEvent ev)  
{  
    int action = ev.getAction();  
    switch (action)  
    {  
    case MotionEvent.ACTION_DOWN:  
        //如果你觉得需要拦截  
        return true ;   
    case MotionEvent.ACTION_MOVE:  
        //如果你觉得需要拦截  
        return true ;   
    case MotionEvent.ACTION_UP:  
        //如果你觉得需要拦截  
        return true ;   
    }  
      
    return false;  
}  
    

默认是不拦截的,即返回false;如果你需要拦截,只要return true就行了,这要该事件就不会往子View传递了,并且如果你在DOWN retrun true ,则DOWN,MOVE,UP子View都不会捕获事件;如果你在MOVE return true , 则子View在MOVE和UP都不会捕获事件。

如何不被拦截

如果ViewGroup的onInterceptTouchEvent(ev) 当ACTION_MOVE时return true ,即拦截了子View的MOVE以及UP事件;

此时子View希望依然能够响应MOVE和UP时该咋办呢?

Android给我们提供了一个方法:requestDisallowInterceptTouchEvent(boolean) 用于设置是否允许拦截,我们在子View的dispatchTouchEvent中直接这么写:

@Override  
    public boolean dispatchTouchEvent(MotionEvent event)  
    {  
        getParent().requestDisallowInterceptTouchEvent(true);    
        int action = event.getAction();  
  
        switch (action)  
        {  
        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;  
  
        default:  
            break;  
        }  
        return super.dispatchTouchEvent(event);  
    }  

getParent().requestDisallowInterceptTouchEvent(true); 这样即使ViewGroup在MOVE的时候return true,子View依然可以捕获到MOVE以及UP事件。

  1. 如果ViewGroup找到了能够处理该事件的View,则直接交给子View处理,自己的onTouchEvent不会被触发;
  2. 可以通过复写onInterceptTouchEvent(ev)方法,拦截子View的事件(即return true),把事件交给自己处理,则会执行自己对应的onTouchEvent方法
  3. 子View可以通过调用getParent().requestDisallowInterceptTouchEvent(true); 阻止ViewGroup对其MOVE或者UP事件进行拦截;

比如你需要写一个类似slidingmenu的左侧隐藏menu,主Activity上有个Button、ListView或者任何可以响应点击的View,你在当前View上死命的滑动,菜单栏也出不来;因为MOVE事件被子View处理了~ 你需要这么做:在ViewGroup的dispatchTouchEvent中判断用户是不是想显示菜单,如果是,则在onInterceptTouchEvent(ev)拦截子View的事件;自己进行处理,这样自己的onTouchEvent就可以顺利展现出菜单栏了~~

参考

http://blog.csdn.net/yanzi1225627/article/details/22592831
http://blog.csdn.net/guolin_blog/article/details/9097463
http://www.cnblogs.com/sunzn/archive/2013/05/10/3064129.html