国内最全IT社区平台 联系我们 | 收藏本站
阿里云优惠2流量王
您当前位置:首页 > php开源 > 综合技术 > 彻底理解View事件体系!

彻底理解View事件体系!

来源:程序员人生   发布时间:2016-07-06 13:43:05 阅读次数:1103次

我的简书同步发布:完全理解View事件体系!

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

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

1 View事件相干基础

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

1.1 View的坐标及宽高

在Android系统中,1个子View在ViewGroup中显示的区域由top、right、bottom、left4个属性肯定。它们分别肯定4条边,以下图所示:

子View所在区域

这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区域产生变化,就得改变topleftrightbottom。如果我们想让View的动画是实际的位置产生变化,并且要兼容3.0之前的软件,该怎样办呢?为了解决这个问题,从3.0开始,加了几个新的参数:xytranslationXtranslationY

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

这样,如果我们想要移动View,只需改变translationXtranslationY就能够了,top和left不会产生变化。也能够使用属性动画去改变translationXtranslationY

1.2 手势辨认

(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中。

2 View事件分发机制

2.1 3个重要函数

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

事件分发过程

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

2.2 事件来源

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

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

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

当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设置OnTouchListenerOnClickListener

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,因而可知,onTouchonTouchEvent优先级高。代码中,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,表示对此系列的事件不处理(不消耗),那末该系列事件又会返回到ViewGrouponTouchEvent。后续的moveup事件也不会再交给子ViewonTouchEvent了。这个进程我们暂时先放1放,回到我们前面所说的,为何onClick不履行?注意!甚么是点击?其实,点击包括downup,因此我们需要判断downup是不是都是在当前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才会履行。

2.4 事件的回传进程

我们知道,在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

看到,当子ViewonTouchEvent返回的是false,那末该系列的事件会回到ViewGrouponTouchEvent。注意,down事件先到达子View的onTouchEvent,如果子View不消耗,则down事件及其后续的事件会传到ViewGrouponTouchEvent。而ViewGrouponTouchEvent也是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
  • ViewonTouchEvent默许返回true,即消耗事件。
  • View没有onInterceptTouchEvent方法。
生活不易,码农辛苦
如果您觉得本网站对您的学习有所帮助,可以手机扫描二维码进行捐赠
程序员人生
------分隔线----------------------------
分享到:
------分隔线----------------------------
关闭
程序员人生