下面我们来讨论Android事件分发机制.
#一、点击事件小例子分析
为了分析安卓事件分发机制,我们先分析一个小例子。项目名叫做ClickExample1.下载地址:
1.项目源码
界面如下:
布局文件如下,布局中1个LinearLayout中有1个Button按钮。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/my_linearLayout">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Click me!"
android:id="@+id/my_btn"
android:layout_gravity="center_vertical"/>
</LinearLayout>
Activity如下 ,实现了 OnTouchListenser和OnClickListener两个接口。
public class MainActivity extends Activity implements View.OnTouchListener,View.OnClickListener{
private LinearLayout mLayout;
private Button mButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mLayout =(LinearLayout)this.findViewById(R.id.my_linearLayout);
mButton=(Button)this.findViewById(R.id.my_btn);
mLayout.setOnTouchListener(this);
mButton.setOnTouchListener(this);
mLayout.setOnClickListener(this);
mButton.setOnClickListener(this);
}
@Override
public void onClick(View v) {
Log.i("ClickExample1","OnClickListener--OnClick--"+v);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.i("ClickExample1","OnTouchListener--OnTouch--action="+event.getAction()+"--"+v);
return false;
}
}
##2.事件分析
主要对onClick和onTouch两个方法进行分析
事件1:点击Button区域时打印如下:
事件2.点击除过Button以外区域的其他地方时打印如下:
事件3.点击Button时按在Button区域,并在buton区域晃动一下松开后的打印如下:
其中onTouch
方法中有一个MotionEvent
参数,该类中定义了点击事件类型,并在事件发生时传递给onTouch方法,事件类型以及它们的的值定义在MotionEvent 类中,常见的值有下面几种:
public static final int ACTION_DOWN = 0;
public static final int ACTION_UP = 1;
public static final int ACTION_MOVE = 2;
通过对log日志的分析,我们可以得出下面的结论:
事件1:onTouch(ACTION_DOWN)
->onTouch(ACTION_UP)
->onClick()
事件2:onTouch(ACTION_DOWN)
->onTouch(ACTION_UP)
->onClick()
事件3:onTouch(ACTION_DOWN)
->onTouch(ACTION_MOVE
->…->onTouch(ACTION_MOVE
->onTouch(ACTION_UP)
->onClick()
可以看到,onTouch方法在onClick方法之前执行。
其中onTouch方法的返回值为false
,如果我们将它的返回值改为true
的话,即:
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.i("ClickExample1","OnTouchListener--OnTouch--action="+event.getAction()+"--"+v);
return true;
}
再次运行该APP,会发现onClick
方法得不到执行
上面的例子总结起来就是:
1.onTouch
方法先于onClick
方法执行
2.onTouch
方法返回值为true
时,后面的onClick
方法不会继续执行
#二、源码分析
首先我们查看两个类View 和ViewGroup的类的继承关系
通过View
和ViewGroup
的继承关系我们知道:所有view控件都是继承自View
类,布局控件ViewGroup
也继承自View
类。与事件分发相关的方法是View
控件中dispatchTouchEvent
方法。
1.dispatchTouchEvent
方法
Android 6.0中View
控件中dispatchTouchEvent
方法如下:
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;
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
// Clean up after nested scrolls if this is the end of a gesture;
// also cancel it if we tried an ACTION_DOWN but we didn't want the rest
// of the gesture.
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
其中跟onTouch
方法调用相关的关键代码为
...
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;
}
}
...
其中ListenerInfo
为View的内部类,它包含多个xxxListener
接口成员变量,其中就包括上一小节实现的OnTouchListener
、OnClickListener
两个接口,mListenerInfo
为ListenerInfo
在View内部的实例,当我们调用setxxxListener
时,会把xxxListener
的实例设置到mListenerInfo
中,例如setOnClickListener
方法的具体实现
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
在onTouch
方法调用相关的关键代码中,我们发现 mListenerInfo
、mOnTouchListener
,以及mViewFlags
的值会影响onTouch
方法的执行,同时onTouch
的返回值会影响后面onTouchEvent
方法的执行,当onTouch
返回true
时,后面的onTouchEvent
不会执行。其中onClick
方法跟onTouchEvent
方法存在联系。
总结起来就是:
1.产生触摸事件时,View
组件的dispatchTouchEvent
方法会被首先调用,在dispatchTouchEvent
方法中,会依次调用onTouch
和onTouchEvent
方法,其中onTouchEvent
方法会引用onClick
方法的调用。
2.当mListenerInfo
、mOnTouchListener
的值为NULL
以或者mViewFlags
的值不为ENABLED
时,会中断onTouch
方法的执行,直接执行onTouchEvent
方法,这时dispatchTouchEvent
的返回值与onTouchEvent
的返回值相同。当onTouch
方法执行后返回值为true
时,会中断onTouchEvent
的执行,dispatchTouchEvent
的返回值为true
。当onTouch
方法执行后返回值为false
时,onTouchEvent
方法会执行,dispatchTouchEvent
的返回值与onTouchEvent
的返回值相同。
##2.onTouchEvent方法
在dispatchTouchEvent
方法中,我们知道onTouchEvent
方法与onClick
方法之间存在联系,下面我们来分析onTouchEvent
方法。
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// 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)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_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 (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// 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) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
if (performButtonActionOnTouchDown(event)) {
break;
}
// Walk up the hierarchy to determine if we're inside a scrolling container.
boolean isInScrollingContainer = isInScrollingContainer();
// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
checkForLongClick(0);
}
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
setPressed(false);
}
}
break;
}
return true;
}
return false;
}
其中
...
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
...
部分判断当前view是否DISABLED
,如果是DISABLED
的话,且CLICKABLE
直接消费该事件,返回true
,onClick
方法不会得到执行。如果是DISABLED
,且不是CLICKABLE
则直接返回false
,onClick
方法不会得到执行。总结起来就是当前view是DISABLED
的,onClick
方法不会得到执行,且返回 CLICKABLE
的布尔值。
接着如果View是ENBALE
和UNCLICKABLE
的话,直接返回false
。如果View是ENBALE
和CLICKABLE
的话如根据触摸事件类型进入相应的switch分支中,最后返回true
。
...
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_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 (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// 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) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;
if (performButtonActionOnTouchDown(event)) {
break;
}
// Walk up the hierarchy to determine if we're inside a scrolling container.
boolean isInScrollingContainer = isInScrollingContainer();
// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
checkForLongClick(0);
}
break;
...
其中ACTION_DOWN
事件进入ACTION_DOWN
分支中进行相关处理,ACTION_UP
事件进入 ACTION_UP
中 进行相关的处理,在ACTION_UP
中,会依次获取焦点、设置pressed状态、在pressed状态下创建一个名为PerformClick
的Runnable
接口并post到UI线程消息队列中执行performClick
方法,其中performClick
方法如下
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}
在performClick
方法中会判断是否设置了OnClickListener
接口,如果设置了就执行onClick
方法并返回true
,如果没有就返回false
.其中setOnClickListener
方法会同时设置view为ONCLICKABLE
。
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
总结起来就是:
1.onClick
方法会在onTouchEvent
方中的ACTION_UP
方法中触发。
2.当前view是DISABLED
的,执行 onTouchEvent
方法时onClick
方法不会得到执行,且返回 CLICKABLE
的真值。
2.当dispatchTouchEvent
在进行事件分发的时候,只有前一个action
返回true
,才会触发下一个action
。(待验证)
三、小例子验证
我们新建一个ExampleClick2
小例子,自定义实现一个Button
按钮,重写其中跟事件相关的方法
3.1 项目源码
自定义Button
public class TestButton extends Button {
public TestButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("ClickExample2","onTouchEvent--action="+event.getAction());
return super.onTouchEvent(event);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i("ClickExample2","dispatchTouchEvent--action="+event.getAction());
return super.dispatchTouchEvent(event);
}
}
布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/my_linear"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<com.lengyu.free.clickexample2.TestButton
android:id="@+id/my_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button Test!"/>
</LinearLayout>
MainActivity.java文件代码。
package com.lengyu.free.clickexample2;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;
public class MainActivity extends Activity implements View.OnTouchListener,View.OnClickListener{
private LinearLayout mLayout;
private TestButton mButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mLayout=(LinearLayout)findViewById(R.id.my_linear);
mButton=(TestButton)findViewById(R.id.my_btn);
mLayout.setOnTouchListener(this);
mButton.setOnTouchListener(this);
mLayout.setOnClickListener(this);
mButton.setOnClickListener(this);
}
@Override
public void onClick(View v) {
Log.i("ClickExample2","onClickListener--onClick--"+v);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.i("ClickExample2","onTouchListener--onTouch--action="+event.getAction()+v);
return false;
}
}
3.2 事件分析
事件1:点击Button区域,在Button区域移动后离开。
日志显示:
通过日志分析,我们知道,依次产生了4个事件 :ACTION_DOWN
->ACTION_MOVE
->ACTION_MOVE
->ACTION-UP
,
对于前三项,依次调用dispatchTouchEvent
->onTouch
->onTouchEvent
,返回true。
第四项
dispatchTouchEvent
->onTouch
->onTouchEvent
->onClick
,返回true。
我们对上面的TestButton代码进行如下修改
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("ClickExample2","onTouchEvent--action="+event.getAction());
return true;
}
进行下面的事件2
事件2:点击Button区域
日志输出
点击时发现Button没有长按状态且onClick
方法没有执行,这是由于没有调用父类中的onTouch方法所导致的。
接着修改TestButton代码onTouchEvent方法
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("ClickExample2","onTouchEvent--action="+event.getAction());
super.onTouchEvent(event);
return true;
}
事件3:点击Button区域
日志输出:
和事件1:的日志输出类似
接着修改TestButton代码onTouchEvent方法
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("ClickExample2","onTouchEvent--action="+event.getAction());
return false;
}
事件4:点击Button区域
日志输出:
可以看 dispatchTouchEvent
第一次分发ACTION_DOWN
事件返回false
后,后面的事件不再分发,而是触发了父控件LinearLayout
的onTouch
和onTouchEvent
事件。
同理修改TestButton代码onTouchEvent方法
public boolean onTouchEvent(MotionEvent event) {
Log.i("ClickExample2","onTouchEvent--action="+event.getAction());
super.onTouchEvent(event);
return false;
}
事件5:点击Button区域
日志输出
保持之前TestButton代码其它代码不变。修改其中的dispatchTouchEvent
方法如下
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i("ClickExample2","dispatchTouchEvent--action="+event.getAction());
return true;
}
事件6:点击Button区域
日志输出
dispatchTouchEvent
方法不调用父方法的话,其它任何事件都得不到触发。
保持之前TestButton代码其它代码不变。修改其中的dispatchTouchEvent
方法如下
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i("ClickExample2","dispatchTouchEvent--action="+event.getAction());
super.dispatchTouchEvent(event);
return true;
}
事件7:点击Button区域
日志输出
跟事件1类似。
修改代码dispatchTouchEvent
代码,返回false
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i("ClickExample2","dispatchTouchEvent--action="+event.getAction());
return false;
}
事件8:点击Button区域
日志输出
dispatchTouchEvent
代码,返回false
,跟事件4处理流程相似。事件不分发,触发LinearLayout
中的事件,按钮没有按下状态。
接着修改dispatchTouchEvent
中代码,调用父类中的方法,返回false
,如下:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i("ClickExample2","dispatchTouchEvent--action="+event.getAction());
super.dispatchTouchEvent(event);
return false;
}
事件9:点击Button区域
日志输出
dispatchTouchEvent
代码,返回false
,跟事件4处理流程相似。事件不分发,触发LinearLayout
中的事件,由于调用了父类的方法,按钮有按下状态。
修改dispatchTouchEvent
返回值为true
,onTouchEvent
为false
:
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i("ClickExample2","onTouchEvent--action="+event.getAction());
super.onTouchEvent(event);
return false;
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i("ClickExample2","dispatchTouchEvent--action="+event.getAction());
super.dispatchTouchEvent(event);
return true;
}
事件10:点击Button区域
日志输出:
事件仍然得到了传递
修改dispatchTouchEvent
返回值为false
,onTouchEvent
为true
:
事件11:点击Button区域
事件没有得到传递,调用了LinearLayout中的方法
终结起来就是dispatchTouchEvent
派发事件的,如果返回值为false
将停止下次事件派发,如果返回true
将继续下次派发.
#四 总结
通过上面的分析,我们对于VIew事件分发机制可以得出以下结论:
1.产生触摸事件时,View 组件的dispatchTouchEvent方法会被首先调用,在dispatchTouchEvent方法中,会依次调用onTouch和onTouchEvent方法,其中onTouchEvent方法会引用onClick方法的调用。
2.当mListenerInfo 、mOnTouchListener的值为NULL以或者mViewFlags的值不为ENABLED
时,会中断onTouch方法的执行,直接执行onTouchEvent方法,这时dispatchTouchEvent的返回值与onTouchEvent的返回值相同。当onTouch方法执行后返回值为true时,会中断onTouchEvent的执行,dispatchTouchEvent的返回值为true。当onTouch方法执行后返回值为false时,onTouchEvent方法会执行,dispatchTouchEvent的返回值与onTouchEvent的返回值相同。
3.onClick
方法会在onTouchEvent
方中的ACTION_UP
方法中触发。但是如果在调用onTouchEvent
方法时,View 是DISENABLED
状态的话,onClick
方法不会得到执行,且返回 CLICKABLE
的布尔值。
4.当dispatchTouchEvent
在进行事件分发的时候,只有前一个action
返回true
,才会触发下一个action
。
参考文章:
http://blog.csdn.net/yanbober/article/details/45887547/