Android事件处理分发机制的总结:一(事件处理)
2016-06-13 18:58
417 查看
从View的dispatchTouchEvent可以看出,事件最终的处理无非是交给TouchListener的onTouch方法或者是交由onTouchEvent处理,由于onTouch默认是空实现,由程序员来编写逻辑,那么我们来看看onTouchEvent事件。
首先我们来看一个比较简单的onTouchEvent的处理,那就是View,我们知道,View只能响应click和longclick,不具备滑动等特性。
从上面的代码我们总结一下View对触摸事件的处理:
**1、是否为diabale,如果是,直接根据是否设置了click和longclick来返回。
2、是否设置了触摸代理对象,如果有,把事件传递给触摸代理对象,交由其处理,如果消费了,直接返回
3、是否为click或者longclick的,如果是,返回true,不是返回false。
而View对click和longclick的处理如下:
1、Down:
1.判断是否可以触摸上下文菜单。
2.是否在可以滑动的容器中,如果是先设置临时按压,再发送一个延迟消息把临时按压改为按压,并发送一个延迟500毫秒的事件去执行长按代码
3.如果不在滚动容器中,直接设置按压状态,并发送一个延迟500毫秒的事件去执行长按代码。
2、Move:
1、取触摸点坐标判断是否在View中(额外增加了8像素的范围)
2、如果在,不用做任何事。
3、如果不在,取消临时按压到按压回调,取消长按延迟回调,设置为非按压状态
3、Up
1、判断是否为按压或者临时按压状态
2、如果不是,不做任何处理
3、如果是先判断其是否可以获取焦点,然后请求焦点。
4、如果是临时按压状态,设置临时按压状态为按压状态。保证界面被绘制成按压状态,让用户可以看见。
5、如果长按回调还未触发,取消长按回调,如果不是焦点状态,触发click事件。
6、如果是临时按压状态,发送一个延迟取消按压状态的,保证按压状态持续一段时间,让用户可见。
7、如果不是临时按压状态,直接发送消息取消按压状态。发送失败,直接取消按压状态。
8、取消把临时按压设置按压的回调。**
从中我们知道View的onTouchEvent主要处理了click和longclick事件,当按下时,向消息机制发送一个延迟500毫秒的长按回调事件,当移动时候判断是否移出了View的范围,超出则取消事件。当离开时,判断长按事件是否触发了,如果没触发且不是焦点,就触发click事件。
在这里最绕的就是临时按压和按压状态,临时按压是为了处理滑动容器的,让处于滑动容器中,按下时,我们先设置的是临时按压,持续64毫秒,是为了判断接下来的时间内是否发生了move事件,如果发生了,将不会再出发按压状态,这样不会让用户看到listView滚动时,item还处于按压状态。在离开时,我们再次判断是否处于临时按压,如果是在64毫秒内触发了down和up,说明按压状态还没来得急绘制,则强制设置为按压状态,保证用户能看到,并在取消回调的方法上加上64毫秒的延迟
之前看了View的onTouch,知道里面处理了点击事件,那么我们再来看看ScrollView来看看滑动事件的处理:
通过以上分析,我们得出以下知识:
**1.在down事件的时候先判断触摸是否处于边缘,如果是,则不处理
2.在down事件中判断落点是否在子View中,如果不在,不处理
3.在down事件中判断是否仍在滑动,如果是,先停止
4.记录第一个按下点的索引值
5.每次事件都记录住当前的y值
6.在move事件中通过记录的索引值找到对应的点,获取y坐标
7.与上一次y坐标进行比对,scrollBy两次的差值
8.在up事件的时候计算每秒的速度,并且有最大速度进行限制,当计算的速度大于系统默认的最小速度时,只想fling
9.up和cancel事件还原变量为默认值
10.如果为多点离开,进行多点离开的处理
11.该处理方式时:如果离开的是第一个按下的点,那么由第二个按下的点代替其进行y值偏移计算的基点,并清空速度计算的帮助类,重新记录MotionEvnet**
首先我们来看一个比较简单的onTouchEvent的处理,那就是View,我们知道,View只能响应click和longclick,不具备滑动等特性。
public boolean onTouchEvent(MotionEvent event) { final int viewFlags = mViewFlags; //先判断标示位是否为disable,也就是无法处理事件。 if ((viewFlags&ENABLED_MASK)==DISABLED) { if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); }//如果是UP事件,并且状态为按压,取消按压。 //系统源码解释:虽然是disable,但是还是可以消费掉触摸事件,只是不触发任何click或者longclick事件。 //根据是否可点击,可长按来决定是否消费点击事件。 return (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)); } if (mTouchDelegate != null) { //先检查触摸的代理对象是否存在,如果存在,就交由代理对象处理。 // 触摸代理对象是可以进行设置的,一般用于当我们手指在某个View上,而让另外一个View响应事件,另外一个View就是该View的事件代理对象。 if (mTouchDelegate.onTouchEvent(event)) {//如果代理对象消费了,则返回true消费该事件 return true; } } if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { //如果是可点击或者长按的标识位执行下面的逻辑,这些标志位可以设置,也可以设置了对应的listener后自动添加 //因为作为一个View,它只能单纯的接受处理点击事件,像滑动之类的复杂事件普通View是不具备的。 switch (event.getAction()) { case MotionEvent.ACTION_UP://处理Up事件 boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;//是否包含临时按压状态 if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {//如果本身处于被按压状态或者临时按压状态 //临时按压状态会在下面的Move事件中说明 boolean focusTaken = false; if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { //如果它可以获取焦点,并且可以通过触摸来获取焦点,并且现在不是焦点,则请求获取焦点,因为一个被按压的View理论上应该获取焦点 focusTaken = requestFocus(); } if (prepressed) { //如果是临时按压,则设置为按压状态,PFLAG_PREPRESSED是一个非常短暂的状态,用于在某些时候短时间内表示Pressed状态,但不需要绘制 setPressed(true);//设置为按压状态,是因为临时按压不会绘制,这个时候强制绘制一次,确保用户能够看见按压状态 } if (!mHasPerformedLongPress) { //是否执行了长按事件,还没有的话,这个时候可以移除长按的回调了,因为UP都已经触发,说明从按下到UP的时间不足以触发longPress //至于longPress,会在Down事件中说明 removeLongPressCallback(); if (!focusTaken) {//如果是焦点状态,就不会触摸click,这是为什么呢?因为焦点状态一般是交给按键处理的, //pressed状态才是交给触摸处理,如果它是焦点,那么它的click事件应该由按键来触发 if (mPerformClick == null) { //封装一个Runnable对象,这个对象中实际就调用了performClick(); mPerformClick = new PerformClick(); } if (!post(mPerformClick)) {//向消息队列发生该runnabel,如果发送不成功,则直接执行该方法。 performClick();//这个方法内部会调用clickListner } //为什么不直接执行呢?如果这个时候直接执行,UP事件还没执行完,发送post,可以保障在这个代码块执行完毕之后才执行 } } (mUnsetPressedState == null) {//仍旧是创建一个Runnabel对象,执行setPressed(false) mUnsetPressedState = new UnsetPressedState(); } if (prepressed) { //如果是临时按压状态,之前的Down和move都还未触发按压状态,只在up时设置了,这个状态才刚刚绘制,为了保证用户能看到,发生一个64秒的延迟消息,来取消按压状态。 postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration()); //这是一个64毫秒的短暂时间,这是为了让这个按压状态持续一小段时间,以便手指离开时候,还能看见View的按压状态 } else if (!post(mUnsetPressedState)) {//如果不是临时按压,则直接发送,发送失败,则直接执行 mUnsetPressedState.run(); if } removeTapCallback(); //移除这个callBack,这个callBack内部就是把临时按压状态设置成按压状态,因为这个已经没必要了,手指已经up了 } break; case MotionEvent.ACTION_DOWN: mHasPerformedLongPress = false; //按下事件把长按事件执行的变量设置为false,代表还没执行长按,因为才按下,表示新的一个长按事件可以开始计算了 if (performButtonActionOnTouchDown(event)) { //先把这个事件交由该方法,该方法内部会判断是否为上下文的菜单按钮,或者是否为鼠标右键,如果是就弹出上下文菜单。 //现在有些手机的上下文菜单按钮也是在屏幕触屏上的 break; } //这个方法会一直往上找父View,判断自身是否在一个可以滚动的容器中 boolean isInScrollingContainer = isInScrollingContainer(); //如果是在一个滚动的容器中,那么按压事件将会被推迟一段时间,如果这段时间内,发生了Move,那么按压状态讲不会被显示,直接滚动父视图 if (isInScrollingContainer) { mPrivateFlags |= PFLAG_PREPRESSED; //先添加临时的按压状态,该状态表示按压,但不会绘制 if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); //创建一个runnable对象,这个runnable内部会取消临时按压状态,设置为按压状态,并启动长按的延迟事件 } postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); //向消息机制发生一个64毫秒的延迟时间,该事件会取消临时按压状态,设置为直接按压,并启动长按时间的计时 } else { //如果不在一个滚动的容器中,则直接设置按压状态,并启动长按计时 setPressed(true); checkForLongClick(0); //长按事件就是向消息机制发送一个runnable对象,封装的就是我们在lisner中的代码,延迟500毫秒执行,也就是说长按事件在我们按下的时候发送,在up的时候检查一下执行了吗?如果没执行,就取消,并执行click } break; case MotionEvent.ACTION_CANCEL: //如果是取消事件,那就好办了,把我们之前发送的几个延迟runnable对象给取消掉 setPressed(false); //设置为非按压状态 removeTapCallback(); //取消mPendingCheckForTap,也就是不用再把临时按压设置为按压了 removeLongPressCallback(); //取消长按事件的延迟回调 break; case MotionEvent.ACTION_MOVE: //move事件 final int x = (int) event.getX(); //取触摸点坐标 final int y = (int) event.getY(); // 用于判断是否在View中,为什么还要判断呢? //这是因为父View是在Down事件中判断是否在该View中的,如果在,以后的Move和up都会传递过来,不再进行范围判断 if (!pointInView(x, y, mTouchSlop)) { //mTouchSlop是一个常量,不同的手机值不一样,dpi越高,值大,一般数值为8,也就是说,就算你的落点超出了View的8像素位置,也算在View中。 //是因为人的手指触摸点比较大,有可能你感觉点在某个控件的边缘,但是实际落点已经超出这个View,所以这里给了8像素的范围 removeTapCallback();//如果在范围外,就移除这些runnable回调 if ((mPrivateFlags & PFLAG_PRESSED) != 0) { //如果是按压状态,就取消长按,设置为非按压状态,为什么这个时候取消呢,因为在Down的时候,我们可以知道,只有是按压状态,才会设置长按 removeLongPressCallback(); setPressed(false); } } break; } return true; //至此,可以返回true,消费该事件 } return false; //如果不可点击,也不可长按,则返回false,因为View只具备消费点击事件 }
从上面的代码我们总结一下View对触摸事件的处理:
**1、是否为diabale,如果是,直接根据是否设置了click和longclick来返回。
2、是否设置了触摸代理对象,如果有,把事件传递给触摸代理对象,交由其处理,如果消费了,直接返回
3、是否为click或者longclick的,如果是,返回true,不是返回false。
而View对click和longclick的处理如下:
1、Down:
1.判断是否可以触摸上下文菜单。
2.是否在可以滑动的容器中,如果是先设置临时按压,再发送一个延迟消息把临时按压改为按压,并发送一个延迟500毫秒的事件去执行长按代码
3.如果不在滚动容器中,直接设置按压状态,并发送一个延迟500毫秒的事件去执行长按代码。
2、Move:
1、取触摸点坐标判断是否在View中(额外增加了8像素的范围)
2、如果在,不用做任何事。
3、如果不在,取消临时按压到按压回调,取消长按延迟回调,设置为非按压状态
3、Up
1、判断是否为按压或者临时按压状态
2、如果不是,不做任何处理
3、如果是先判断其是否可以获取焦点,然后请求焦点。
4、如果是临时按压状态,设置临时按压状态为按压状态。保证界面被绘制成按压状态,让用户可以看见。
5、如果长按回调还未触发,取消长按回调,如果不是焦点状态,触发click事件。
6、如果是临时按压状态,发送一个延迟取消按压状态的,保证按压状态持续一段时间,让用户可见。
7、如果不是临时按压状态,直接发送消息取消按压状态。发送失败,直接取消按压状态。
8、取消把临时按压设置按压的回调。**
从中我们知道View的onTouchEvent主要处理了click和longclick事件,当按下时,向消息机制发送一个延迟500毫秒的长按回调事件,当移动时候判断是否移出了View的范围,超出则取消事件。当离开时,判断长按事件是否触发了,如果没触发且不是焦点,就触发click事件。
在这里最绕的就是临时按压和按压状态,临时按压是为了处理滑动容器的,让处于滑动容器中,按下时,我们先设置的是临时按压,持续64毫秒,是为了判断接下来的时间内是否发生了move事件,如果发生了,将不会再出发按压状态,这样不会让用户看到listView滚动时,item还处于按压状态。在离开时,我们再次判断是否处于临时按压,如果是在64毫秒内触发了down和up,说明按压状态还没来得急绘制,则强制设置为按压状态,保证用户能看到,并在取消回调的方法上加上64毫秒的延迟
之前看了View的onTouch,知道里面处理了点击事件,那么我们再来看看ScrollView来看看滑动事件的处理:
public boolean onTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) { //如果是down事件,并且触摸到边缘,就不处理EdgeFlags代表是否为边缘,其值是1/2/4/8。代表上下左右 return false; } if (mVelocityTracker == null) { //这是一个追踪触摸事件,并计算速度的帮助类,实现原理就是用三个数组分别记录每次触摸的x/y和时间 mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(ev); final int action = ev.getAction(); switch (action & MotionEvent.ACTION_MASK) {//与上ff,去掉高位有关多点的信息 case MotionEvent.ACTION_DOWN: {//如果是down final float y = ev.getY();//获取y坐标 if (!(mIsBeingDragged = inChild((int) ev.getX(), (int) y))) {//判断是否开始拖动 //原理就是判断落点是否在child中,ScrollView只能由一个child,如果在,返回true,反之false //也就是说落点在child中,就是准备开始拖动,不在,就直接返回,这可能是因为设置了padding之类的缘故造成的 return false; } if (!mScroller.isFinished()) {//判断滚动是否完成 mScroller.abortAnimation();//如果没完成,停止滚动 //对应上一次用户手指离开时候处理fling状态,这次按下手指,直接停止滚动 } //记录y坐标,以便下次事件来对比 mLastMotionY = y; mActivePointerId = ev.getPointerId(0);//记住多点的id,下次取值时只取该点的 break; } case MotionEvent.ACTION_MOVE: if (mIsBeingDragged) {//可以看出,如果down的时候落点在child外,则以后就算滑进了child也不处理 //根据上次记录的多点id,找到对应的点,取y值 final int activePointerIndex = ev.findPointerIndex(mActivePointerId); final float y = ev.getY(activePointerIndex); final int deltaY = (int) (mLastMotionY - y);//计算位移 mLastMotionY = y;//重新记录y值 scrollBy(0, deltaY);//滚动指定的距离,这也说明了ScrollView只具备纵向滑动 } break; case MotionEvent.ACTION_UP: if (mIsBeingDragged) {//如果是离开事件 final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);//计算最后1秒钟内的速度,并给定一个最大速度进行限制 //这个最大速度是根据屏幕密度不同而不同的,所以大家也没事别使劲滑动屏幕,因为有这个最大速度限制 //获取y方向的速度 int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); if (getChildCount() > 0 && Math.abs(initialVelocity) > mMinimumVelocity) { //如果有子View,并且计算出来的y的速度比最小速度要大,执行fling状态 //手指滑动的方向和屏幕移动的方向是相反的,所以这里加- fling(-initialVelocity); } mActivePointerId = INVALID_POINTER;//给mActivePointerId重新赋值为-1,防止下次事件找到了错误的点 mIsBeingDragged = false;//恢复默认值 if (mVelocityTracker != null) {//清空速度计算帮助类 mVelocityTracker.recycle(); mVelocityTracker = null; } } break; case MotionEvent.ACTION_CANCEL: if (mIsBeingDragged && getChildCount() > 0) {//判断条件,只有这2个条件成立,才会发生滚动事件,下面的值才会被改变,才需要恢复默认 mActivePointerId = INVALID_POINTER; mIsBeingDragged = false; if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } } break; case MotionEvent.ACTION_POINTER_UP://多点触摸时,不是最后一个点离开 onSecondaryPointerUp(ev); break; } return true; } //用于应对先按下1点,然后按下2点,1点离开后,2点仍能继续滑动的逻辑 private void onSecondaryPointerUp(MotionEvent ev) { final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;//首先对高位进行与操作,然后右移8位,获取其高位代表index的值 final int pointerId = ev.getPointerId(pointerIndex);//取出该点的id if (pointerId == mActivePointerId) {//如果这个id对应的就是第一个按下的点 //理论上pointerIndex应该是0,所以用第二个按下的点,即1index的点代替 final int newPointerIndex = pointerIndex == 0 ? 1 : 0; mLastMotionY = ev.getY(newPointerIndex);//取出新点的y坐标 mActivePointerId = ev.getPointerId(newPointerIndex);//记录新点的id if (mVelocityTracker != null) {//清空之前存入的MotionEvent,也就是说最后的速度只计算该点产生的 mVelocityTracker.clear(); } } }
通过以上分析,我们得出以下知识:
**1.在down事件的时候先判断触摸是否处于边缘,如果是,则不处理
2.在down事件中判断落点是否在子View中,如果不在,不处理
3.在down事件中判断是否仍在滑动,如果是,先停止
4.记录第一个按下点的索引值
5.每次事件都记录住当前的y值
6.在move事件中通过记录的索引值找到对应的点,获取y坐标
7.与上一次y坐标进行比对,scrollBy两次的差值
8.在up事件的时候计算每秒的速度,并且有最大速度进行限制,当计算的速度大于系统默认的最小速度时,只想fling
9.up和cancel事件还原变量为默认值
10.如果为多点离开,进行多点离开的处理
11.该处理方式时:如果离开的是第一个按下的点,那么由第二个按下的点代替其进行y值偏移计算的基点,并清空速度计算的帮助类,重新记录MotionEvnet**
相关文章推荐
- 使用C++实现JNI接口需要注意的事项
- Android IPC进程间通讯机制
- Android Manifest 用法
- [转载]Activity中ConfigChanges属性的用法
- Android之获取手机上的图片和视频缩略图thumbnails
- Android之使用Http协议实现文件上传功能
- Android学习笔记(二九):嵌入浏览器
- android string.xml文件中的整型和string型代替
- i-jetty环境搭配与编译
- android之定时器AlarmManager
- android wifi 无线调试
- Android Native 绘图方法
- Android java 与 javascript互访(相互调用)的方法例子
- android 代码实现控件之间的间距
- android FragmentPagerAdapter的“标准”配置
- Android"解决"onTouch和onClick的冲突问题
- android:installLocation简析
- android searchView的关闭事件
- SourceProvider.getJniDirectories