上次给大家分享了自定义View的要点。这次就和大家聊一聊View的事件分发和处理。为什么主题是View,因为作为一个初级应用层作为一个Android工程师,和我打交道最多的就是各种View。只有详细了解他们各自的习惯,才能更好地与他们沟通,达到预期的效果。基本保留View,MotionEvent我们可以详细的说出Android的四大组件:Activity,Service,ContentProvider和BoardcastReceiver,但是除了这四大组件之外,我们用的比较多的,其中肯定包括View,View是用户的与程序沟通的入口,也是程序向用户展示信息的窗口。关于View,一些基本的属性还是需要了解的。left,top,right,bottom分别代表视图的左上角和右下角相对于x轴和y轴的坐标,传递视图的getWidth和getHeight的值这些计算了四个值,在Android3.0中加入了属性x,y,translationX,translationY,方便我们平移视图。x和y表示当前视图左上角的xy坐标,translationX和translationY表示视图相对于其父容器的偏移量,默认值为0。MotionEvent表示用户的触摸事件,用户的一次点击、触摸或滑动会产生一系列的MotionEvents:MotionEvent.ACTION_DOWN表示用户的手指刚接触到屏幕MotionEvent.ACTION_MOVE表示用户的手指正在移动MotionEvent.ACTION_UP表示用户的手指刚刚触摸了屏幕屏幕被抬起,因此用户触摸屏幕可能会产生这些事件:点击屏幕并释放,Down->Up点击屏幕,然后滑动一段距离,释放屏幕,Down->Move->…->Move->Up了解了这些基础知识之后,我们来学习如何分发这些事件因为有事件拦截,所以先说ViewGroup,下面再简单介绍一下View的事件处理。在事件分发的过程中,主要涉及三个方法:dispatchTouchEvent(MotionEventevent);onInterceptTouchEvent(运动事件事件);onTouchEvent();乍一看,这三个方法是有界的,如果这时候再往源码里钻,我就更糊涂了。借用任玉刚的一段伪代码来解释一下三者的关系:.dispatchTouchEvent(event);}returnconsume;}从这段伪代码可以看出,在dispatchTouchEvent中,首先调用ViewGroup自己的onInterceptTouchEvent方法判断是否要拦截,如果此时自己拦截,则调用自己的onTouchEvent方法,如果onTouchEvent方法返回True,则说明本次事件被消费,事件传递到此结束,如果返回False,则证明本次MotionEvent没有被消费,那么本次如果是当前ViewGroup的onInterceptTouchEvent返回False,它会调用它的子视图的dispatchTouchEvent方法,这样事件就会被传递下去,如果它的子视图处理No,那么它会回来调用onTouchEvent方法of视图组。当然,这并没有体现在这段伪代码中。我用一个通俗的例子来解释一下:领导接到一个任务(可能是上级给的),你自己看了看,决定好好休息一下。如果他今天不上班,他就把这个任务交给手下小王。小王的默认属性是任务来了就接,不管他能不能做,他都会去做。如果这是一个简单的任务,那么小王就解决了,这个任务也就完成了。可惜,这一次,小王并没有解决任务,然后向领导汇报。领导没办法,手下没有人能解决,只好自己动手,开始解决问题,再解决,任务就完成了。这是ViewGroup层的事件分发。当然不是那么简单。这只是一种简单的理解方式。事实上,在真实的事件分发中,有很多问题需要注意:一个完整的事件序列从Down开始,可能包含几个Moves,然后以Up结束一个视图一旦一个事件被拦截,完整的事件序列在哪里currenteventlocated会被view处理,体现在真实的代码中,就是一旦view拦截了down事件,那么后续的move和up事件都不会调用onInterceptTouchEvent,而是直接由它来处理,也就是说在onInterceptTouchEvent中处理事件是不合适的,因为有可能在事件来临时直接跳过onInterceptTouchEvent方法。这也意味着一个ViewGroup一旦不拦截ACTION_DOWN,就无法接收到这个事件序列中的其他Action,所以在处理ACTION_DOWN的时候需要特别小心。在onTouchEvent中,需要判断MotionEvent的Action,因为一次点击操作会调用两次onTouchEvent方法,一次是ACTION_DOWN,一次是ACTION_UP。如果你滑动你的手,就会有几个ACTION_MOVE。默认情况下,ViewGroup不会拦截任何事件。源码中ViewGroup的onInterceptTouchEvent方法默认返回false。整个事件分发好像是从外传到内。父视图将事件传递给子视图。理论上子View是没有办法影响父View的事件处理的,但是有一个标志,requestDisallowInterceptTouchEvent方法。通过这个方法,子View可以影响父View的事件处理。这可以用来解决父视图和子视图之间的滑动冲突。如果你想了解更多,可以搜索它的相关用法,这里就不做展开了。View只静默接受View与ViewGroup不同的是,View中没有onInterceptTouchEvent方法,因为View是事件处理的最后一级,不需要判断是否拦截,必须拦截,是否可以处理或者不行,你要试试看,所以View中的调用流程是:dispatchTouchEvent->onTouchEvent而且,***onTouchEvent的返回值默认是True,也就是说事件一般会被消费传过去,就是看中间有没有人拦截。这时候读者可能会有疑问:TextView的onTouchEvent的返回值也是True吗?答案是:可以,那为什么在点击TextView的时候可以触发其父view的onTouchEvent呢?从理论上讲,它不应该。TextView消费了本次Event,没有返回。理论上是这样,但是因为TextView的clickable和longClickable属性都是false,当这两个属性都为false时,不会消费事件,所以TextView不会消费事件,这就可以解释为什么ATextView会放在最上面一个Button,点击TextView仍然可以触发按钮的点击事件。这里可能需要提醒一下大家,这是我之前踩过的一个坑。我将视图的启用状态设置为false,然后为其添加了onClickListener。这个时候我以为它的点击事件不会被触发,但是还是可以点击的。后来了解到view的enable状态和onTouchEvent没有关系,只有clickable状态对onTouchEvent有影响,还有一点,把view的enable设置为false确实会把view的clickable设置为false,但是设置view的onclickListener会把view的clickable变成true,所以最好的解决办法是设置改变那两行代码的顺序,问题就解决了。详细说明GesutureDetector是怎么处理的,费了九牛二虎之力,终于截获了事件,接下来我们需要做点什么了,不然不好意思白费了那么多口舌。说到事件处理,我们首先想到的是setOnClickListener,但是不知道onClickListener的优先级是最高的,优先级会在下一节进行说明,而这里,我们主要思考如何处理事件,当我们兴奋的拿到一系列事件,却又无从下手时,连最简单的点击事件都得自己处理,更不用说平移、旋转、缩放等操作了,但是官方的GestureDetector为我们提供了可能性。官方提供的GestureDetector是一个手势辅助检测类,默认能够检测多种手势:classSimpleGestureListenerimplementsGestureDetector.OnGestureListener{@OverridepublicbooleanonDown(MotionEvente){returnfalse;}@OverridepublicvoidonShowPress(MotionEvente){}@OverridepublicbooleanonSingleTapUp(MotionEvente){returnfalse;}@OverridepublicbooleanonScroll(MotionEvente1,MotionEvente2,floatvelocityX,floatvelocityY){returnfalse;}@OverridepublicvoidonLongPress(MotionEvente){}@OverridepublicbooleanonFling(MotionEvente1,MotionEvente2,floatvelocityX,floatvelocityY){returnfalse;}}通过这个类,我们可以点击和长按,除了按下,还有是滑动、双击等各种手势,分别处理。如果这些还是不能满足你的好奇心,还有一个官方的ScaleGestureDetector,从名字就可以判断出来。这是一个缩放手势的检测辅助类,大牛模仿了ScaleGestureDetector的思想做了平移和旋转辅助类,然后基于这些辅助类我们几乎可以做任何我们想做的事情。下面我写了一个支持平移、缩放、旋转的小Demo。privatevoidinit(){scaleGesture=newScaleGestureDetector(getContext(),newScaleListener());moveGesture=newMoveGestureDetector(getContext(),newMovingListener());rotateGesture=newRotateGestureDetector(getContext(),newRotateListener());}@MoveridepublicEbooleanon{scaleGesture.onTouchEvent(事件);moveGesture.onTouchEvent(事件);rotateGesture.onTouchEvent(事件);returntrue;}privateclassScaleListenerimplementsScaleGestureDetector.OnScaleGestureListener{@OverridepublicbooleanonScale(ScaleGestureDetectordetector){setScaleX(detector.getScaleFactor()*getScaleX());setScaleFactorScaleY()*getScaleY());returntrue;}@OverridepublicbooleanonScaleBegin(ScaleGestureDetectordetector){returntrue;}@OverridepublicvoidonScaleEnd(ScaleGestureDetectordetector){}}只贴出部分代码,看来轮换还是有问题,会稍后更正。有用的读者可以详细了解完整代码。我会在文章底部给出链接。同时感谢Androidmultitouchgesturedetectors的作者提供了这么方便的手势操作类onTouchListenerOnTouchEventOnClickListener我们知道onTou在chEvent之前你一定知道onClickListener和onTouchListener,它们都是事件消费者。onTouchListener在onTouch方法中生效,而onTouch必须先于onTouchEvent,即一旦设置了onTouchListener,并且***onTouch方法返回True,那么onTouchEvent将不再执行,onClickListener与onTouchEvent有一定关系。onTouchEvent的默认实现会调用onClickListener的onClick方法。如果重写onTouchEvent,因为onClickListener不能接受ACTION_DOWN和ACTION_UP,那么设置onClickListener将不会再次生效。此时点击或长按只能在onTouchEvent中处理
