记得大家刚开始接触安卓的时候,一个setOnClickListener就能实现一个View的点击,当时是如此的激动~。这大概就是大家对Android触摸事件最初的接触吧。今天我们来聊下Android重要的触摸事件分发机制。
例子
我们来举一个栗子吧~
MyButton.java
public class MyButton extends Button
{
@Override
public boolean onTouchEvent(MotionEvent event)
{
int action = event.getAction();
switch (action)
{
case MotionEvent.ACTION_DOWN:
Log.e("w", "onTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e("w", "onTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e("w", "onTouchEvent ACTION_UP");
break;
default:
break;
}
return super.onTouchEvent(event);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event)
{
int action = event.getAction();
switch (action)
{
case MotionEvent.ACTION_DOWN:
Log.e("w", "dispatchTouchEvent ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e("w", "dispatchTouchEvent ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e("w", "dispatchTouchEvent ACTION_UP");
break;
default:
break;
}
return super.dispatchTouchEvent(event);
}
}
在onOutchEvent和disatchTouchEvent中打印日志
main_activity.xml
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<io.weimu.caoyang.MyButton
android:id="@+id/id_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="click me" />
</LinearLayout>
MainActivity.java
public class MainActivity extends Activity
{
private Button mButton ;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mButton = (Button) findViewById(R.id.id_btn);
mButton.setOnTouchListener(new OnTouchListener()
{
@Override
public boolean onTouch(View v, MotionEvent event)
{
int action = event.getAction();
switch (action)
{
case MotionEvent.ACTION_DOWN:
Log.e(“w”, "onTouch ACTION_DOWN");
break;
case MotionEvent.ACTION_MOVE:
Log.e(“w”, "onTouch ACTION_MOVE");
break;
case MotionEvent.ACTION_UP:
Log.e(“w”, "onTouch ACTION_UP");
break;
default:
break;
}
return false;
}
});
}
}
我们在MyButton设置了OnTouchListener监听,
我们看一下Log的打印
标志 | 消息 |
---|---|
E/MyButton(879): | dispatchTouchEvent ACTION_DOWN |
E/MyButton(879): | onTouch ACTION_DOWN |
E/MyButton(879): | onTouchEvent ACTION_DOWN |
E/MyButton(879): | dispatchTouchEvent ACTION_MOVE |
E/MyButton(879): | onTouch ACTION_MOVE |
E/MyButton(879): | onTouchEvent ACTION_MOVE |
E/MyButton(879): | dispatchTouchEvent ACTION_UP |
E/MyButton(879): | onTouch ACTION_UP |
E/MyButton(879): | onTouchEvent ACTION_UP` |
按照上面的简单实例我们可以简单得出一个结论:View的事件分发无论DOWN、MOVE、UP都会经过dispatchTouchEvent
、onTouch
(如果设置的话)、onTouchEvent
源码解读:
Step1 View
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
...
//重点判断onTouch
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//重点判断onTouchEvent
if (!result && onTouchEvent(event)) {
result = true;
}
...
return result;
}
先判断mOnTouchListener是否为空?改View是否为Enable?onTouch是否返回true?若三个同时成立,返回true,且onTouchEvent不会执行。
mOnTouchListener从哪里来呢?
public void setOnTouchListener(OnTouchListener l) {
mOnTouchListener = l;
}
可以看到这就是栗子中Activity.java里mButton.setOnTouchListener
设置的。
如果我们设置了onTouchListener,且设置返回为true,那么View的onTouchEvent就不会执行!
Step2 View
public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
//情况1:如果view为disenable且可点击,返回true
//此情况还是会消费此触摸事件,只是不做反应罢了
if ((viewFlags & ENABLED_MASK) == DISABLED) {
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
...
//情况2:如果View为enable且可点击,返回ture
//大部分的触摸操纵都在这里面
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
//这是Part02
break;
case MotionEvent.ACTION_DOWN:
//这是Part01
break;
case MotionEvent.ACTION_CANCEL:
//这是Part04
break;
case MotionEvent.ACTION_MOVE:
//这是Part03
break;
}
return true;
}
//情况3:如果View为enable但不能点击,直接返回false
return false;
}
在onTouchEvent工有3个主要情况:
- 情况1:如果view为disEnable且clickable,返回true。此情况还是会消费此触摸事件,只是不做反应
- 情况2:如果View为enable且clickable,返回ture。大部分的触摸操纵都在这里面
- 情况3:如果View为enable但unClickable,直接返回false。其实就是View为unClickable,基本就是返回false了。
view的enable和clickable都可以在java和xml设置。
以上代码可以看出,onToucheEvent里的重点操作都在switch里了,这里我们分几个步骤进行分析
Part01 ACTION_DOWN
case MotionEvent.ACTION_DOWN:
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPrivateFlags |= PREPRESSED;
mHasPerformedLongPress = false;
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
break;
- 初始化CheckForTap,此类为Runnable
- 给mPrivateFlags设置一个PREPRESSED的标识
- 设置mHasPerformedLongPress=false;表示长按事件还未触发;
- 发送一个延迟为ViewConfiguration.getTapTimeout()=115的延迟消息,到达延时时间后会执行CheckForTap()里面的run方法:
CheckForTap
private final class CheckForTap implements Runnable {
public void run() {
mPrivateFlags &= ~PREPRESSED;
mPrivateFlags |= PRESSED;
refreshDrawableState();
//检测长按
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
postCheckForLongClick(ViewConfiguration.getTapTimeout());
}
}
}
- 取消mPrivateFlags的PREPRESSED
- 设置PRESSED标识
- 刷新背景
- 如果View支持长按事件,则再发一个延时消息,检测长按;
具体如何检测长按呢?
private void postCheckForLongClick(int delayOffset) {
mHasPerformedLongPress = false;
if (mPendingCheckForLongPress == null) {
mPendingCheckForLongPress = new CheckForLongPress();
}
mPendingCheckForLongPress.rememberWindowAttachCount();
postDelayed(mPendingCheckForLongPress,
ViewConfiguration.getLongPressTimeout() - delayOffset);
}
- 初始化CheckForLongPress,此类为Runnable
- 发送一个延迟为ViewConfiguration.getLongPressTimeout() - delayOffset=(500-115=385)的延迟消息,到达延时时间后会执行CheckForLongPress()里面的run方法:
CheckForLongPress
class CheckForLongPress implements Runnable {
private int mOriginalWindowAttachCount;
public void run() {
if (isPressed() && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
if (performLongClick()) {
mHasPerformedLongPress = true;
}
}
}
经过一系列判断,最终调用performLongClick()即长按的接口调用。
这里我们可以得出一个小结论:
当用户点击视图时,超过500ms后且设置了长按监听的话,会触发长按监听接口!
Wonder疑问
- 那当用户在500ms内将手抬起会是什么情况呢?
- LongClick已经有了,那我们平时使用的的Click呢?
Part02 ACTION_UP
case MotionEvent.ACTION_UP:
//判断mPrivateFlags是否包含PREPRESSED
boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;
//如果包含PRESSED或者PREPRESSED则进入执行体,在115ms的前后抬起都会进入执行体。
if ((mPrivateFlags & PRESSED) != 0 || prepressed) {
//如果该视图还未获取焦点,则获之
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
//判断是否为长按状态,不是的话,进入执行体
if (!mHasPerformedLongPress) {
//这是一个轻点击操作,所以要移除长按检测操作
removeLongPressCallback();
//只有在按下状态时才执行点击动作
if (!focusTaken) {
//使用Runnable进行发送消息,而不是直接执行performClick。
//这样视图可以在点击操作前更新其可视化状态
if (mPerformClick == null) {
mPerformClick = new PerformClick();//*重点01*
}
//重点 * 调用平时用的click
if (!post(mPerformClick)) {
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();//*重点02*
}
//根据视图的mPrivateFlags的状态进行操作
if (prepressed) {
mPrivateFlags |= PRESSED;
refreshDrawableState();
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
//移除点击事件的检测操作
removeTapCallback();
}
break;
mPrivateFlags的状态:125ms前为prepressed(点击前),125ms后位pressed(点击后)。以上代码已经做了注释。这些操作就是500ms内的点击操作处理。
以上有两个比较重要的点,这里分析一下:
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;
}
//设置点击事件回调
public void setOnClickListener(OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
以上代码可以很清楚的看到onClick的调用!
UnsetPressedState
private final class UnsetPressedState implements Runnable {
public void run() {
setPressed(false);
}
}
public void setPressed(boolean pressed) {
if (pressed) {
mPrivateFlags |= PRESSED;
} else {
mPrivateFlags &= ~PRESSED;
}
refreshDrawableState();
dispatchSetPressed(pressed);
}
我们可以看到无论如何这个Runnable都会执行,只是对不同的状态(prePressed,pressed)进行处理。修改mPrivateFlags的状态,刷新背景,分发SetPress等。
这里我们可以得出一个小结论:
当用户点击视图时,低于500ms设置onClick的接口,就会触发onClick的接口。且这个过程是在OnTouchEvent的ACTION_UP完成。
Part03 ACTION_MOVE
case MotionEvent.ACTION_MOVE:
final int x = (int) event.getX();
final int y = (int) event.getY();
//判断该触摸事件是否已经移出控件外
int slop = mTouchSlop;
if ((x < 0 - slop) || (x >= getWidth() + slop) ||
(y < 0 - slop) || (y >= getHeight() + slop)) {
//当触摸移出当前视图
//移除点击回调
removeTapCallback();
if ((mPrivateFlags & PRESSED) != 0) {
//移除长按检测
removeLongPressCallback();
//mPrivateFlags去除PRESSED标志
mPrivateFlags &= ~PRESSED;
//刷新背景
refreshDrawableState();
}
}
break;
ACTION_MOVE的工作相对简单一点:不断的记录x,y。判断当前触摸事件是否已经移除当前控件之外?如果移除了,移除相对应的检测回调,以及刷新相对应的变量和背景。
Part04 ACTION_CANCLE
case MotionEvent.ACTION_CANCEL:
mPrivateFlags &= ~PRESSED;
refreshDrawableState();
removeTapCallback();
break;
ACTION_CANCEL的工作主要是:刷新相对应的变量和背景,移除响度应的检测回调。一般遇到的比较少。有一种情况是:当用户保持按下操作,并从你的控件转移到外层控件时,会触发ACTION_CANCEL。
总结 Summary
- View的事件转发流程为:View.dispatchEvent->View.setOnTouchListener->View.onTouchEvent
- 在dispatchTouchEvent中会进行OnTouchListener的判断,如果OnTouchListener不为null且返回true,则表示事件被消费,onTouchEvent不会被执行;否则执行onTouchEvent。
- 长按点击的回调是在ACTION_DOWN调用的。
- 轻按点击的回调是在ACTION_UP调用的。
- 判断触摸事件是否移除了当前控件是在ACTION_MOVE监听的。
额外 Extra
我们在来举一个栗子:
public class MainActivity extends Activity
{
private Button mButton ;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mButton = (Button) findViewById(R.id.id_btn);
mButton.setOnClickListener(new OnClickListener()
{
@Override
public void onClick(View v)
{
Log.e("e", "轻触点击");
}
});
mButton.setOnLongClickListener(new OnLongClickListener()
{
@Override
public boolean onLongClick(View v)
{
Log.e("e", "长按点击");
return false;
}
});
}
}
如果onLongClick返回的是ture(表示消费了),则onClick无法触发。如果返回false,就可以。大家可以结合下上面的代码解析看看为什么会这样~
PS:本文
整理
自以下文章,若有发现问题请致邮 caoyanglee92@gmail.com
工匠若水 Android触摸屏事件派发机制详解与源码分析一(View篇)
Hohohong Android View 事件分发机制 源码解析 (上)