Android View 事件分发机制 源码解析 (上)
一直想写事件分发机制的文章,不管咋样,也得自己研究下事件分发的源码,写出心得~
首先我们先写个简单的例子来测试View的事件转发的流程~
1、案例
为了更好的研究View的事件转发,我们自定以一个MyButton继承Button,然后把跟事件传播有关的方法进行复写,然后添加上日志~
MyButton
package com.example.zhy_event03; import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.widget.Button; public class MyButton extends Button { private static final String TAG = MyButton.class.getSimpleName(); public MyButton(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onTouchEvent(MotionEvent event) { int action = event.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: Log.e(TAG, "onTouchEvent ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: Log.e(TAG, "onTouchEvent ACTION_MOVE"); break; case MotionEvent.ACTION_UP: Log.e(TAG, "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(TAG, "dispatchTouchEvent ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: Log.e(TAG, "dispatchTouchEvent ACTION_MOVE"); break; case MotionEvent.ACTION_UP: Log.e(TAG, "dispatchTouchEvent ACTION_UP"); break; default: break; } return super.dispatchTouchEvent(event); } }
在onTouchEvent和dispatchTouchEvent中打印了日志~
然后把我们自定义的按钮加到主布局文件中;
布局文件:
<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" tools:context=".MainActivity" > <com.example.zhy_event03.MyButton android:id="@+id/id_btn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="click me" /> </LinearLayout>
最后看一眼MainActivity的代码
package com.example.zhy_event03; import android.app.Activity; import android.os.Bundle; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.View.OnTouchListener; import android.widget.Button; public class MainActivity extends Activity { protected static final String TAG = "MyButton"; 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(TAG, "onTouch ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: Log.e(TAG, "onTouch ACTION_MOVE"); break; case MotionEvent.ACTION_UP: Log.e(TAG, "onTouch ACTION_UP"); break; default: break; } return false; } }); } }
在MainActivity中,我们还给MyButton设置了OnTouchListener这个监听~
好了,跟View事件相关一般就这三个地方了,一个onTouchEvent,一个dispatchTouchEvent,一个setOnTouchListener;
下面我们运行,然后点击按钮,查看日志输出:
08-31 06:09:39.030: E/MyButton(879): dispatchTouchEvent ACTION_DOWN 08-31 06:09:39.030: E/MyButton(879): onTouch ACTION_DOWN 08-31 06:09:39.049: E/MyButton(879): onTouchEvent ACTION_DOWN 08-31 06:09:39.138: E/MyButton(879): dispatchTouchEvent ACTION_MOVE 08-31 06:09:39.138: E/MyButton(879): onTouch ACTION_MOVE 08-31 06:09:39.147: E/MyButton(879): onTouchEvent ACTION_MOVE 08-31 06:09:39.232: E/MyButton(879): dispatchTouchEvent ACTION_UP 08-31 06:09:39.248: E/MyButton(879): onTouch ACTION_UP 08-31 06:09:39.248: E/MyButton(879): onTouchEvent ACTION_UP
我有意点击的时候蹭了一下,不然不会触发MOVE,手抖可能会打印一堆MOVE的日志~~~
好了,可以看到,不管是DOWN,MOVE,UP都会按照下面的顺序执行:
1、dispatchTouchEvent
2、setOnTouchListener的onTouch
3、onTouchEvent
下面就跟随日志的脚步开始源码的探索~
2、dispatchTouchEvent
首先进入View的dispatchTouchEvent
/** * Pass the touch screen motion event down to the target view, or this * view if it is the target. * * @param event The motion event to be dispatched. * @return True if the event was handled by the view, false otherwise. */ public boolean dispatchTouchEvent(MotionEvent event) { if (!onFilterTouchEventForSecurity(event)) { return false; } if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)) { return true; } return onTouchEvent(event); }
直接看13行:首先判断mOnTouchListener不为null,并且view是enable的状态,然后mOnTouchListener.onTouch(this, event)返回true,这三个条件如果都满足,直接return true ; 也就是下面的onTouchEvent(event)不会被执行了;
那么mOnTouchListener是和方神圣,我们来看看:
/** * Register a callback to be invoked when a touch event is sent to this view. * @param l the touch listener to attach to this view */ public void setOnTouchListener(OnTouchListener l) { mOnTouchListener = l; }其实就是我们在Activity中设置的setOnTouchListener。
也就是说:如果我们设置了setOnTouchListener,并且return true,那么View自己的onTouchEvent就不会被执行了,当然了,本例我们return false,我们还得往下探索 ;
已经解决一个常见的问题:View的onTouchListener和onTouchEvent的调用关系,相信大家应该已经明白了~let's go;继续往下。
3、View的onTouchEvent:
接下来是View的onTouchEvent:
/** * Implement this method to handle touch screen motion events. * * @param event The motion event. * @return True if the event was handled, false otherwise. */ 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; }
代码还是比较长的,
10-15行,如果当前View是Disabled状态且是可点击则会消费掉事件(return true);可以忽略,不是我们的重点;17-21行,如果设置了mTouchDelegate,则会将事件交给代理者处理,直接return true,如果大家希望自己的View增加它的touch范围,可以尝试使用TouchDelegate,这里也不是重点,可以忽略;
接下来到我们的重点了:
23行的判断:如果我们的View可以点击或者可以长按,则,注意IF的范围,最终一定return true ;
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
//...
return true;
}
接下来就是 switch (event.getAction())了,判断事件类型,DOWN,MOVE,UP等;
我们按照例子执行的顺序,先看 case MotionEvent.ACTION_DOWN (71-78行):
1、MotionEvent.ACTION_DOWN
75行:给mPrivateFlags设置一个PREPRESSED的标识
76行:设置mHasPerformedLongPress=false;表示长按事件还未触发;
77行:发送一个延迟为ViewConfiguration.getTapTimeout()的延迟消息,到达延时时间后会执行CheckForTap()里面的run方法:
1、ViewConfiguration.getTapTimeout()为115毫秒;
2、CheckForTap
private final class CheckForTap implements Runnable { public void run() { mPrivateFlags &= ~PREPRESSED; mPrivateFlags |= PRESSED; refreshDrawableState(); if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) { postCheckForLongClick(ViewConfiguration.getTapTimeout()); } } }
在run方法里面取消mPrivateFlags的PREPRESSED,然后设置PRESSED标识,刷新背景,如果View支持长按事件,则再发一个延时消息,检测长按;
private void postCheckForLongClick(int delayOffset) { mHasPerformedLongPress = false; if (mPendingCheckForLongPress == null) { mPendingCheckForLongPress = new CheckForLongPress(); } mPendingCheckForLongPress.rememberWindowAttachCount(); postDelayed(mPendingCheckForLongPress, ViewConfiguration.getLongPressTimeout() - delayOffset); }
class CheckForLongPress implements Runnable { private int mOriginalWindowAttachCount; public void run() { if (isPressed() && (mParent != null) && mOriginalWindowAttachCount == mWindowAttachCount) { if (performLongClick()) { mHasPerformedLongPress = true; } } }
可以看到,当用户按下,首先会设置标识为PREPRESSED,如果在115毫秒内抬起了,UP时会移除CheckForTap这个回调(UP时会分析);
如果115后,没有抬起,会将View的标识设置为PRESSED且去掉PREPRESSED标识,然后发出一个检测长按的延迟任务,延时为:ViewConfiguration.getLongPressTimeout() - delayOffset(500ms -115ms),这个115ms刚好时检测额PREPRESSED时间;也就是用户从DOWN触发开始算起,如果500ms内没有抬起则认为触发了长按事件:
1、如果此时设置了长按的回调,则执行长按时的回调,且如果长按的回调返回true;才把mHasPerformedLongPress置为ture;
2、否则,如果没有设置长按回调或者长按回调返回的是false;则mHasPerformedLongPress依然是false;
好了DOWN就分析完成了;大家回个神,下面回到VIEW的onTouchEvent中的ACTION_MOVE:
2、MotionEvent.ACTION_MOVE
86到105行:
87-88行:拿到当前触摸的x,y坐标;
91行判断当然触摸点有没有移出我们的View,如果移出了:
1、执行removeTapCallback();
2、然后判断是否包含PRESSED标识,如果包含,移除长按的检查:removeLongPressCallback();
3、最后把mPrivateFlags中PRESSED标识去除,刷新背景;
private void removeTapCallback() { if (mPendingCheckForTap != null) { mPrivateFlags &= ~PREPRESSED; removeCallbacks(mPendingCheckForTap); } }这个是移除,DOWN触发时设置的PREPRESSED的检测;即当前触发时机在DOWN触发不到115ms时,你就已经移出控件外了;
如果115ms后,你才移出控件外,则你的当前mPrivateFlags一定为PRESSED且发送了长按的检测;
就会走上面的2和3;首先移除removeLongPressCallback()
private void removeLongPressCallback() {
if (mPendingCheckForLongPress != null) {
removeCallbacks(mPendingCheckForLongPress);
}
}
然后把mPrivateFlags中PRESSED标识去除,刷新背景;
好了,MOVE我们也分析完成了,总结一下:只要用户移出了我们的控件:则将mPrivateFlags取出PRESSED标识,且移除所有在DOWN中设置的检测,长按等;
下面再回个神,回到View的onTouchEvent的ACTION_UP:
3、MotionEvent.ACTION_UP
26到69行:
27行:判断mPrivateFlags是否包含PREPRESSED
28行:如果包含PRESSED或者PREPRESSED则进入执行体,也就是无论是115ms内或者之后抬起都会进入执行体。
36行:如果mHasPerformedLongPress没有被执行,进入IF
38行:removeLongPressCallback();移除长按的检测
45-50行:如果mPerformClick如果mPerformClick为null,初始化一个实例,然后立即通过handler添加到消息队列尾部,如果添加失败则直接执行performClick();添加成功,在mPerformClick的run方法中就是执行performClick();
终于执行了我们的click事件了,下面看一下performClick()方法:
public boolean performClick() { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); if (mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); mOnClickListener.onClick(this); return true; } return false; }
if (mOnClickListener != null) {
mOnClickListener.onClick(this);
return true;
}
久违了~我们的mOnClickListener ;
别激动,还没结束,回到ACTION_UP,
58行:如果prepressed为true,进入IF体:
为mPrivateFlags设置表示为PRESSED,刷新背景,125毫秒后执行mUnsetPressedState
否则:mUnsetPressedState.run();立即执行;也就是不管咋样,最后mUnsetPressedState.run()都会执行;
看看这个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);
}
把我们的mPrivateFlags中的PRESSED取消,然后刷新背景,把setPress转发下去。
ACTION_UP的最后一行:removeTapCallback(),如果mPendingCheckForTap不为null,移除;
4、总结
好了,代码跨度还是相当大的,下面需要总结下:
1、整个View的事件转发流程是:
View.dispatchEvent->View.setOnTouchListener->View.onTouchEvent
在dispatchTouchEvent中会进行OnTouchListener的判断,如果OnTouchListener不为null且返回true,则表示事件被消费,onTouchEvent不会被执行;否则执行onTouchEvent。
2、onTouchEvent中的DOWN,MOVE,UP
DOWN时:
a、首先设置标志为PREPRESSED,设置mHasPerformedLongPress=false ;然后发出一个115ms后的mPendingCheckForTap,如果115ms内抬起手指,触发了UP,则不会触发click事件,并且最终执行的是UnsetPressedState对象,setPressed(false)将setPress的传递下去;这种情况很少发生,可能只会在压力测试的时候会发现无法触发click事件;
b、如果115ms内没有触发UP,则将标志置为PRESSED,清除PREPRESSED标志,同时发出一个延时为500-115ms的,检测长按任务消息;
c、如果500ms内(从DOWN触发开始算),则会触发LongClickListener:
此时如果LongClickListener不为null,则会执行回调,同时如果LongClickListener.onClick返回true,才把mHasPerformedLongPress设置为true;否则mHasPerformedLongPress依然为false;
MOVE时:
主要就是检测用户是否划出控件,如果划出了:
115ms内,直接移除mPendingCheckForTap;
115ms后,则将标志中的PRESSED去除,同时移除长按的检查:removeLongPressCallback();
UP时:
a、如果115ms内,触发UP,此时标志为PREPRESSED,则执行UnsetPressedState,setPressed(false);会把setPress转发下去,可以在View中复写dispatchSetPressed方法接收;
b、如果是115ms-500ms间,即长按还未发生,则首先移除长按检测,执行onClick回调;
c、如果是500ms以后,那么有两种情况:
i.设置了onLongClickListener,且onLongClickListener.onClick返回true,则点击事件OnClick事件无法触发;
ii.没有设置onLongClickListener或者onLongClickListener.onClick返回false,则点击事件OnClick事件依然可以触发;d、最后执行mUnsetPressedState.run(),将setPressed传递下去,然后将PRESSED标识去除;
最后问个问题,然后再运行个例子结束:
1、setOnLongClickListener和setOnClickListener是否只能执行一个
不是的,只要setOnLongClickListener中的onClick返回false,则两个都会执行;返回true则会屏幕setOnClickListener
最后我们给MyButton同时设置setOnClickListener和setOnLongClickListener,运行看看:
package com.example.zhy_event03; import android.app.Activity; import android.os.Bundle; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; import android.view.View.OnTouchListener; import android.widget.Button; import android.widget.Toast; public class MainActivity extends Activity { protected static final String TAG = "MyButton"; 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(TAG, "onTouch ACTION_DOWN"); break; case MotionEvent.ACTION_MOVE: Log.e(TAG, "onTouch ACTION_MOVE"); break; case MotionEvent.ACTION_UP: Log.e(TAG, "onTouch ACTION_UP"); break; default: break; } return false; } }); mButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Toast.makeText(getApplicationContext(), "onclick",Toast.LENGTH_SHORT).show(); } }); mButton.setOnLongClickListener(new OnLongClickListener() { @Override public boolean onLongClick(View v) { Toast.makeText(getApplicationContext(), "setOnLongClickListener",Toast.LENGTH_SHORT).show(); return false; } }); } }效果图:
可以看到LongClickListener已经ClickListener都触发了~
最后,本篇博文完成了对View的事件分发机制的整个流程的说明,并且对源码进行了分析;
当然了,View结束,肯定到我们的ViewGroup了,请点击:Android ViewGroup事件分发机制
相关推荐
源码分析2.1 Activity中的分发流程dispatchTouchEventonTouchEvent总结2.2 ViewGroup中的分发流程dispatchTouchEventonInterceptTouchEvent总结2.3 View中的分发流程dispatchTouchEventonTouchEventACTION_...
主要介绍了Android中View的容器ViewGroup,本文从源码解析ViewGroup的事件分发机制等,非常有深度,需要的朋友可以参考下
安卓java读取网页源码 AndroidStudy Android进阶内容整理 blog地址链接: ...Android事件分发机制完全解析,带你从源码的角度彻底理解(上) 2.公共技术点之 View 事件传递 3.EventBus 源码解析 EventBus gre
View事件分发机制 View滑动 自定义View User Interface TextView ImageView Gallary ScrollView RecyclerView 绘制图形 动画 图片加载 图片压缩 图片缓存 LruCache DiskLruCache 消息机制 Handler ...
3.4 View的事件分发机制 140 3.4.1 点击事件的传递规则 140 3.4.2 事件分发的源码解析 144 3.5 View的滑动冲突 154 3.5.1 常见的滑动冲突场景 155 3.5.2 滑动冲突的处理规则 156 3.5.3 滑动冲突的解决方式 ...
3.4 View的事件分发机制 / 140 3.4.1 点击事件的传递规则 / 140 3.4.2 事件分发的源码解析 / 144 3.5 View的滑动冲突 / 154 3.5.1 常见的滑动冲突场景 / 155 3.5.2 滑动冲突的处理规则 / 156 3.5.3 滑动...
对于Android开发者来说,View无疑是开发中经常接触的,包括它的事件分发机制、测量、布局、绘制流程等,如果要自定义一个View,那么应该对以上流程有所了解、研究。本系列文章将会为大家带来View的工作流程详细解析...
/ 133 3.3 弹性滑动 / 135 3.3.1 使用Scroller / 136 3.3.2 通过动画 / 138 3.3.3 使用延时策略 / 139 3.4 View的事件分发机制 / 140 3.4.1 点击事件的传递规则 / 140 3.4.2 事件分发的源码解析 / 144 3.5 ...
Android相关总结啥也不是JAVA相关知识点链接基础泛型注解反射并发序列化Json解析IO网络数据结构相关知识点链接HashMap阻塞队列JVM相关知识点链接JVMView相关知识点链接自定义View事件分发机制RecyclerView解析...
的事件分发机制实例和源码解析。 本篇参照郭神 Blog 中的实例,来分析 ViewGroup 的事件分发机制。 本章介绍 View(视图) 动画相关概念以及应用。 本篇介绍 Handler 和 Message 以及 Looper 的基本用法和工作原理。 ...
个人图片文件仓库 ...深入理解事件分发机制 FastDFS Android View 的滑动方式 从源码角度深入理解OkHttp 从源码角度深入理解Retrofit2 从源码角度深入理解Glide hexo 博客搭建 Git Spring Cloud Docker Kubernetes