之前在设置监听事件遇到了一些问题,今天参考学习了一些文档和博客,记录总结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
通常一个按键触发的顺序是:
- onClick->MotionEvent.ACTION_DOWN
- onClick->MotionEvent.ACTION_UP
- 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都会按照下面的顺序执行:
- dispatchTouchEvent (view中)
- setOnTouchListener的onTouch (监听事件设置 activity 中)
- 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事件。
- 如果ViewGroup找到了能够处理该事件的View,则直接交给子View处理,自己的onTouchEvent不会被触发;
- 可以通过复写onInterceptTouchEvent(ev)方法,拦截子View的事件(即return true),把事件交给自己处理,则会执行自己对应的onTouchEvent方法
- 子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