我的简书同步发布:完全理解View事件体系!
转载请注明出处:【huachao1001的专栏:http://blog.csdn.net/huachao1001】
View的事件体系整体上理解还是比较简单的,但是却有很多细节。这些细节很容易忘记,本文的目标是理解性的记忆,争取做到看完不忘。最近在温习,希望本文能对你也有所帮助。如果你已对View事件体系有1定的了解,那末查漏补缺,看看你是否是已掌握了以下内容呢?
在正式接触View事件体系之前,先看看相干基础部份。
在Android系统中,1个子View在ViewGroup中显示的区域由top、right、bottom、left4个属性肯定。它们分别肯定4条边,以下图所示:
这4个参数我们可以通过以下方法得到:
//假定v是个View实例
//View v=···;
int top = v.getTop();
int right = v.getRight();
int bottom = v.getBottom();
int left = v.getLeft();
拿到这4个参数后,我们也能够计算出宽高:
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)VelocityTracker 速度追踪
我们知道,很多ViewGroup
中,假定手指滑动的距离相同,但是滑动速度不同,那末滑动速度越快,ViewGroup
中内容转动的距离越远。那末如何辨认用户滑动的速度呢?固然了,你可以在onTouchEvent
中不断的监听计算。但是那样的代码太臃肿了,而且容易算错。好在Android
系统内置了速度追踪类VelocityTracker
。有了它,妈妈不再用担心如何计算速度追踪。先看看怎样用:
//event1般是通过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
中。
前面做了基础热身以后,我们现在开始学习View的事件分发机制。View的事件分发主要是由3个函数决定:dispatchTouchEvent
、 onInterceptTouchEvent
和 onTouchEvent
。1个触摸事件,如果事件坐标处于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。这1个进程为同1个事件系列,当不拦截该系列事件是,该系列事件的所有的事件都不会拦截。
我们知道,我们直接通过onTouchEvent里面的形参就能够拿到事件对象,可是事件对象时从哪里产生的?又是经历过哪些曲折的道路才到达目的地的?
首先,Activity
拿到事件对象,Activity
把事件对象传递给PhoneWindow
,PhoneWindow
再传递给DecorView
,DecorView
通过遍历再传递到我们的ViewGroup
。那末Activity
又是从哪里得到事件对象的呢?这里面就触及的比较底层了,感兴趣的童鞋参考任玉刚的《 Android中MotionEvent的来源和ViewRootImpl 》这篇文章。
当1个View处理触摸事件时,如果同时设置了OnTouchListener
(内含onTouch
抽象方法)、OnClickListener
(内含onClick
抽象方法).那末到底哪一个函数先履行?我们做1个实验,自定义1个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
了。这个进程我们暂时先放1放,回到我们前面所说的,为何onClick
不履行?注意!甚么是点击?其实,点击包括down
和up
,因此我们需要判断down
和up
是不是都是在当前View区域内,我们固然就没办法只根据1个事件来判断是不是需要履行onClick
。因此,onTouchEvent
的返回值不能用于决定是不是把事件传递给onClick
。如果想把事件传递到onClick
函数,我们需要在onTouchEvent
里做判断,并显式调用OnClickListener
实例对象的onClick
。固然了,你可以不用自己写,直接在你的onTouchEvent中的最后1句改成:
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
才会履行。
我们知道,在ViewGroup
中,事件是dispatchTouchEvent
->onInterceptTouchEvent
->onTouchEvent
。由onInterceptTouchEvent
决定是不是将事件传递给子View。如果传递给子View,但是子View其实不想处理这个系列的事件(子View的onTouchEvent
返回false),该怎样处理这个系列事件呢?难道就抛弃这个系列的触摸事件不管了吗?固然不是!我们先看1段测试代码:
自定义的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
也是1样,如果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
,而是默默的消逝掉。我们知道,1旦消耗down
时间,接下来的该系列所有的事件都会交给这个View
,因此,如果不处理down
之外的事件,这些事件就会被“抛弃”。- 如果
ViewGroup
决定拦截,那末这个系列事件都只能由它处理,并且onInterceptTouchEvent
不会再被调用。- 某个
View
,在onTouchEvent
中,如果针对最开始的down
事件都返回false
,那末接下来的事件系列都不会交给这个View
。ViewGroup
默许不拦截事件,即onInterceptTouchEvent
默许返回false
。View
的onTouchEvent
默许返回true
,即消耗事件。View
没有onInterceptTouchEvent
方法。