您的位置:首页 > 其它

彻底理解View事件体系!

2016-06-27 14:18 330 查看
我的简书同步发布:彻底理解View事件体系!

转载请注明出处:【huachao1001的专栏:http://blog.csdn.net/huachao1001】

View的事件体系整体上理解还是比较简单的,但是却有很多细节。这些细节很容易忘记,本文的目标是理解性的记忆,争取做到看完不忘。最近在复习,希望本文能对你也有所帮助。如果你已经对View事件体系有一定的了解,那么查漏补缺,看看你是不是已经掌握了以下内容呢?

1 View事件相关基础

在正式接触View事件体系之前,先看看相关基础部分。

1.1 View的坐标及宽高

在Android系统中,一个子View在ViewGroup中显示的区域由top、right、bottom、left四个属性确定。它们分别确定四条边,如下图所示:



这四个参数我们可以通过如下方法得到:

//假设v是个View实例
//View v=···;
int top = v.getTop();
int right = v.getRight();
int bottom = v.getBottom();
int left = v.getLeft();


拿到这四个参数后,我们也可以计算出宽高:

int width = right-left;
int height = bottom-top;


我们知道,在Android3.0(api 11)之前,是不能用属性动画的,只能用补间动画,而补间动画所做的动画效果只是将View的显示转为图片,然后再针对这个图片做透明度、平移、旋转、缩放等效果。这带来的问题是,
View
所在的区域并没有发生变化,变化的只是个“幻影”而已。也就是说,在Android 3.0之前,要想将
View
区域发生变化,就得改变
top
left
right
bottom
。如果我们想让
View
的动画是实际的位置发生变化,并且要兼容3.0之前的软件,该怎么办呢?为了解决这个问题,从3.0开始,加了几个新的参数:
x
y
translationX
translationY


x = left + translationX;
y = top + translationY;


这样,如果我们想要移动View,只需改变
translationX
translationY
就可以了,top和left不会发生变化。也可以使用属性动画去改变
translationX
translationY


1.2 手势识别

(1)VelocityTracker 速度追踪

我们知道,很多
ViewGroup
中,假设手指滑动的距离相同,但是滑动速度不同,那么滑动速度越快,
ViewGroup
中内容滚动的距离越远。那么如何识别用户滑动的速度呢?当然了,你可以在
onTouchEvent
中不断的监听计算。但是那样的代码太臃肿了,而且容易算错。好在
Android
系统内置了速度追踪类
VelocityTracker
。有了它,妈妈再也不用担心如何计算速度追踪。先看看怎么用:

//event一般是通过onTouchEvent函数传递的MotionEvent对象
VelocityTracker vt=VelocityTracker.obtain();
vt.addMovement(event);


VelocityTracker.obtain();
这句可以看出,这里是使用了
享元模式
,对享元模式不太熟悉的童鞋请参考我的另一篇文章《从Android代码中来记忆23种设计模式》 。那么如何获取当前的移动速度呢?

vt.computeCurrentVelocity(1000);
int xv=(int) vt.getXVelocity();
int yv=(int) vt.getYVelocity();


在调用获取x和y方向的速度之前,先要调用
computeCurrentVelocity
函数,用于设定计算速度的时间间隔。很显然,速度的计算为(终端位置-起始位置)/间隔时间。

既然是享元模式,那肯定是需要回收的啦~我们看看如何回收VelocityTracker对象:

vt.clear();
vt.recycle();


(2)GestureDetector手势检测

同样,我们有时还需要检测用户的:单击、滑动、长按、双击等动作。懒得自己去计算时间来识别,直接用系统的
GestureDector
来监听这些事件,
GestureDector
的使用也非常简单:

GestureDetector.OnGestureListener listener=new GestureDetector.OnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
//手指出品按下的瞬间
return false;
}

@Override
public void onShowPress(MotionEvent e) {
//手指触摸屏幕,并且尚未松开或拖动。与onDown的区别是,onShowPress强调没用松开和没有拖动
}

@Override
public boolean onSingleTapUp(MotionEvent e) {
//手指离开屏幕(单击)
return false;
}

@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
//手指按下并拖动,当前正在拖动
return false;
}

@Override
public void onLongPress(MotionEvent e) {
//手指长按事件
}

@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
//手指快速滑动
return false;
}
};
GestureDetector mGestureDetector = new GestureDetector(this,listener);

//防止长按后无法拖动的问题
mGestureDetector.setIsLongpressEnabled(false);


既然要让GestureDetector来识别各种动作事件,那么就得让GestureDetector来接管事件管理,即在onTouchEvent里面只写入如下代码:

return mGestureDetector.onTouchEvent(event);


我们看到,
OnGestureListener
监听器包含了各种事件的监听。除了
OnGestureListener
以外,还有
OnDoubleTapListener
它主要是处理双击相关的事件,可以通过
setOnDoubleTapListener
将该监听器设置到
GestureDetector
中。

2 View事件分发机制

2.1 三个重要函数

前面做了基础热身之后,我们现在开始学习View的事件分发机制。View的事件分发主要是由3个函数决定:
dispatchTouchEvent
onInterceptTouchEvent
以及
onTouchEvent
。一个触摸事件,如果事件坐标处于
ViewGroup
所“管辖范围”,首先调用的是该
ViewGroup
dispatchTouchEvent
函数,
dispatchTouchEvent
函数内部调用
onInterceptTouchEvent
函数,用于判断是否拦截该事件,如果拦截,则调用
ViewGroup
onTouchEvent
。否则调用子
View
dispatchTouchEvent
函数,可以参考如下图:



注意,上述图中,只是描述事件从
ViewGroup
往下传递过程,没有考虑子
View
onTouchEvent
的返回值,即没有考虑事件从子
View
往上回传的过程。后面再介绍事件回传的过程。
ViewGroup
是否拦截事件,是通过
onTnterceptTouchEvent
返回值来确定,当返回
true
时,表示拦截该事件,那么该系列事件全部传递给
ViewGroup
onTouchEvent
,如果返回
false
,则表示不拦截该系列事件,该系列事件全部交给子
View
来处理。为什么我们说是“该系列事件”,而不是说“该事件”呢?注意,View的事件体系中,从down->move->……->move->up。这一个过程为同一个事件系列,如果在onInterceptTouchEvent中返回false,那么所有的事件都不会再交给ViewGroup的的onTouchEvent。

2.2 事件来源

我们知道,我们直接通过onTouchEvent里面的形参就可以拿到事件对象,可是事件对象时从哪里产生的?又是经历过哪些曲折的道路才到达目的地的?

首先,
Activity
拿到事件对象,
Activity
把事件对象传递给
PhoneWindow
PhoneWindow
再传递给
DecorView
DecorView
通过遍历再传递到我们的
ViewGroup
。那么
Activity
又是从哪里得到事件对象的呢?这里面就涉及的比较底层了,感兴趣的童鞋参考任玉刚的《 Android中MotionEvent的来源和ViewRootImpl 》这篇文章。

2.3 从onTouch、onClick、onTouchEvent优先级开始

当一个View处理触摸事件时,如果同时设置了
OnTouchListener
(内含
onTouch
抽象方法)、
OnClickListener
(内含
onClick
抽象方法).那么到底哪个函数先执行?我们做一个实验,自定义一个
View
,重写
onTouchEvent
:

@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN: {
Log.d("--> down ", "onTouchEvent");
break;
}
case MotionEvent.ACTION_MOVE: {
Log.d("--> move ", "onTouchEvent");
break;
}
case MotionEvent.ACTION_UP: {
Log.d("--> up ", "onTouchEvent");
break;
}

}
return true;
}


并在MainActivity设置
OnTouchListener
OnClickListener


myView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
Log.d("--> down", "onTouch");
break;
}
case MotionEvent.ACTION_MOVE: {
Log.d("--> move", "onTouch");
break;
}
case MotionEvent.ACTION_UP: {
Log.d("--> up", "onTouch");
break;
}

}
return false;
}
});

myView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d("-->", "onClick");
}
});


点击后,打印的日志信息如下:

06-27 00:36:56.756 2407-2407/? D/--> down: onTouch
06-27 00:36:56.756 2407-2407/? D/--> down: onTouchEvent
06-27 00:36:56.848 2407-2407/? D/--> up: onTouch
06-27 00:36:56.849 2407-2407/? D/--> up: onTouchEvent


注意到,首先执行的是
onTouch
然后再执行
onTouchEvent
,由此可见,
onTouch
onTouchEvent
优先级高。代码中,
onTouch
返回的是
false
,表示不消耗事件,因此,触摸事件能顺利的从
onTouch
传递到
onTouchEvent
,现在我们把
onTouch
返回值改为
true
,表示消耗触摸事件,看看会打印什么日志:

06-27 00:42:09.783 2499-2499/? D/--> down: onTouch
06-27 00:42:09.863 2499-2499/? D/--> up: onTouch


正如我们所猜想的那样,并没有执行
onTouchEvent
。我们看到,
onClick
并没有执行。这是为什么呢?仔细看看
onTouchEvent
的返回值,我们看到,
onTouchEvent
返回的是
true
,表示消耗触摸事件,而此时
onClick
就没执行了。是不是可以猜想:
onTouchEvent
优先级比
onClick
高。我们把
onTouchEvent
返回值改为
false
,看看日志信息(确保
onTouch
返回值也是
false
,否则
onTouchEvent
连触摸事件都拿不到,更别谈是否消耗触摸事件的问题了):

06-27 00:48:22.214 2947-2947/? D/--> down: onTouch
06-27 00:48:22.214 2947-2947/? D/--> down: onTouchEvent


什么?!!!,为什么还是没有执行
onClick
?仔细观察会发现连
up
事件也没了~。为什么
up
事件没有了呢?主要是,
onTouchEvent
返回
false
,表示对此系列的事件不处理(不消耗),那么该系列事件又会返回到
ViewGroup
onTouchEvent
。后续的
move
up
事件也不会再交给子
View
onTouchEvent
了。这个过程我们暂时先放一放,回到我们前面所说的,为什么
onClick
不执行?注意!什么是点击?其实,点击包含
down
up
,因此我们需要判断
down
up
是否都是在当前View区域内,我们当然就没办法只根据一个事件来判断是否需要执行
onClick
。因此,
onTouchEvent
的返回值不能用于决定是否把事件传递给
onClick
。如果想把事件传递到
onClick
函数,我们需要在
onTouchEvent
里做判断,并显式调用
OnClickListener
实例对象的
onClick
。当然了,你可以不用自己写,直接在你的onTouchEvent中的最后一句改为:

return super.onTouchEvent(event);


View在onTouchEvent函数中,根据触摸事件判断,显式的调用了
OnClickListener
实例对象的
onClick
。调用过程封装到
performClick
函数中,看看
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;
}


因此可以得出结论,执行的顺序是:
onTouch->onTouchEvent->onClick
。当
onTouch
返回
false
时,
onTouchEvent
才会执行,当
onTouchEvent
显式调用
onClick
时,
onClick
才会执行。

2.4 事件的回传过程

我们知道,在
ViewGroup
中,事件是
dispatchTouchEvent
->
onInterceptTouchEvent
->
onTouchEvent
。由
onInterceptTouchEvent
决定是否将事件传递给子View。如果传递给子View,但是子View并不想处理这个系列的事件(子View的
onTouchEvent
返回false),该怎么处理这个系列事件呢?难道就抛弃这个系列的触摸事件不管了吗?当然不是!我们先看一段测试代码:

自定义的ViewGroup,重新如下函数:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
print(ev, "ViewGroup dispatchTouchEvent");
return super.dispatchTouchEvent(ev);
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
print(ev, "ViewGroup onInterceptTouchEvent");
//不拦截,将事件往子View传递
return false;
}

@Override
public boolean onTouchEvent(MotionEvent event) {

print(event, "ViewGroup onTouchEvent");
return true;

}


为了减少重复代码,我们定义了
print
函数:

private void print(MotionEvent event, String msg) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN: {
Log.d("--> down ", msg);
break;
}
case MotionEvent.ACTION_MOVE: {
Log.d("--> move ", msg);
break;
}
case MotionEvent.ACTION_UP: {
Log.d("--> up ", msg);
break;
}

}

}


自定义View,重写如下函数:

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
print(event, "childView dispatchTouchEvent");
return super.dispatchTouchEvent(event);
}

@Override
public boolean onTouchEvent(MotionEvent event) {

print(event, "childView onTouchEvent");
//子View不处理该系列事件
return false;
}


触摸子View后,打印如下信息:

06-27 01:25:38.491 3666-3666/? D/--> down: ViewGroup dispatchTouchEvent
06-27 01:25:38.491 3666-3666/? D/--> down: ViewGroup onInterceptTouchEvent
06-27 01:25:38.491 3666-3666/? D/--> down: childView dispatchTouchEvent
06-27 01:25:38.491 3666-3666/? D/--> down: childView onTouchEvent
06-27 01:25:38.491 3666-3666/? D/--> down: ViewGroup onTouchEvent
06-27 01:25:38.589 3666-3666/? D/--> up: ViewGroup dispatchTouchEvent
06-27 01:25:38.589 3666-3666/? D/--> up: ViewGroup onTouchEvent


看到,当子
View
onTouchEvent
返回的是
false
,那么该系列的事件会回到
ViewGroup
onTouchEvent
。注意,
down
事件先到达子View的
onTouchEvent
,如果子View不消耗,则
down
事件及其后续的事件会传到
ViewGroup
onTouchEvent
。而
ViewGroup
onTouchEvent
也是一样,如果
ViewGroup
不处理该系列事件,又会继续回传到
ViewGroup
的父View的
onTouchEvent
。如下图所示:



我们以上讨论的点击位置都是子View所处的区域,即如下如所示。



如果点击不是子View所处的区域,事件的传递会是怎么样的呢?我们看看日志信息:

06-27 01:48:25.064 3666-3666/? D/--> down: ViewGroup dispatchTouchEvent
06-27 01:48:25.064 3666-3666/? D/--> down: ViewGroup onInterceptTouchEvent
06-27 01:48:25.064 3666-3666/? D/--> down: ViewGroup onTouchEvent
06-27 01:48:25.143 3666-3666/? D/--> move: ViewGroup dispatchTouchEvent
06-27 01:48:25.143 3666-3666/? D/--> move: ViewGroup onTouchEvent
06-27 01:48:25.143 3666-3666/? D/--> up: ViewGroup dispatchTouchEvent
06-27 01:48:25.143 3666-3666/? D/--> up: ViewGroup onTouchEvent


可以看到,子
View
并没有调用任何函数。这很容易理解,因为压根就跟子
View
没有半毛钱关系,要是点击任意区域子
View
都会有事件传递过去那才奇怪呢!因此,可以看出,
ViewGroup
在传递触摸事件时,会遍历子
View
,判断触摸点是否在各个子
View
中,如果在,则触发调用相关函数。如果点击的位置没有子View,那么不管onIntercepTouchEvent返回的是什么,ViewGroup的onTouchEvent都会执行!

最后,有几点必须要知道的:

如果
View
只消耗
down
事件,而不消耗其他事件,那么其他事件不会回传给
ViewGroup
,而是默默的消逝掉。我们知道,一旦消耗
down
时间,接下来的该系列所有的事件都会交给这个
View
,因此,如果不处理
down
以外的事件,这些事件就会被“遗弃”。

如果
ViewGroup
决定拦截,那么这个系列事件都只能由它处理,并且
onInterceptTouchEvent
不会再被调用。

某个
View
,在
onTouchEvent
中,如果针对最开始的
down
事件都返回
false
,那么接下来的事件系列都不会交给这个
View


ViewGroup
默认不拦截事件,即
onInterceptTouchEvent
默认返回
false


View
onTouchEvent
默认返回
false
,即不消耗事件。

View
没有
onInterceptTouchEvent
方法。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: