【Android 1.6】View和ViewGroup的touch事件分析和总结
ENV: android 1.6
View事件的派发其实非常简单,不是想象的那么复杂,你也别把它看得那么复杂,你简单看它,它就很简单;你复杂看它,它就较复杂。在我看来,它其实很简单,无非就是java里面的接口,抽象类,继承,方法覆写这些概念。
View分为两种类型:View和ViewGroup,怎么理解这两种类型?
首先,假象你自己就是Google工程师中的一员,接到一个任务:让你去设计一个android系统出来,你该如何如考虑这个设计?
其实系统完全可以只存在一个View类型,而不需要ViewGroup类型,View类型完全可以实现成既当作原子对象(最小单元),也可以实现成容纳其他的同类对象。但是由于View这个类本身就比较庞大了,已经有8千多行的代码,如果再容纳其他同类对象,那么单个类的代码逻辑势必更加庞大冗杂,维护起来就会非常困难。为了后期维护容易,于是将容纳其他同类对象的逻辑单独抽取出来,继承View,重新定义一个新类,取名为ViewGroup。
顾名思义,View的群组,可以容纳View的对象。
因此,在看源码的时候,你要时时刻刻站在Google工程师的角度思考问题,站在一个设计者的角度去思考,时刻思考Google工程师写这样一行代码,他想做什么?他的初衷是什么?他是怎么想的?你要把他的大脑打开,进入他的想法里,一探究竟;而不是把自己当作观众看客。
一切的概念源自于生活,你可以将View的这两种类型联系到生活中来,站在生活中去寻找相应的物体与之对应起来,这样更容易理解。
我把View比作实体的砖头,把ViewGroup比作空心的砖头,这个空心的砖头里面可以继续容纳其他实体的砖头,即ViewGroup可以容纳View,而View是最小的单元,不能再容纳其他东西,砖头都具备共同的性质,都是泥土制作的,即他们都有共同的行为和属性。将共同的行为和属性抽取出来定义一个类叫做View类,这个View类是一个实体的砖头;除了共同的行为和属性之外,还有其他的特点,比如是空心的特点,可以继续容纳其他东西,将这个特点区别开来重新定义,叫做ViewGroup类。
上面这个比喻,你有没有理解都不要紧。这个只是个人的理解。下面从源码角度进入分析 >>>
示例下:
说明:
setOnTouchListener方法如果返回false,则打印如下log:
zhanghu@winth:~$ adb logcat -s kkkkMainActivity
--------- beginning of system
--------- beginning of main
09-12 13:52:51.147 11059 11059 D kkkkMainActivity: setOnTouchListener
09-12 13:52:51.175 11059 11059 D kkkkMainActivity: setOnTouchListener
09-12 13:52:51.192 11059 11059 D kkkkMainActivity: setOnTouchListener
09-12 13:52:51.210 11059 11059 D kkkkMainActivity: setOnTouchListener
09-12 13:52:51.227 11059 11059 D kkkkMainActivity: setOnTouchListener
09-12 13:52:51.237 11059 11059 D kkkkMainActivity: setOnTouchListener
09-12 13:52:51.238 11059 11059 D kkkkMainActivity: setOnTouchListener
09-12 13:52:51.250 11059 11059 D kkkkMainActivity: setOnClickListener
setOnTouchListener方法如果返回true,则打印如下log:
zhanghu@winth:~$ adb logcat -s kkkkMainActivity
--------- beginning of main
--------- beginning of system
09-12 13:57:12.019 11333 11333 D kkkkMainActivity: setOnTouchListener
09-12 13:57:12.037 11333 11333 D kkkkMainActivity: setOnTouchListener
09-12 13:57:12.055 11333 11333 D kkkkMainActivity: setOnTouchListener
09-12 13:57:12.067 11333 11333 D kkkkMainActivity: setOnTouchListener
09-12 13:57:12.067 11333 11333 D kkkkMainActivity: setOnTouchListener
总结:View如果既设置了touch监听,又设置了click监听,见上示例,click监听是否执行,取决于touch监听是否返回false。即:如果touch监听消费了事件(返回了true),则click监听不会再执行。
2 dispatchTouchEvent方法中,如果touch监听返回了false,则继续执行onTouchEvent方法,下面看onTouchEvent方法:
说明:
上面代码已经比较详细的注释说明了,此处不再赘述。总结一条:
View对象(如TextView,Buttion,etc.)如果既设置了touch监听,又设置了click监听:
A 如果touch事件监听返回false,则先响应touch事件,再响应click事件
B 如果touch事件监听返回true, 则只响应touch事件,不再响应click事件
2.事件的传递流向永远是:父view-->子view,总结如下:
A 当且仅当:onInterceptTouchEvent返回true,且disallowIntercept为false时(默认值即为false,即子View没有明确要求父View不拦截事件),事件不会往子View传递,完全交由父View去处理事件,此时把父view(ViewGroup类型)当成常规的View(View类型)去处理事件。
B 当且仅当:disallowIntercept为true时(即子View明确要求父View不拦截事件),不论onInterceptTouchEvent返回true还是返回false,Down事件都会传递给子View,若找到了处理事件的目标子View(mMotionTarget),后续的事件(move,up)是否继续传递给给目标子View.
取决于在此期间:有没有改变disallowIntercept的值,使disallowIntercept为false及onInterceptTouchEvent是否能够返回true,如果满足这两个条件,则父view会拦截后续的事件(move,up),不会再往目标子View传递。如果不满足,则后续的事件(move,up)继续传递给目标子View。
C 如果进入了 if (disallowIntercept || !onInterceptTouchEvent(ev)) 判断去寻找目标子view,但是没有找到能够处理事件的目标子view,则事件又会回到父View,此时会把ViewGroup当成常规的View类型,调用super.dispatchTouchEvent(ev)去进行常规view事件的处理。
D 能否走到ViewGroup的onTouchEvent方法,得看是否会将ViewGroup当作常规的view类型来处理,如满足if (target == null) 这个条件或target就是ViewGroup(此时ViewGroup就是处理事件的目标view,此时把它当作常规的view)
以上ABCD已经说明了ViewGroup事件处理包含的所有情况,我觉得已经说得比较清楚了,如果你觉得还不是很清楚,那我强烈建议你看源码再结合demo验证下所得到的结论,一切都清楚了。因为答案就在源码中,看源码反倒更容易理解,因为文字总是不能够表达的尽如人意。
示例一 通过requestDisallowInterceptTouchEvent(true)明确要求父view不拦截事件,而父CustomFrameLayout的onInterceptTouchEvent方法返回了true,表达的意图是想要拦截事件,但是由于disallowIntercept为true了,即使父CustomFrameLayout的onInterceptTouchEvent方法返回了true,也不会执行onInterceptTouchEvent方法的。下面验证下:
CustomTextView.java
package com.android.touchtest;
import android.content.Context;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.TextView;
public class CustomTextView extends TextView {
private static final String TAG = "kkkkkkkk-CustomTextView";
public CustomTextView(Context context, @Nullable AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr);
}
public CustomTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public CustomTextView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.d(TAG, "dispatchTouchEvent " + codeToString(event.getAction()));
return super.dispatchTouchEvent(event);
}
private String codeToString( int code) {
switch (code) {
case MotionEvent.ACTION_DOWN:
return "ACTION_DOWN";
case MotionEvent.ACTION_UP:
return "ACTION_UP";
case MotionEvent.ACTION_MOVE:
return "ACTION_MOVE";
case MotionEvent.ACTION_CANCEL:
return "ACTION_CANCEL";
default:
return "";
}
}
}
CustomFrameLayout.java
package com.android.touchtest;
import android.content.Context;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.FrameLayout;
public class CustomFrameLayout extends FrameLayout {
private static final String TAG = "kkkkkkkk-CustomFrameLayout";
public CustomFrameLayout(Context context, @Nullable AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr);
}
public CustomFrameLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public CustomFrameLayout(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d(TAG, "onInterceptTouchEvent " + codeToString(ev.getAction()));
// return super.onInterceptTouchEvent(ev); //super.onInterceptTouchEvent(ev) = false
return true; //想要拦截事件,具体最后是否会拦截事件,取决于子对父的disallowIntercept的值
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG, "onTouchEvent " + codeToString(event.getAction()));
return super.onTouchEvent(event);
}
private String codeToString( int code) {
switch (code) {
case MotionEvent.ACTION_DOWN:
return "ACTION_DOWN";
case MotionEvent.ACTION_UP:
return "ACTION_UP";
case MotionEvent.ACTION_MOVE:
return "ACTION_MOVE";
case MotionEvent.ACTION_CANCEL:
return "ACTION_CANCEL";
default:
return "";
}
}
}
activity_main.xml
MainActivity.java
package com.android.touchtest;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
public class MainActivity extends Activity {
private static final String TAG = "kkkkkkkk-MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
setCustomTextViewListener();
// setCustomFrameLayoutListener();
}
private void setCustomFrameLayoutListener() {
CustomFrameLayout mCustomFrameLayout = (CustomFrameLayout) findViewById(R.id.custom_frame_layout);
mCustomFrameLayout.setOnTouchListener( new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
// TODO
return false;
}
});
mCustomFrameLayout.setOnClickListener( new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d(TAG, "mCustomFrameLayout setOnClickListener");
}
});
}
private void setCustomTextViewListener() {
CustomTextView mCustomTextView = (CustomTextView) findViewById(R.id.custom_text_view);
// 明确要求父view不拦截事件,此处置为true了,系统会在up或cancel事件时重置为false,所以
// 如果希望父view一直不拦截事件,则不应该依赖于此处。
mCustomTextView.getParent().requestDisallowInterceptTouchEvent( true);
mCustomTextView.setOnTouchListener( new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
// TODO
return true;
}
});
mCustomTextView.setOnClickListener( new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d(TAG, "mCustomTextView setOnClickListener");
}
});
}
private String codeToString( int code) {
switch (code) {
case MotionEvent.ACTION_DOWN:
return "ACTION_DOWN";
case MotionEvent.ACTION_UP:
return "ACTION_UP";
case MotionEvent.ACTION_MOVE:
return "ACTION_MOVE";
case MotionEvent.ACTION_CANCEL:
return "ACTION_CANCEL";
default:
return "";
}
}
}
滑动黑色区域打印log:
10035:D/kkkkkkkk-CustomTextView( 1444): dispatchTouchEvent ACTION_DOWN
10036:D/kkkkkkkk-CustomTextView( 1444): dispatchTouchEvent ACTION_MOVE
10037:D/kkkkkkkk-CustomTextView( 1444): dispatchTouchEvent ACTION_MOVE
10038:D/kkkkkkkk-CustomTextView( 1444): dispatchTouchEvent ACTION_MOVE
10039:D/kkkkkkkk-CustomTextView( 1444): dispatchTouchEvent ACTION_MOVE
10040:D/kkkkkkkk-CustomTextView( 1444): dispatchTouchEvent ACTION_MOVE
10041:D/kkkkkkkk-CustomTextView( 1444): dispatchTouchEvent ACTION_MOVE
10042:D/kkkkkkkk-CustomTextView( 1444): dispatchTouchEvent ACTION_MOVE
10043:D/kkkkkkkk-CustomTextView( 1444): dispatchTouchEvent ACTION_MOVE
10044:D/kkkkkkkk-CustomTextView( 1444): dispatchTouchEvent ACTION_MOVE
10045:D/kkkkkkkk-CustomTextView( 1444): dispatchTouchEvent ACTION_MOVE
10046:D/kkkkkkkk-CustomTextView( 1444): dispatchTouchEvent ACTION_UP
示例二 子View没有调用requestDisallowInterceptTouchEvent(true)方法去明确要求父View不拦截事件,事件是否会传递给子view,由onInterceptTouchEvent方法返回值决定,上面示例中的onInterceptTouchEvent返回的是true,所以,事件不会往子view传递。验证下:
基于示例一,注释掉MainActivity类中的mCustomTextView.getParent().requestDisallowInterceptTouchEvent(true);这一句,打印log如下:
693:D/kkkkkkkk-CustomFrameLayout( 1586): onInterceptTouchEvent ACTION_DOWN
694:D/kkkkkkkk-CustomFrameLayout( 1586): onTouchEvent ACTION_DOWN
打印Log如下:
970:D/kkkkkkkk-CustomTextView( 1687): dispatchTouchEvent ACTION_DOWN
971:D/kkkkkkkk-CustomTextView( 1687): dispatchTouchEvent ACTION_MOVE
972:D/kkkkkkkk-CustomFrameLayout( 1687): onInterceptTouchEvent ACTION_MOVE
973:D/kkkkkkkk-CustomTextView( 1687): dispatchTouchEvent ACTION_CANCEL
974:D/kkkkkkkk-CustomFrameLayout( 1687): onTouchEvent ACTION_MOVE
975:D/kkkkkkkk-CustomFrameLayout( 1687): onTouchEvent ACTION_MOVE
976:D/kkkkkkkk-CustomFrameLayout( 1687): onTouchEvent ACTION_MOVE
977:D/kkkkkkkk-CustomFrameLayout( 1687): onTouchEvent ACTION_MOVE
978:D/kkkkkkkk-CustomFrameLayout( 1687): onTouchEvent ACTION_MOVE
979:D/kkkkkkkk-CustomFrameLayout( 1687): onTouchEvent ACTION_MOVE
980:D/kkkkkkkk-CustomFrameLayout( 1687): onTouchEvent ACTION_MOVE
981:D/kkkkkkkk-CustomFrameLayout( 1687): onTouchEvent ACTION_MOVE
982:D/kkkkkkkk-CustomFrameLayout( 1687): onTouchEvent ACTION_MOVE
983:D/kkkkkkkk-CustomFrameLayout( 1687): onTouchEvent ACTION_UP
继续修改下,将MainActivity类的setCustomFrameLayoutListener();的注释放开,mCustomFrameLayout.setOnTouchListener的onTouch方法返回true,即父view要消费touch事件,如下修改:
打印log如下:
768:D/kkkkkkkk-CustomTextView( 1755): dispatchTouchEvent ACTION_DOWN
769:D/kkkkkkkk-CustomTextView( 1755): dispatchTouchEvent ACTION_MOVE
770:D/kkkkkkkk-CustomFrameLayout( 1755): onInterceptTouchEvent ACTION_MOVE
771:D/kkkkkkkk-CustomTextView( 1755): dispatchTouchEvent ACTION_CANCEL
772:D/kkkkkkkk-MainActivity( 1755): mCustomFrameLayout setOnTouchListener
773:D/kkkkkkkk-MainActivity( 1755): mCustomFrameLayout setOnTouchListener
774:D/kkkkkkkk-MainActivity( 1755): mCustomFrameLayout setOnTouchListener
775:D/kkkkkkkk-MainActivity( 1755): mCustomFrameLayout setOnTouchListener
从上面log可以看到,示例三刚好验证了满足 dispatchTouchEvent的 if (!disallowIntercept && onInterceptTouchEvent(ev)) 这个条件及下一个move事件的if (target == null) 这个条件,所以走到了CustomFrameLayout的onTouch方法里面,如果CustomFrameLayout的onTouch方法返回了false,则会走onTouchEvent方法(见View的dispatchTouchEvent方法)
上面示例了交互的情况,其他情况都比较简单。
目前Android版本已经到了7.0(nougat)了,Android 随着版本升级,touch事件的源码也在跟随着系统的升级而写得越来越复杂,加入了很多旁枝末节,这些旁枝末节,对于分析流程是一种干扰;由于Android的版本升级是向下兼容的,万变不离其宗,研究Android早期的版本,可以更容易理解touch事件的分发,本篇以Android1.6版本的源码进行讲解,由简及繁,理解了早期的源码,再进入高版本的研究也会更容易许多。
View事件的派发其实非常简单,不是想象的那么复杂,你也别把它看得那么复杂,你简单看它,它就很简单;你复杂看它,它就较复杂。在我看来,它其实很简单,无非就是java里面的接口,抽象类,继承,方法覆写这些概念。
View分为两种类型:View和ViewGroup,怎么理解这两种类型?
首先,假象你自己就是Google工程师中的一员,接到一个任务:让你去设计一个android系统出来,你该如何如考虑这个设计?
其实系统完全可以只存在一个View类型,而不需要ViewGroup类型,View类型完全可以实现成既当作原子对象(最小单元),也可以实现成容纳其他的同类对象。但是由于View这个类本身就比较庞大了,已经有8千多行的代码,如果再容纳其他同类对象,那么单个类的代码逻辑势必更加庞大冗杂,维护起来就会非常困难。为了后期维护容易,于是将容纳其他同类对象的逻辑单独抽取出来,继承View,重新定义一个新类,取名为ViewGroup。
顾名思义,View的群组,可以容纳View的对象。
因此,在看源码的时候,你要时时刻刻站在Google工程师的角度思考问题,站在一个设计者的角度去思考,时刻思考Google工程师写这样一行代码,他想做什么?他的初衷是什么?他是怎么想的?你要把他的大脑打开,进入他的想法里,一探究竟;而不是把自己当作观众看客。
一切的概念源自于生活,你可以将View的这两种类型联系到生活中来,站在生活中去寻找相应的物体与之对应起来,这样更容易理解。
我把View比作实体的砖头,把ViewGroup比作空心的砖头,这个空心的砖头里面可以继续容纳其他实体的砖头,即ViewGroup可以容纳View,而View是最小的单元,不能再容纳其他东西,砖头都具备共同的性质,都是泥土制作的,即他们都有共同的行为和属性。将共同的行为和属性抽取出来定义一个类叫做View类,这个View类是一个实体的砖头;除了共同的行为和属性之外,还有其他的特点,比如是空心的特点,可以继续容纳其他东西,将这个特点区别开来重新定义,叫做ViewGroup类。
上面这个比喻,你有没有理解都不要紧。这个只是个人的理解。下面从源码角度进入分析 >>>
一 View事件的分析和总结
public
boolean dispatchTouchEvent(MotionEvent event) {
//如果当前view或目标view已经消费了touch事件,则直接返回true,否则调用onTouchEvent方法进行后续点击事件的判断处理
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch( this, event)) {
return true;
}
return onTouchEvent(event);
}
//如果当前view或目标view已经消费了touch事件,则直接返回true,否则调用onTouchEvent方法进行后续点击事件的判断处理
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch( this, event)) {
return true;
}
return onTouchEvent(event);
}
示例下:
TextView mTextView = (TextView) findViewById(R.id.text);
mTextView.setOnTouchListener( new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d(TAG, "setOnTouchListener");
// 1.此处返回false,则先响应touch事件,再响应click事件
// 2.此处返回true,则只响应touch事件,不再响应click事件
return false;
}
});
mTextView.setOnClickListener( new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d(TAG, "setOnClickListener");
}
});
mTextView.setOnTouchListener( new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d(TAG, "setOnTouchListener");
// 1.此处返回false,则先响应touch事件,再响应click事件
// 2.此处返回true,则只响应touch事件,不再响应click事件
return false;
}
});
mTextView.setOnClickListener( new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d(TAG, "setOnClickListener");
}
});
说明:
setOnTouchListener方法如果返回false,则打印如下log:
zhanghu@winth:~$ adb logcat -s kkkkMainActivity
--------- beginning of system
--------- beginning of main
09-12 13:52:51.147 11059 11059 D kkkkMainActivity: setOnTouchListener
09-12 13:52:51.175 11059 11059 D kkkkMainActivity: setOnTouchListener
09-12 13:52:51.192 11059 11059 D kkkkMainActivity: setOnTouchListener
09-12 13:52:51.210 11059 11059 D kkkkMainActivity: setOnTouchListener
09-12 13:52:51.227 11059 11059 D kkkkMainActivity: setOnTouchListener
09-12 13:52:51.237 11059 11059 D kkkkMainActivity: setOnTouchListener
09-12 13:52:51.238 11059 11059 D kkkkMainActivity: setOnTouchListener
09-12 13:52:51.250 11059 11059 D kkkkMainActivity: setOnClickListener
setOnTouchListener方法如果返回true,则打印如下log:
zhanghu@winth:~$ adb logcat -s kkkkMainActivity
--------- beginning of main
--------- beginning of system
09-12 13:57:12.019 11333 11333 D kkkkMainActivity: setOnTouchListener
09-12 13:57:12.037 11333 11333 D kkkkMainActivity: setOnTouchListener
09-12 13:57:12.055 11333 11333 D kkkkMainActivity: setOnTouchListener
09-12 13:57:12.067 11333 11333 D kkkkMainActivity: setOnTouchListener
09-12 13:57:12.067 11333 11333 D kkkkMainActivity: setOnTouchListener
总结:View如果既设置了touch监听,又设置了click监听,见上示例,click监听是否执行,取决于touch监听是否返回false。即:如果touch监听消费了事件(返回了true),则click监听不会再执行。
2 dispatchTouchEvent方法中,如果touch监听返回了false,则继续执行onTouchEvent方法,下面看onTouchEvent方法:
public
boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
// 如果当前View diable了,直接返回不进行事件处理,假如当前view属于可点击的,则返回true,标志应该消费事件;否则返回false
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));
}
// 给当前view的touch代理对象一个机会去处理touch事件,如果当前view的touch代理对象的onTouchEvent方法返回了true,
// 则此处直接返回true
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
//当是点击事件或长点击事件,则进入循环,处理完之后,返回true
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
if ((mPrivateFlags & PRESSED) != 0) { //当没有被ACTION_CANCEL时,进入循环
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
//请求focus,需要聚焦的控件,第一次点击是聚焦,第二次点击才响应点击事件,所以此处一般返回false
focusTaken = requestFocus();
}
if (!mHasPerformedLongPress) {
//如果是轻轻的点击,则移除长按事件的检查
// This is a tap, so remove the longpress check
if (mPendingCheckForLongPress != null) {
removeCallbacks(mPendingCheckForLongPress);
}
// 如果我们已经按压了,且没有focusTaken时,执行点击事件
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
performClick(); //执行点击事件
}
}
if (mUnsetPressedState == null) {
//构建取消按压的Runnable对象,提供一个取消按压的能力
mUnsetPressedState = new UnsetPressedState();
}
// 如果取消按压的操作没有执行成功,则立即执行取消按压的动作
if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
}
break;
case MotionEvent.ACTION_DOWN: //按下事件
mPrivateFlags |= PRESSED; //添加标志,表示已经按压了
refreshDrawableState(); //更新drawable state
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) { //如果是长按事件,则处理长按操作
postCheckForLongClick();
}
break;
case MotionEvent.ACTION_CANCEL:
mPrivateFlags &= ~PRESSED; //添加标志,表示按压要取消掉
refreshDrawableState(); //更新drawable state
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)) {
// 移动到button区域外面了
// Outside button
if ((mPrivateFlags & PRESSED) != 0) { //当没有被ACTION_CANCEL掉时
// Remove any future long press checks
if (mPendingCheckForLongPress != null) {
//移除长按事件的检查操作
removeCallbacks(mPendingCheckForLongPress);
}
// 将状态切换成not pressed,并更新drawable state
// Need to switch from pressed to not pressed
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
}
} else {
// 移动到button区域内部了
// Inside button
if ((mPrivateFlags & PRESSED) == 0) { //之前没有按压当前buttion,移动过来才按压上的
// Need to switch from not pressed to pressed
mPrivateFlags |= PRESSED;
refreshDrawableState();
}
}
break;
}
return true;
}
return false;
}
final int viewFlags = mViewFlags;
// 如果当前View diable了,直接返回不进行事件处理,假如当前view属于可点击的,则返回true,标志应该消费事件;否则返回false
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));
}
// 给当前view的touch代理对象一个机会去处理touch事件,如果当前view的touch代理对象的onTouchEvent方法返回了true,
// 则此处直接返回true
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
//当是点击事件或长点击事件,则进入循环,处理完之后,返回true
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
if ((mPrivateFlags & PRESSED) != 0) { //当没有被ACTION_CANCEL时,进入循环
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
//请求focus,需要聚焦的控件,第一次点击是聚焦,第二次点击才响应点击事件,所以此处一般返回false
focusTaken = requestFocus();
}
if (!mHasPerformedLongPress) {
//如果是轻轻的点击,则移除长按事件的检查
// This is a tap, so remove the longpress check
if (mPendingCheckForLongPress != null) {
removeCallbacks(mPendingCheckForLongPress);
}
// 如果我们已经按压了,且没有focusTaken时,执行点击事件
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
performClick(); //执行点击事件
}
}
if (mUnsetPressedState == null) {
//构建取消按压的Runnable对象,提供一个取消按压的能力
mUnsetPressedState = new UnsetPressedState();
}
// 如果取消按压的操作没有执行成功,则立即执行取消按压的动作
if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
}
break;
case MotionEvent.ACTION_DOWN: //按下事件
mPrivateFlags |= PRESSED; //添加标志,表示已经按压了
refreshDrawableState(); //更新drawable state
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) { //如果是长按事件,则处理长按操作
postCheckForLongClick();
}
break;
case MotionEvent.ACTION_CANCEL:
mPrivateFlags &= ~PRESSED; //添加标志,表示按压要取消掉
refreshDrawableState(); //更新drawable state
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)) {
// 移动到button区域外面了
// Outside button
if ((mPrivateFlags & PRESSED) != 0) { //当没有被ACTION_CANCEL掉时
// Remove any future long press checks
if (mPendingCheckForLongPress != null) {
//移除长按事件的检查操作
removeCallbacks(mPendingCheckForLongPress);
}
// 将状态切换成not pressed,并更新drawable state
// Need to switch from pressed to not pressed
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
}
} else {
// 移动到button区域内部了
// Inside button
if ((mPrivateFlags & PRESSED) == 0) { //之前没有按压当前buttion,移动过来才按压上的
// Need to switch from not pressed to pressed
mPrivateFlags |= PRESSED;
refreshDrawableState();
}
}
break;
}
return true;
}
return false;
}
说明:
上面代码已经比较详细的注释说明了,此处不再赘述。总结一条:
View对象(如TextView,Buttion,etc.)如果既设置了touch监听,又设置了click监听:
A 如果touch事件监听返回false,则先响应touch事件,再响应click事件
B 如果touch事件监听返回true, 则只响应touch事件,不再响应click事件
二 ViewGroup事件的分析和总结
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
final float xf = ev.getX();
final float yf = ev.getY();
final float scrolledXFloat = xf + mScrollX;
final float scrolledYFloat = yf + mScrollY;
final Rect frame = mTempRect;
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (action == MotionEvent.ACTION_DOWN) {
if (mMotionTarget != null) {
// this is weird, we got a pen down, but we thought it was
// already down!
// XXX: We should probably send an ACTION_UP to the current
// target.
mMotionTarget = null;
}
// 当前ViewGroup被要求不允许拦截事件,或当前ViewGroup允许去拦截但是并没有真正拦截事件
// 能否进入if判断去寻找能够处理事件的子view,分为两种情况:
// 一.disallowIntercept为false,表示子View没有明确要求父View不拦截事件,此时事件是否往子View传递,取决于onInterceptTouchEvent的返回值
// 1.onInterceptTouchEvent返回false,则进入if判断,表示事件会传递到子View,进入if判断后去寻找处理事件的目标View,如果找到了处理事件的目标view,则将该view赋值给mMotionTarget。
// 2.onInterceptTouchEvent返回true,则不会进入if判断,也就不会去寻找目标View(此时mMotionTarget不会被赋值,即为null),表示父View会拦截事件,不会往子View传递事件。
// 二.disallowIntercept为true,表示子View明确要求父View不拦截事件,不论onInterceptTouchEvent返回true或false,都会进入if判断,去寻找处理事件的目标View。
// If we're disallowing intercept or if we're allowing and we didn't
// intercept
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
// reset this event's action (just to protect ourselves)
ev.setAction(MotionEvent.ACTION_DOWN);
// We know we want to dispatch the event down, find a child
// who can handle it, start with the front-most child.
final int scrolledXInt = ( int) scrolledXFloat;
final int scrolledYInt = ( int) scrolledYFloat;
final View[] children = mChildren;
final int count = mChildrenCount;
for ( int i = count - 1; i >= 0; i--) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
child.getHitRect(frame);
if (frame.contains(scrolledXInt, scrolledYInt)) {
// offset the event to the view's coordinate system
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
// child如果是ViewGroup类型,则继续递归调用本类中的dispatchTouchEvent方法
// child如果是View类型,则调用View类的dispatchTouchEvent方法,具体见View类的dispatchTouchEvent方法分析
if (child.dispatchTouchEvent(ev)) {
// Event handled, we have a target now.
mMotionTarget = child;
return true;
}
// The event didn't get handled, try the next view.
// Don't reset the event's location, it's not
// necessary here.
}
}
}
}
}
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
if (isUpOrCancel) {
// 之前已经将FLAG_DISALLOW_INTERCEPT添加到mGroupFlags了,此处将mGroupFlags中的FLAG_DISALLOW_INTERCEPT
// 取消掉,以重置mGroupFlags
// Note, we've already copied the previous state to our local
// variable, so this takes effect on the next event
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// The event wasn't an ACTION_DOWN, dispatch it to our target if
// we have one.
final View target = mMotionTarget;
if (target == null) { //如果没有找到处理事件的目标View
// We don't have a target, this means we're handling the
// event as a regular view.
ev.setLocation(xf, yf);
// 没有找到处理事件的目标View,则直接调用父类的super.dispatchTouchEvent方法
// 此处可以理解成:
// FrameLayout内有三个子view,但是这三个子view都没有去处理事件,因此就把FrameLayout当成一个常规的View
// 然后将touch事件交给它去处理,例如:当为FrameLayout设置touch事件的时候,这个FrameLayout就去响应touch事件
return super.dispatchTouchEvent(ev);
}
// 如果我们有一个子view处理事件,且没有明确要求ViewGroup不拦截事件,再且onInterceptTouchEvent返回true,则当前的ViewGroup传递一个ACTION_CANCEL事件给子View,
// 并清除子view对象(mMotionTarget置为null),当下一个move事件进来时,由于target已经被mMotionTarget置null了,所以直接调用上一步的super.dispatchTouchEvent(ev),
// 将当前的ViewGroup来当作常规的View类型来处理。
// 举例理解:
// 一个自定义的FrameLayout内有包裹着三个子view,当这个自定义的FrameLayout被允许去拦截事件时,就传给之前处理Down事件的这个子view一个ACTION_CANCEL事件,
// 并把处理Down事件的这个子view对象给清除掉,使这个子view不再处理Down后续的事件,将后续的事件交给这个自定义的FrameLayout来处理(此时将ViewGroup当作常规的view来处理,如果view的onTouch返回了false,则传给onTouchEvent方法去处理点击事件)。
// if have a target, see if we're allowed to and want to intercept its
// events
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
final float xc = scrolledXFloat - ( float) target.mLeft;
final float yc = scrolledYFloat - ( float) target.mTop;
ev.setAction(MotionEvent.ACTION_CANCEL);
ev.setLocation(xc, yc);
if (!target.dispatchTouchEvent(ev)) { //子View去处理ACTION_CANCEL事件
// target didn't handle ACTION_CANCEL. not much we can do
// but they should have.
}
// clear the target
mMotionTarget = null;
// Don't dispatch this event to our own view, because we already
// saw it when intercepting; we just want to give the following
// event to the normal onTouchEvent().
return true;
}
// 如果是up或cancel事件,则将mMotionTarget置null,清除目标view
if (isUpOrCancel) {
mMotionTarget = null;
}
// 子View继续处理后续的move,up或cancel事件
// finally offset the event to the target's coordinate system and
// dispatch the event.
final float xc = scrolledXFloat - ( float) target.mLeft;
final float yc = scrolledYFloat - ( float) target.mTop;
ev.setLocation(xc, yc);
return target.dispatchTouchEvent(ev);
}
public boolean dispatchTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
final float xf = ev.getX();
final float yf = ev.getY();
final float scrolledXFloat = xf + mScrollX;
final float scrolledYFloat = yf + mScrollY;
final Rect frame = mTempRect;
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (action == MotionEvent.ACTION_DOWN) {
if (mMotionTarget != null) {
// this is weird, we got a pen down, but we thought it was
// already down!
// XXX: We should probably send an ACTION_UP to the current
// target.
mMotionTarget = null;
}
// 当前ViewGroup被要求不允许拦截事件,或当前ViewGroup允许去拦截但是并没有真正拦截事件
// 能否进入if判断去寻找能够处理事件的子view,分为两种情况:
// 一.disallowIntercept为false,表示子View没有明确要求父View不拦截事件,此时事件是否往子View传递,取决于onInterceptTouchEvent的返回值
// 1.onInterceptTouchEvent返回false,则进入if判断,表示事件会传递到子View,进入if判断后去寻找处理事件的目标View,如果找到了处理事件的目标view,则将该view赋值给mMotionTarget。
// 2.onInterceptTouchEvent返回true,则不会进入if判断,也就不会去寻找目标View(此时mMotionTarget不会被赋值,即为null),表示父View会拦截事件,不会往子View传递事件。
// 二.disallowIntercept为true,表示子View明确要求父View不拦截事件,不论onInterceptTouchEvent返回true或false,都会进入if判断,去寻找处理事件的目标View。
// If we're disallowing intercept or if we're allowing and we didn't
// intercept
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
// reset this event's action (just to protect ourselves)
ev.setAction(MotionEvent.ACTION_DOWN);
// We know we want to dispatch the event down, find a child
// who can handle it, start with the front-most child.
final int scrolledXInt = ( int) scrolledXFloat;
final int scrolledYInt = ( int) scrolledYFloat;
final View[] children = mChildren;
final int count = mChildrenCount;
for ( int i = count - 1; i >= 0; i--) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
child.getHitRect(frame);
if (frame.contains(scrolledXInt, scrolledYInt)) {
// offset the event to the view's coordinate system
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
// child如果是ViewGroup类型,则继续递归调用本类中的dispatchTouchEvent方法
// child如果是View类型,则调用View类的dispatchTouchEvent方法,具体见View类的dispatchTouchEvent方法分析
if (child.dispatchTouchEvent(ev)) {
// Event handled, we have a target now.
mMotionTarget = child;
return true;
}
// The event didn't get handled, try the next view.
// Don't reset the event's location, it's not
// necessary here.
}
}
}
}
}
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
if (isUpOrCancel) {
// 之前已经将FLAG_DISALLOW_INTERCEPT添加到mGroupFlags了,此处将mGroupFlags中的FLAG_DISALLOW_INTERCEPT
// 取消掉,以重置mGroupFlags
// Note, we've already copied the previous state to our local
// variable, so this takes effect on the next event
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// The event wasn't an ACTION_DOWN, dispatch it to our target if
// we have one.
final View target = mMotionTarget;
if (target == null) { //如果没有找到处理事件的目标View
// We don't have a target, this means we're handling the
// event as a regular view.
ev.setLocation(xf, yf);
// 没有找到处理事件的目标View,则直接调用父类的super.dispatchTouchEvent方法
// 此处可以理解成:
// FrameLayout内有三个子view,但是这三个子view都没有去处理事件,因此就把FrameLayout当成一个常规的View
// 然后将touch事件交给它去处理,例如:当为FrameLayout设置touch事件的时候,这个FrameLayout就去响应touch事件
return super.dispatchTouchEvent(ev);
}
// 如果我们有一个子view处理事件,且没有明确要求ViewGroup不拦截事件,再且onInterceptTouchEvent返回true,则当前的ViewGroup传递一个ACTION_CANCEL事件给子View,
// 并清除子view对象(mMotionTarget置为null),当下一个move事件进来时,由于target已经被mMotionTarget置null了,所以直接调用上一步的super.dispatchTouchEvent(ev),
// 将当前的ViewGroup来当作常规的View类型来处理。
// 举例理解:
// 一个自定义的FrameLayout内有包裹着三个子view,当这个自定义的FrameLayout被允许去拦截事件时,就传给之前处理Down事件的这个子view一个ACTION_CANCEL事件,
// 并把处理Down事件的这个子view对象给清除掉,使这个子view不再处理Down后续的事件,将后续的事件交给这个自定义的FrameLayout来处理(此时将ViewGroup当作常规的view来处理,如果view的onTouch返回了false,则传给onTouchEvent方法去处理点击事件)。
// if have a target, see if we're allowed to and want to intercept its
// events
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
final float xc = scrolledXFloat - ( float) target.mLeft;
final float yc = scrolledYFloat - ( float) target.mTop;
ev.setAction(MotionEvent.ACTION_CANCEL);
ev.setLocation(xc, yc);
if (!target.dispatchTouchEvent(ev)) { //子View去处理ACTION_CANCEL事件
// target didn't handle ACTION_CANCEL. not much we can do
// but they should have.
}
// clear the target
mMotionTarget = null;
// Don't dispatch this event to our own view, because we already
// saw it when intercepting; we just want to give the following
// event to the normal onTouchEvent().
return true;
}
// 如果是up或cancel事件,则将mMotionTarget置null,清除目标view
if (isUpOrCancel) {
mMotionTarget = null;
}
// 子View继续处理后续的move,up或cancel事件
// finally offset the event to the target's coordinate system and
// dispatch the event.
final float xc = scrolledXFloat - ( float) target.mLeft;
final float yc = scrolledYFloat - ( float) target.mTop;
ev.setLocation(xc, yc);
return target.dispatchTouchEvent(ev);
}
总结:
1.不论是View类型,还是ViewGroup类型,其事件都是首先从dispatchTouchEvent开始分发,除非系统的默认设计不能满足事件的分发处理,一般情况下不建议覆写dispatchTouchEvent方法2.事件的传递流向永远是:父view-->子view,总结如下:
A 当且仅当:onInterceptTouchEvent返回true,且disallowIntercept为false时(默认值即为false,即子View没有明确要求父View不拦截事件),事件不会往子View传递,完全交由父View去处理事件,此时把父view(ViewGroup类型)当成常规的View(View类型)去处理事件。
B 当且仅当:disallowIntercept为true时(即子View明确要求父View不拦截事件),不论onInterceptTouchEvent返回true还是返回false,Down事件都会传递给子View,若找到了处理事件的目标子View(mMotionTarget),后续的事件(move,up)是否继续传递给给目标子View.
取决于在此期间:有没有改变disallowIntercept的值,使disallowIntercept为false及onInterceptTouchEvent是否能够返回true,如果满足这两个条件,则父view会拦截后续的事件(move,up),不会再往目标子View传递。如果不满足,则后续的事件(move,up)继续传递给目标子View。
C 如果进入了 if (disallowIntercept || !onInterceptTouchEvent(ev)) 判断去寻找目标子view,但是没有找到能够处理事件的目标子view,则事件又会回到父View,此时会把ViewGroup当成常规的View类型,调用super.dispatchTouchEvent(ev)去进行常规view事件的处理。
D 能否走到ViewGroup的onTouchEvent方法,得看是否会将ViewGroup当作常规的view类型来处理,如满足if (target == null) 这个条件或target就是ViewGroup(此时ViewGroup就是处理事件的目标view,此时把它当作常规的view)
以上ABCD已经说明了ViewGroup事件处理包含的所有情况,我觉得已经说得比较清楚了,如果你觉得还不是很清楚,那我强烈建议你看源码再结合demo验证下所得到的结论,一切都清楚了。因为答案就在源码中,看源码反倒更容易理解,因为文字总是不能够表达的尽如人意。
事件分析和总结讲述完毕!!!!!
到此可以止步了,后面的内容主要是通过demo验证这个结论的,不想看的,可以到此终止了。
博客原文:http://blog.csdn.net/yelangjueqi/article/details/52525979
示例一 通过requestDisallowInterceptTouchEvent(true)明确要求父view不拦截事件,而父CustomFrameLayout的onInterceptTouchEvent方法返回了true,表达的意图是想要拦截事件,但是由于disallowIntercept为true了,即使父CustomFrameLayout的onInterceptTouchEvent方法返回了true,也不会执行onInterceptTouchEvent方法的。下面验证下:
CustomTextView.java
package com.android.touchtest;
import android.content.Context;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.TextView;
public class CustomTextView extends TextView {
private static final String TAG = "kkkkkkkk-CustomTextView";
public CustomTextView(Context context, @Nullable AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr);
}
public CustomTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public CustomTextView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.d(TAG, "dispatchTouchEvent " + codeToString(event.getAction()));
return super.dispatchTouchEvent(event);
}
private String codeToString( int code) {
switch (code) {
case MotionEvent.ACTION_DOWN:
return "ACTION_DOWN";
case MotionEvent.ACTION_UP:
return "ACTION_UP";
case MotionEvent.ACTION_MOVE:
return "ACTION_MOVE";
case MotionEvent.ACTION_CANCEL:
return "ACTION_CANCEL";
default:
return "";
}
}
}
CustomFrameLayout.java
package com.android.touchtest;
import android.content.Context;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.FrameLayout;
public class CustomFrameLayout extends FrameLayout {
private static final String TAG = "kkkkkkkk-CustomFrameLayout";
public CustomFrameLayout(Context context, @Nullable AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr);
}
public CustomFrameLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public CustomFrameLayout(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d(TAG, "onInterceptTouchEvent " + codeToString(ev.getAction()));
// return super.onInterceptTouchEvent(ev); //super.onInterceptTouchEvent(ev) = false
return true; //想要拦截事件,具体最后是否会拦截事件,取决于子对父的disallowIntercept的值
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG, "onTouchEvent " + codeToString(event.getAction()));
return super.onTouchEvent(event);
}
private String codeToString( int code) {
switch (code) {
case MotionEvent.ACTION_DOWN:
return "ACTION_DOWN";
case MotionEvent.ACTION_UP:
return "ACTION_UP";
case MotionEvent.ACTION_MOVE:
return "ACTION_MOVE";
case MotionEvent.ACTION_CANCEL:
return "ACTION_CANCEL";
default:
return "";
}
}
}
activity_main.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center" >
<com.android.touchtest.CustomFrameLayout
android:id="@+id/custom_frame_layout"
android:layout_width="200dip"
android:layout_height="200dip"
android:layout_gravity="center"
android:background="#00ff00" >
<com.android.touchtest.CustomTextView
android:id="@+id/custom_text_view"
android:layout_width="130dip"
android:layout_height="100dip"
android:layout_gravity="center"
android:background="#000000"
android:text="CustomTextView"
android:textColor="#FFFFFF" />
</com.android.touchtest.CustomFrameLayout>
</FrameLayout>
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center" >
<com.android.touchtest.CustomFrameLayout
android:id="@+id/custom_frame_layout"
android:layout_width="200dip"
android:layout_height="200dip"
android:layout_gravity="center"
android:background="#00ff00" >
<com.android.touchtest.CustomTextView
android:id="@+id/custom_text_view"
android:layout_width="130dip"
android:layout_height="100dip"
android:layout_gravity="center"
android:background="#000000"
android:text="CustomTextView"
android:textColor="#FFFFFF" />
</com.android.touchtest.CustomFrameLayout>
</FrameLayout>
MainActivity.java
package com.android.touchtest;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
public class MainActivity extends Activity {
private static final String TAG = "kkkkkkkk-MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
setCustomTextViewListener();
// setCustomFrameLayoutListener();
}
private void setCustomFrameLayoutListener() {
CustomFrameLayout mCustomFrameLayout = (CustomFrameLayout) findViewById(R.id.custom_frame_layout);
mCustomFrameLayout.setOnTouchListener( new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
// TODO
return false;
}
});
mCustomFrameLayout.setOnClickListener( new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d(TAG, "mCustomFrameLayout setOnClickListener");
}
});
}
private void setCustomTextViewListener() {
CustomTextView mCustomTextView = (CustomTextView) findViewById(R.id.custom_text_view);
// 明确要求父view不拦截事件,此处置为true了,系统会在up或cancel事件时重置为false,所以
// 如果希望父view一直不拦截事件,则不应该依赖于此处。
mCustomTextView.getParent().requestDisallowInterceptTouchEvent( true);
mCustomTextView.setOnTouchListener( new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
// TODO
return true;
}
});
mCustomTextView.setOnClickListener( new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d(TAG, "mCustomTextView setOnClickListener");
}
});
}
private String codeToString( int code) {
switch (code) {
case MotionEvent.ACTION_DOWN:
return "ACTION_DOWN";
case MotionEvent.ACTION_UP:
return "ACTION_UP";
case MotionEvent.ACTION_MOVE:
return "ACTION_MOVE";
case MotionEvent.ACTION_CANCEL:
return "ACTION_CANCEL";
default:
return "";
}
}
}
滑动黑色区域打印log:
10035:D/kkkkkkkk-CustomTextView( 1444): dispatchTouchEvent ACTION_DOWN
10036:D/kkkkkkkk-CustomTextView( 1444): dispatchTouchEvent ACTION_MOVE
10037:D/kkkkkkkk-CustomTextView( 1444): dispatchTouchEvent ACTION_MOVE
10038:D/kkkkkkkk-CustomTextView( 1444): dispatchTouchEvent ACTION_MOVE
10039:D/kkkkkkkk-CustomTextView( 1444): dispatchTouchEvent ACTION_MOVE
10040:D/kkkkkkkk-CustomTextView( 1444): dispatchTouchEvent ACTION_MOVE
10041:D/kkkkkkkk-CustomTextView( 1444): dispatchTouchEvent ACTION_MOVE
10042:D/kkkkkkkk-CustomTextView( 1444): dispatchTouchEvent ACTION_MOVE
10043:D/kkkkkkkk-CustomTextView( 1444): dispatchTouchEvent ACTION_MOVE
10044:D/kkkkkkkk-CustomTextView( 1444): dispatchTouchEvent ACTION_MOVE
10045:D/kkkkkkkk-CustomTextView( 1444): dispatchTouchEvent ACTION_MOVE
10046:D/kkkkkkkk-CustomTextView( 1444): dispatchTouchEvent ACTION_UP
示例二 子View没有调用requestDisallowInterceptTouchEvent(true)方法去明确要求父View不拦截事件,事件是否会传递给子view,由onInterceptTouchEvent方法返回值决定,上面示例中的onInterceptTouchEvent返回的是true,所以,事件不会往子view传递。验证下:
基于示例一,注释掉MainActivity类中的mCustomTextView.getParent().requestDisallowInterceptTouchEvent(true);这一句,打印log如下:
693:D/kkkkkkkk-CustomFrameLayout( 1586): onInterceptTouchEvent ACTION_DOWN
694:D/kkkkkkkk-CustomFrameLayout( 1586): onTouchEvent ACTION_DOWN
基于示例一,CustomTextView类的dispatchTouchEvent方法修改如下:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.d(TAG, "dispatchTouchEvent " + codeToString(event.getAction()));
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_UP:
break;
case MotionEvent.ACTION_MOVE:
getParent().requestDisallowInterceptTouchEvent(false);
break;
default:
break;
}
return super.dispatchTouchEvent(event);
}
打印Log如下:
970:D/kkkkkkkk-CustomTextView( 1687): dispatchTouchEvent ACTION_DOWN
971:D/kkkkkkkk-CustomTextView( 1687): dispatchTouchEvent ACTION_MOVE
972:D/kkkkkkkk-CustomFrameLayout( 1687): onInterceptTouchEvent ACTION_MOVE
973:D/kkkkkkkk-CustomTextView( 1687): dispatchTouchEvent ACTION_CANCEL
974:D/kkkkkkkk-CustomFrameLayout( 1687): onTouchEvent ACTION_MOVE
975:D/kkkkkkkk-CustomFrameLayout( 1687): onTouchEvent ACTION_MOVE
976:D/kkkkkkkk-CustomFrameLayout( 1687): onTouchEvent ACTION_MOVE
977:D/kkkkkkkk-CustomFrameLayout( 1687): onTouchEvent ACTION_MOVE
978:D/kkkkkkkk-CustomFrameLayout( 1687): onTouchEvent ACTION_MOVE
979:D/kkkkkkkk-CustomFrameLayout( 1687): onTouchEvent ACTION_MOVE
980:D/kkkkkkkk-CustomFrameLayout( 1687): onTouchEvent ACTION_MOVE
981:D/kkkkkkkk-CustomFrameLayout( 1687): onTouchEvent ACTION_MOVE
982:D/kkkkkkkk-CustomFrameLayout( 1687): onTouchEvent ACTION_MOVE
983:D/kkkkkkkk-CustomFrameLayout( 1687): onTouchEvent ACTION_UP
继续修改下,将MainActivity类的setCustomFrameLayoutListener();的注释放开,mCustomFrameLayout.setOnTouchListener的onTouch方法返回true,即父view要消费touch事件,如下修改:
mCustomFrameLayout.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d(TAG, "mCustomFrameLayout setOnTouchListener");
return true;
}
});
打印log如下:
768:D/kkkkkkkk-CustomTextView( 1755): dispatchTouchEvent ACTION_DOWN
769:D/kkkkkkkk-CustomTextView( 1755): dispatchTouchEvent ACTION_MOVE
770:D/kkkkkkkk-CustomFrameLayout( 1755): onInterceptTouchEvent ACTION_MOVE
771:D/kkkkkkkk-CustomTextView( 1755): dispatchTouchEvent ACTION_CANCEL
772:D/kkkkkkkk-MainActivity( 1755): mCustomFrameLayout setOnTouchListener
773:D/kkkkkkkk-MainActivity( 1755): mCustomFrameLayout setOnTouchListener
774:D/kkkkkkkk-MainActivity( 1755): mCustomFrameLayout setOnTouchListener
775:D/kkkkkkkk-MainActivity( 1755): mCustomFrameLayout setOnTouchListener
从上面log可以看到,示例三刚好验证了满足 dispatchTouchEvent的 if (!disallowIntercept && onInterceptTouchEvent(ev)) 这个条件及下一个move事件的if (target == null) 这个条件,所以走到了CustomFrameLayout的onTouch方法里面,如果CustomFrameLayout的onTouch方法返回了false,则会走onTouchEvent方法(见View的dispatchTouchEvent方法)
上面示例了交互的情况,其他情况都比较简单。
Android 高版本中,调用requestDisallowInterceptTouchEvent可能失效,见 探究requestDisallowInterceptTouchEvent失效的原因
查看源码:
Android 1.6 View.java
Android 1.6 ViewGroup.java