您的位置:首页 > 移动开发 > Android开发

Android View事件分发机制

2016-03-29 11:44 232 查看
最近在开发中遇到view滑动冲突的问题,由于一开始就知道这个问题与view事件分发有关,之后在网上看了几篇关于事件分发的资料后,开发中遇到的问题很快便得到解决。

在这里总结一下我对view事件分发的理解。

首先,看下事件分发流程图:



Button事件演示

在对view的事件分发机制进行分析前,我们可以通过一个demo看看Button的事件处理的流程。

在布局文件中添加一个button控件,然后在代码中实现Button的setOnClickListener和setOnTouchListener方法,注册Click监听和Touch监听。

/**
* button事件
*/
private void showButtonTouch() {
mBtn = (Button) findViewById(R.id.btn);
mBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e(TAG, "Button onClick");
}
});
mBtn.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
Log.e(TAG, "Button onTouch " + "ACTION_UP");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "Button onTouch " + "ACTION_MOVE");
break;
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "Button onTouch " + "ACTION_DOWN");
break;
}

return false;
}
});
}


<?xml version="1.0" encoding="utf-8"?>
<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:orientation="vertical">

<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button事件测试"/>
</LinearLayout>


demo运行起来之后点击button,看下log日志(在点击时移动一下保证ACTION_MOVE事件能被触发):



在这里通过日志可以看出事件处理的流程。

View事件分发源码解析

在View的源码中,我们可以看到dispatchTouchEvent方法,这个方法可以理解为是View事件分发的入口。(代码基于2.2即API 8,建议大家在理解源码时不要看太高的版本,高版本源码会有过多的优化,会妨碍我们对于代码主要功能逻辑的理解

dispatchTouchEvent方法解析

以下是View中dispatchTouchEvent方法代码:

public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}


在这整个方法中,事件分发的关键就在这个判断中 if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&mOnTouchListener.onTouch(this, event))

如果这个判断为true,那么该方法返回true;否则,执行onTouchEvent()方法并根据onTouchEvent方法的返回值返回具体结果。

mOnTouchListener != null:是否为空,mOnTouchListener 对象就是我们通过mBtn.setOnTouchListener(new View.OnTouchListener() {})设置的,所以这里不为空。

(mViewFlags & ENABLED_MASK) == ENABLED:判断控件是否是enable,很显然为true。

mOnTouchListener.onTouch(this, event):最后就是判断onTouch方法中返回值是否为true,onTouch方法就是我们在Button控件注册Touch监听时@Override的onTouch方法。

在前面的demo中,因为mOnTouchListener.onTouch(this, event)方法的返回值为false,我们在log中看到button先执行touch事件在执行click事件。在这里我们将onTouch方法的返回值改为true,可以看到以下log:



在log中可以看到click方法没有被执行,这又是为什么呢?

其实在这里大家通过阅读dispatchTouchEvent方法代码可以想到,因为onTouch返回值为true,所以这个判断条件成立即dispatchTouchEvent方法返回true。if (onTouchEvent(event))->onTouchEvent方法没有被执行,会不会是由于这个原因导致click方法没有被执行呢? 很显然我们要看看onTouchEvent方法的源码。

onTouchEvent源码解析:

以下是onTouchEvent方法的源码:

public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;

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));
}

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 & PREPRESSED) != 0;
if ((mPrivateFlags & 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 (!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) {
mPrivateFlags |= PRESSED;
refreshDrawableState();
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
break;

case MotionEvent.ACTION_DOWN:
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPrivateFlags |= PREPRESSED;
mHasPerformedLongPress = false;
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
break;

case MotionEvent.ACTION_CANCEL:
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
removeTapCallback();
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 = mTouchSlop;
if ((x < 0 - slop) || (x >= getWidth() + slop) ||
(y < 0 - slop) || (y >= getHeight() + slop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();

// Need to switch from pressed to not pressed
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
}
}
break;
}
return true;
}

return false;
}


对于onTouchEvent方法,主要的功能逻辑我们只需要从第17行代码开始阅读便可。

首先判断View是否是可点击的,因为Button默认是可点击的,所以这个条件成立,我们可以进入到switch分支判断中。在这里给大家留下一个疑问,如果这个条件不成立即就是控件是不可点击的,会出现什么样的情况呢?

MotionEvent.ACTION_UP:在这个分支判断中我们可以看到在43行有个performClick()方法,我们进入到这个方法体中可以看到:

public boolean performClick() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

if (mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
mOnClickListener.onClick(this);
return true;
}

return false;
}


在该方法中可以看到mOnClickListener.onClick(this)被调用了,到此处我们可以确认Button的click事件就是在onTouchEvent方法中调用的

TextView和Button比较

在onTouchEvent中曾留下一个疑问,如果判断控件是否可点击为false,会出现什么样的情况呢?

为了验证这个问题我们可以通过TextView和Button进行比较一下,因为TextView默认不可点击,Button默认可点击的。

首先在xml中添加TextView和Button,在代码中分别为他们注册setOnTouchListener监听,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<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:orientation="vertical">

<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button事件测试"/>

<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/holo_green_light"
android:text="TextView事件测试" />
</LinearLayout>


/**
* button事件
*/
private void showButtonTouch() {
mBtn = (Button) findViewById(R.id.btn);
mBtn.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
Log.e(TAG, "Button onTouch " + "ACTION_UP");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "Button onTouch " + "ACTION_MOVE");
break;
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "Button onTouch " + "ACTION_DOWN");
break;
}

return false;
}
});
}
/**
* TextView事件
*/
private void showTextTouch() {
mTv = (TextView) findViewById(R.id.tv);
mTv.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
Log.e(TAG, "TextView onTouch " + "ACTION_UP");
break;
case MotionEvent.ACTION_MOVE:
Log.e(TAG, "TextView onTouch " + "ACTION_MOVE");
break;
case MotionEvent.ACTION_DOWN:
Log.e(TAG, "TextView onTouch " + "ACTION_DOWN");
break;
}
return false;
}
});
}


因为我们需要进入到onTouchEvent方法中,所以必须让setOnTouchListener.onTouch()方法的返回值为false,在这里我们看看demo运行起来后分别点击Button和TextView打印的log:



发现Button的log和之前演示的一样,但是TextView在点击->移动->抬起的过程中只打印一次ACTION_DOWN的log,这是什么原因呢?

在之前的onTouchEvent方法中,我们可以看到:

当控件可点击时,方法会进入到switch分支判断中,当switch执行完成后在129行代码会 return true

而当控件不可点击时,方法会直接到132行,return false

这里需要指明onTouchEvent方法的返回值是touch事件层级传递的关键,说明如下:

return true,那么表示该方法消费了此次事件,可以开始下一个事件

return false,那么表示该方法并未处理完全,该事件仍然需要以某种方式传递下去继续等待处理。

现在就能解释上面TextView为什么只打印一句log了,因为onTouchEvent返回值为false,系统会认为ACTION_DOWN事件没有被执行完成,那么其他的touch事件就不能被触发了。

现在大家再去看事件分发流程图中的View部分想必就能看懂了吧!!!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: