前言
事件分发机制,主要是需要了解View和ViewGroup的。
其中View篇是指单个View控件的分发流程,eg:buttion,textView等,它已经是最小单位了。
而ViewGroup篇则指布局控件的分发流程,eg:LinearLayout等,它包含了很多字View控件,父控件的触摸事件会传递给子控件。在实际的开发中,会存在很多事件的冲突,了解ViewGroup的事件分发,便可以更好的解决这类问题。
一。案例分析
首先,我们写一个非常简单的项目。
<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:id="@+id/layout"
tools:context=".MainActivity" >
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/button"
android:text="button" />
</LinearLayout>
package com.example.viewanalyse;
import android.os.Bundle;
import android.app.Activity;
import android.util.Log;
import android.view.Menu;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnTouchListener;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
public class MainActivity extends Activity implements View.OnTouchListener,View.OnClickListener {
private LinearLayout mLayout;
private Button mButton;
private static final String TAG = "ViewAnalyse";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mLayout =(LinearLayout)findViewById(R.id.layout);
mButton =(Button)findViewById(R.id.button);
mLayout.setOnTouchListener(this);
mButton.setOnTouchListener(this);
mLayout.setOnClickListener(this);
mButton.setOnClickListener(this);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
// TODO Auto-generated method stub
Log.d(TAG, "onTouch-- action="+event.getAction()+" --"+v);
return false;
}
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
Log.d(TAG, "onClick --"+v);
}
}
在activity里面,有一个LinearLayout布局和一个button按钮。给这俩个控件设置了onTouch和onClick监听事件。
1.1 我们用手指点击一下button。打印出来的Log如下:
10-15 09:59:49.991: D/ViewAnalyse(15405): onTouch-- action=0 --android.widget.Button
10-15 09:59:50.021: D/ViewAnalyse(15405): onTouch-- action=2 --android.widget.Button
10-15 09:59:50.031: D/ViewAnalyse(15405): onTouch-- action=1 --android.widget.Button
10-15 09:59:50.031: D/ViewAnalyse(15405): onClick --android.widget.Button
其中ACTION_DOWN=0(按下手指) / ACTION_UP=1(抬起手指) / ACTION_MOVE=2(滑动手指)/
由Log我们可以看出,点击一个button,首先触发View的onTouch()方法(必然包括按下/抬起俩个动作,滑动动作可有可无),然后才会触发onClick()方法。
点击button事件传递顺序如下图所示:
1.2 修改onTouch()方法里面的返回值,false --> true
@Override
public boolean onTouch(View v, MotionEvent event) {
// TODO Auto-generated method stub
Log.d(TAG, "onTouch-- action="+event.getAction()+" --"+v);
return true;
}
我们再进行用手指点击一次button的操作,打印出来的log如下:
10-15 10:32:31.521: D/ViewAnalyse(21527): onTouch-- action=0 --android.widget.Button
10-15 10:32:31.551: D/ViewAnalyse(21527): onTouch-- action=1 --android.widget.Button
我们可以发现如果onTouch()方法返回为true,则onClick()方法不会被调用。可以先理解成onTouch方法返回true就认为这个事件被onTouch消费掉了,因而不会再继续向下传递。
二。View.java传递事件源码分析
2.1 View 和 ViewGroup的关系
它们选择的安全式的组合模式(在组合对象ViewGroup中添加/0移除/获取组件View的方法),
2.2 View.java中的dispatchTouchEvent()方法
在Android中,只要触摸控件都会先触发View.java中的dispatchTouchEvent()方法。(如果自定义View的话,则可以覆写该方法)。
public boolean dispatchTouchEvent(MotionEvent event) {
...
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;
}
dispatchTouchEvent()方法中,我们只看上面列出来的重点。
其中:if (li != null && li.mOnTouchListener != null&& (mViewFlags & ENABLED_MASK) == ENABLED&& li.mOnTouchListener.onTouch(this, event)) 这一句是重点。
在这个实例中,ListenerInfo类li 就是我们通过mButton.setOnTouchListener(this)设置的触摸监听事件。
(mViewFlags & ENABLED_MASK) == ENABLED 表示控件是不是EBABLED的,控件默认是ENABLED的。
接着判断View.OnTouchListener中的onTouch()的返回值(重点):
a.如果onTouch()返回true, 那么接下来的onTouchEvent()方法将不会得到执行(即onClick()方法也不会得到执行),dispatchTouchEvent()返回true(即触发下一个action)。
b.如果onTouch返回false,那么接下来就执行onTouchEvent()方法,onTouchEvent()的返回值决定dispatchTouchEvent()的返回值。
2.3 View.java中的onTouchEvent()方法
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == 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));
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
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) {
// 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();
}
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;
...
}
return true;
}
return false;
}
首先,我们来看6 --14行,如果这个View控件是disabled的,那么onTouchEvent()返回值取决于是否是可点击的clickable.。
控件View是可点击的Clickable,则返回true,表明消费掉这个事件。控件View是不可以点击的disClickable,则返回false。
接着,我们再看22-23行和107行,只要控件View是enabled的,onTouchEvent()都会返回true。
最后,我们看25行和55-56行,在手抬起来(即ACTION_UP)的时候,只要不是长按,就会在UI线程中post一个PerformClick的Runnable,执行performClick()方法。
private final class PerformClick implements Runnable {
@Override
public void run() {
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;
}
在这个实例中,ListenerInfo类li就是我们通过mButton.setOnClickListener(this)设置的点击监听事件。
2.3 总结结论
a. dispatchTouchEvent()方法和onTouchEvent()方法是在View.java中,自定义View的时候,可以覆写该方法。
b. onTouch()和onClick()方法是在activity中,通过控件View注册监听,并覆写该方法。
三. 自定义一个Button
MyButton:
package com.example.viewanalyse;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.widget.Button;
public class MyButton extends Button {
private static final String TAG = "ViewAnalyse";
public MyButton(Context context) {
super(context);
// TODO Auto-generated constructor stub
}
public MyButton(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// TODO Auto-generated method stub
Log.d(TAG, "onTouchEvent-- action="+event.getAction());
return super.onTouchEvent(event);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.d(TAG, "dispatchTouchEvent-- action="+event.getAction());
// TODO Auto-generated method stub
return super.dispatchTouchEvent(event);
}
}
MainActivity:
package com.example.viewanalyse;
import android.os.Bundle;
import android.app.Activity;
import android.util.Log;
import android.view.Menu;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnTouchListener;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
public class MainActivity extends Activity implements View.OnTouchListener,View.OnClickListener {
private LinearLayout mLayout;
private Button mButton;
private static final String TAG = "ViewAnalyse";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mLayout =(LinearLayout)findViewById(R.id.layout);
mButton =(MyButton)findViewById(R.id.button);
mLayout.setOnTouchListener(this);
mButton.setOnTouchListener(this);
mLayout.setOnClickListener(this);
mButton.setOnClickListener(this);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
// TODO Auto-generated method stub
Log.d(TAG, "onTouch-- action="+event.getAction()+" --"+v);
return false;
}
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
Log.d(TAG, "onClick --"+v);
}
}
3.1 还是跟上面一样,点击一下这个button,打印出来的log:
01-02 07:31:57.323: D/ViewAnalyse(13846): dispatchTouchEvent-- action=0
01-02 07:31:57.323: D/ViewAnalyse(13846): onTouch-- action=0 --com.example.viewanalyse.MyButton
01-02 07:31:57.323: D/ViewAnalyse(13846): onTouchEvent-- action=0
01-02 07:31:57.323: D/ViewAnalyse(13846): dispatchTouchEvent-- action=2
01-02 07:31:57.323: D/ViewAnalyse(13846): onTouch-- action=2 --com.example.viewanalyse.MyButton
01-02 07:31:57.323: D/ViewAnalyse(13846): onTouchEvent-- action=2
01-02 07:31:57.323: D/ViewAnalyse(13846): dispatchTouchEvent-- action=1
01-02 07:31:57.323: D/ViewAnalyse(13846): onTouch-- action=1 --com.example.viewanalyse.MyButton
01-02 07:31:57.323: D/ViewAnalyse(13846): onTouchEvent-- action=1
01-02 07:31:57.353: D/ViewAnalyse(13846): onClick --com.example.viewanalyse.MyButton
由log我们可以看出,不管action是ACTION_DOWN=0 / ACTION_UP=1 / ACTION_MOVE=2/,一个action的派发,流程都是:
dispatchTouchEvent() --> onTouch() --> onTouchEvent()
最后UP的时候,触发onClick()方法。 这和我们上述的分析一致。
3.2 修改onTouchEvent()返回值
3.2.1 onTouchEvent()返回false
@Override
public boolean onTouchEvent(MotionEvent event) {
// TODO Auto-generated method stub
Log.d(TAG, "onTouchEvent-- action="+event.getAction());
super.onTouchEvent(event);
return false;
}
用手指点击button,打印出来的log:
01-02 07:45:34.543: D/ViewAnalyse(14452): dispatchTouchEvent-- action=0
01-02 07:45:34.543: D/ViewAnalyse(14452): onTouch-- action=0 --com.example.viewanalyse.MyButton
01-02 07:45:34.543: D/ViewAnalyse(14452): onTouchEvent-- action=0
01-02 07:45:34.543: D/ViewAnalyse(14452): onTouch-- action=0 --android.widget.LinearLayout
01-02 07:45:34.543: D/ViewAnalyse(14452): onTouch-- action=2 --android.widget.LinearLayout
01-02 07:45:34.543: D/ViewAnalyse(14452): onTouch-- action=1 --android.widget.LinearLayout
01-02 07:45:34.573: D/ViewAnalyse(14452): onClick --android.widget.LinearLayout
因为onTouchEvent()返回值决定dispatchTouchEvent()返回值,当dispatchTouchEvent()返回值为false时,不会触发下一个action。
3.2.2onTouchEvent()返回true
@Override
public boolean onTouchEvent(MotionEvent event) {
// TODO Auto-generated method stub
Log.d(TAG, "onTouchEvent-- action="+event.getAction());
super.onTouchEvent(event);
return true;
}
用手指点击button,打印出来的log:
01-02 07:48:44.783: D/ViewAnalyse(15373): dispatchTouchEvent-- action=0
01-02 07:48:44.783: D/ViewAnalyse(15373): onTouch-- action=0 --com.example.viewanalyse.MyButton
01-02 07:48:44.783: D/ViewAnalyse(15373): onTouchEvent-- action=0
01-02 07:48:44.783: D/ViewAnalyse(15373): dispatchTouchEvent-- action=1
01-02 07:48:44.783: D/ViewAnalyse(15373): onTouch-- action=1 --com.example.viewanalyse.MyButton
01-02 07:48:44.783: D/ViewAnalyse(15373): onTouchEvent-- action=1
01-02 07:48:44.813: D/ViewAnalyse(15373): onClick --com.example.viewanalyse.MyButton
可以发现,跟3.1不做任何修改,打印出来的log是一致的,即正常派发。验证了我们之前说的,onTouchEvent()当控件是不可点击的disclickable才返回false,否则返回true。
3.2.3onTouchEvent()不调用父方法,直接返回true
@Override
public boolean onTouchEvent(MotionEvent event) {
// TODO Auto-generated method stub
Log.d(TAG, "onTouchEvent-- action="+event.getAction());
return true;
}
用手指点击button,打印出来的log:
01-02 07:53:52.783: D/ViewAnalyse(16228): dispatchTouchEvent-- action=0
01-02 07:53:52.793: D/ViewAnalyse(16228): onTouch-- action=0 --com.example.viewanalyse.MyButton
01-02 07:53:52.793: D/ViewAnalyse(16228): onTouchEvent-- action=0
01-02 07:53:52.793: D/ViewAnalyse(16228): dispatchTouchEvent-- action=1
01-02 07:53:52.793: D/ViewAnalyse(16228): onTouch-- action=1 --com.example.viewanalyse.MyButton
01-02 07:53:52.793: D/ViewAnalyse(16228): onTouchEvent-- action=1
发现没有调用onClick(()方法。验证了我们之前在源码中分析,onClick()方法是在父类View.java中的onTouchEvent()被调用。
3.3修改dispatchTouchEvent()的返回值
3.3.1dispatchTouchEvent()返回false
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.d(TAG, "dispatchTouchEvent-- action="+event.getAction());
// TODO Auto-generated method stub
super.dispatchTouchEvent(event);
return false;
}
用手指点击button,打印出来的log:
01-02 08:04:57.483: D/ViewAnalyse(17243): dispatchTouchEvent-- action=0
01-02 08:04:57.483: D/ViewAnalyse(17243): onTouch-- action=0 --com.example.viewanalyse.MyButton
01-02 08:04:57.483: D/ViewAnalyse(17243): onTouchEvent-- action=0
01-02 08:04:57.493: D/ViewAnalyse(17243): onTouch-- action=0 --android.widget.LinearLayout
01-02 08:04:57.493: D/ViewAnalyse(17243): onTouch-- action=2 --android.widget.LinearLayout
01-02 08:04:57.493: D/ViewAnalyse(17243): onTouch-- action=1 --android.widget.LinearLayout
01-02 08:04:57.513: D/ViewAnalyse(17243): onClick --android.widget.LinearLayout
与3.2.1(onTouchEvent返回false)打印出来的log一致。即当前派发Down事件,返回true就会继续派发Up/Move事件,返回false就停止派发。