【学习】Android中View的事件体系(上)——View的滑动和事件分发机制

View的基础知识

什么是View

View是所有控件的基类,它是一种界面层控件的抽象,它代表了一个控件。除了View还有ViewGroup,它也继承了View,意味着View本身就可以是单个控件也可以是由多个控件组成的一组控件,通过这种关系形成了View树的结构。ViewGroup内部可以有子View,子View也可以是ViewGroup

View的位置参数

决定位置的四个属性
top(左上角横坐标)、 left(左上角纵坐标)、right(右下角横坐标)、bottom(右下角纵坐标),这些坐标都是相对于View的父容器来说的,因此它是一种相对坐标。在Android中,x轴和y轴分别向右和下。不仅Android,大部分显示系统都是按这个标准来定义坐标系。看下图 image.png 可得出宽高和坐标的关系
width=right-lfet
height=bottom-top

获得四个位置参数的方法

Left:getLeft(); 返回mLeft
Right:getRight(); 返回mRight
Top:getTop(); 返回mTop
Bottom:getBottom(); 返回mBottom

额外参数

从Android3.0开始,View增加了几个参数:x y,它们是View左上角的坐标,translationX和translationY,它们是View左上角相对于父容器的偏移量。这几个参数也是相对于父容器的坐标,并且translationX和translationY的默认值为0。View也为它们提供了get/set方法。
换算关系:x=left+translationX y=top+translationY。
需要注意:View在平移的过程中top和left表示的是原始左上角的位置信息,它们的值不会改变,此时发生改变的是x,y,translationX,translationY。

MotionEvent和TouchSlop

MotionEvent

手指接触屏幕后产生的一系列事件中,典型事件有3种
1.ACTION_DOWN--手指刚接触屏幕
2.ACTION_MOVE--手指在屏幕上移动
3.ACTION_UP--手机2从屏幕上松开的一瞬间

正常情况下,一次手指触摸屏幕的行为会触发一系列点击事件
1.点击屏幕后离开松开:DOWN->UP
2.点击屏幕滑动一会再松开,DOWN->MOVE->...MOVE->UP

通过MotionEvent对象我们可以得到点击事件发生的xy坐标。为此,系统提供了getX/getY(返回的是相对于当前View左上角的xy坐标)和getRawX/getRawY(返回的是相对于手机屏幕左上角的xy坐标)

TouchSlop

TouchSlop是系统所能识别出的被认为是滑动的最小距离,也就是说当两次滑动之间的距离小于这个常值,系统便不认为是在进行滑动操作,不同设备这个值可能是不同的。
获取该常量的方法:ViewConfiguration.get(getContext()).getScaledTouchSlop(); 该常量的意义:当我们处理滑动时,可以利用这个常量来做一些过滤

VelocityTracker、GestureDetector、Scroller

1.VelocityTracker

速度追踪,用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度

使用方法

在View的onTouchEvent方法种追踪当前单击事件的速度
VelocityTracker v=VelocityTracker.obtain();
v.addMovement(event) //event是onTouchEvent方法的参数
接着,当我们先知道当前的滑动速度时,可采用如下方式获得当前的速度
v.computeCurrentVelocity(100);//调用get方法前必须先计算速度,即调用computeCurrentVelocity方法,这里的速度指一段时间内手指所滑过的像素数,参数为时间间隔(ms)
int x=(int)v.getXVelocity();
int y=(int)v.getYVelocity();
计算公式:速度=(终点位置-起点位置)/时间段
根据公式我们可以得出当手指逆着坐标系的正方向滑动时,所产生的速度就为负值。当不需要使用它时,调用clear和recycle方法来重置并回收内存

GestureDetector

手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。

使用方法

1.创建一个GestureDetector对象并实现OnGestureListener接口,根据需要还可以实现OnDoubleTapListener从而监听双击行为。(原书中的单参数的构造器已经被弃用) 看下图

image.png
可使用setIsLongpressEnabled(false)来解决长按屏幕后无法拖动的现象

2.接管目标View的onTouchEvent方法:在待监听View的onTouchEvent方法中
boolean consume=g.onTouchEvent(event);
return consume;
image.png
看上图注释可以看出g.onTouchEvent(event)如果消费/接管了当前事件会返回true

做完上面两步后,我们就可有选择地实现OnGestureListener和OnDoubleTapListener中的方法。 看下图 image.png
image.png

在开发中比较常用的有:onSingleTapUp(单击)、onFling(快速滑动)、onScroll(拖动)、onLongPress(长按)、onDoubleTap(双击)。在实际开发中可以GestureDetector,完全可以在View的onTouchEvent中实现所需的监听。如果只是监听滑动相关的,建议自己在onTouchEvent实现,如果要监听双击这些行为的话就使用GestureDetector。

Scroller

弹性滑动对象,用于实现View的弹性滑动。当我们使用View的scrollTo/scrollBy方法来进行滑动时,其过程是瞬间完成的,这个没有过渡效果的滑动用户体验不好。这个时候就可以使用Scroller来实现有过渡效果的滑动,其过程不是瞬间完成的,而是在一定的时间间隔内完成的。Scroller本身无法让View弹性滑动,它需要和View的computeScroll方法配合使用才能共同完成这个功能,它只是一个移动计算辅助类,用于跟踪控件滑动轨迹,最终还是通过View的scrollTo/By来完成。 看下图

image.png
image.png

基本使用流程

1.通过Seroller的startScroll开始一个滑动动画控制,里面进行一些轨迹参数的设置和计算
2.在调用startScroll后面调用invalidate()让视图重绘从而触发ViewGroup中的computeScroll被调用
3.在computeScroll中,首先调用Scroller中的computeScrollOffest方法,里面根据当前消耗时间进行轨迹坐标的计算,然后取得计算当前滑动的偏移坐标,调用View的serollTo进行滑动控制

View的滑动

滑动几乎是应用的标配,掌握滑动的方法是实现绚丽的自定义控件的基础。

三种方式可以实现View的滑动

1.通过View本身的scrollTo/By

image.png
看上图,我们可以看到scroolBy实际上也是调用了scrollTo。 image.png
看上图scrollBy实现了基于当前位置的相对滑动,而scrollTo实现了基于所传递参数的绝对滑动。

理解滑动过程中View内部的mScrollX和mScrollY的改变规则

这两个属性可以通过getScrollX/Y得到。在滑动过程中,mScrollX的值总是等于View左边缘和View内容左边缘在水平方向的距离,而mScrollY的值总是等于View上边缘和View内容上边缘在竖直方向的距离。View边缘是指View中的内容的边缘,scrollTo和scrollBy只能改变View内容的位置而不能改变View在布局中的位置。两个属性的单位都是像素,并且当View左边缘在View内容左边缘的右边时,mScrollX为正值,当View上边缘在View内容上边缘的下边时,mScrollY为正值,反之为负值。也就是如果从左向右滑动,那么mScrollX为负值,反之为正值,如果从上往下滑动,mScrollY为负值,反之为正值。 看下图

image.png

2.使用动画

通过动画我们能够让一个View进行平移,而平移就是一种滑动,使用动画来移动View,主要是操作View的translationX和translationY属性,既可以采用传统的View动画,也可以采用属性动画。 image.png
View动画是对View的影像做操作,并不能真正改变View的位置参数,包括宽/高,,如果希望动画后的状态得以保留还必须将fillAfter属性设置为true,否则动画完成后其动画效果会消失。使用属性动画不会有这个问题。

3.改变布局参数

改变布局参数即是改变LayoutParams。比如我们想把一个view向右平移100px,我们只需要将这个View的LayoutParams里的marginLeft参数的值增加100px即可

各种滑动方式的对比

scrollTo/By是View提供的原生方法,其作用是专门用于View的滑动,它可以比较方便地实现滑动效果并且不影响内部元素的单击事件,但不能滑动View本身。使用属性动画没有明显缺点,View动画不能改变自身属性。改变布局没有明显缺点,它的主要适用对象是一些具有交互性的View。看下图

image.png

弹性滑动

思想

将一次大的滑动分成若干次小的滑动并在一个时间段内完成

Scroller

前面已经介绍过它的使用方法,接下来分析源码 看下图 image.png
之前将Scroller的时候我们构造一个Scroller对象并调用它的startScroll方法时,Scroller其实什么也没做,它只保存了我们传递的参数。startX和stratY表示滑动的起点,dx,dy表示滑动的距离,duration表示滑动时间,,这里的滑动指的是View内容滑动而非View本身位置的改变。 可以看到仅仅调用startScroll是无法让View滑动的,让View弹性滑动的是startScroll方法下面的invalidate方法,它会导致视图重绘,在View的draw方法中又会去调用computeScroll方法,它在View中是一个空实现,因此需要我们自己去实现,上面的代码已经实现了computeScroll方法,正是因为它才能实现弹性滑动。当View重绘后会在draw方法中调用computeScroll,而computeScroll又会去向Scroller获取当前的scrollX和scrollY,然后通过scrollTo方法实现滑动,接着又调用postInvalidate方法进行第二次重绘,这一次重绘过程和第一次一样,然后继续向Scroller获取当前的ScrollX和ScrollY,并通过scrollTo方法滑动到新的位置,如此反复知道整个滑动过程结束。

computeScrollOffest的实现

image.png 看上图,留意timePassed和mDuration,这个方法会根据时间的流逝的百分比计算出scrollX和scrollY改变的百分比并算出当前的值,它返回true时表示滑动还未结束,false时表示滑动结束。

总结

Scroller本身不能实现View的滑动,需要配合View的computeScroll方法才能完成弹性滑动的效果,他不断地让View重绘,而每一次重绘距滑动起始时间会有一个时间间隔,通过这个间隔Scroller就可以得出View当前的滑动位置,知道了滑动位置就可以通过scrollTo方法完成View的滑动。就这样,View的每一次重绘都会导致View进行小幅度的滑动,而多次小幅度的滑动就组成了弹性滑动。

动画

我们可以利用动画的特性去完成动画本身完成不了的东西。 image.png
看上图,这个动画本质上没有作用于任何View,而是View利用动画的完成比例来计算出View要滑动的距离。

使用延时策略

核心思想

通过发送一系列延时信息从而达到一种渐进式的效果,具体来说可以使用Handler或View的postDelayed方法,也可以使用现成的sleep方法。对于postDelayed来说,我们可以通过它延时发送消息然后再消息中来进行View的滑动,如果接连不断发送这种延时信息就可以实现弹性滑动的效果。对于sleep方法来收,通过在while循环中不断地滑动View和sleep就可以实现弹性滑动。

View的事件分发机制

点击事件的传递规则

所谓点击事件的分发,其实就是对MotionEvent事件的分发过程,即当一个MotionEvent产生了以后,系统需要把这个事件传递给一个具体的View,而这个传递的过程就是分发过程。点击事件的分发过程由三个方法来共同完成

1.public boolean dispatchTouchEvent(MotionEvent ev)

用来进行事件的分发。如果事件能够传递给当前的View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dsipatchTouchEvent影响,表示是否消耗当前事件

2.public boolean onInterceptTouchEvent(MotionEvent event)

在上述方法的内部调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个时间序列中,此方法不会被再次调用,返回结果表示是否拦截当前事件

3.public boolaean onTouchEvent(MotionEvent event)

在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个时间序列中,当前View无法再次接收到事件。

它们的关系

可以用以下伪代码来表示

	boolean consume=false;
	if(onInterceptTouchEvent(ev)){
	consume=onTouchEvent(ev)
}else{
	consume=child.dispatchTouchEvent(ev)
}
	return consume;
}
复制代码

对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,此时它的的dispatchTouchEvent就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前事件,接着时间就会交给这个ViewGroup处理,即它的onTouchEvent会被调用,如果ViewGroup的onInterceptTouchEvent方法返回flase表示它不拦截当前事件,此时当前时间就会继续传递给它的子元素,接着子元素的dispatchEvent方法就会被调用,如此反复直到事件被处理。

当一个View需要处理事件时,如果它设置了OnTouchListener,那么OnTouchListener中的onTouch方法会被回调。这时时间如何处理还要看onTouch的返回值,如果返回false,则当前View的onTouchEvent方法会被调用,如果返回true,那么onTouchEvent方法不会被调用。由此可见,给View设置的OnTouchListener,其优先级比onTouchEvent方法高。在onTouchEvent方法中,如果当前设置的有OnClickListener,那么它的onClick方法会被调用。可以看出,平时我们常用的OnClickListener优先级最低,处于事件传递的尾端。

当一个点击事件产生后,它的传递过程遵循如下顺序:Activity->Window->View,即事件总是先传递给Activity,Activity再传递给Window,最后Window再传递给顶级View,顶级View接收到事件后,就会按照事件分发机制去分发事件。考虑一种情况,如果一个View的onTouchEvent返回false,那么它的父容器的onTouchEvent将会被调用,以此类推,如果所有的元素都不处理这个事件,那么这个事件最终会传递给Activity处理,即Activity的onTouchEvent方法会被调用。约等于上级抛任务给下级,下级搞不定,只能交给水平更高的上级,再不行就交给上级的上级去解决。

事件传递机制的结论

1.同一时间序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以down事件开始,中间含有数量不定的move事件,最终以up事件结束。

2.正常情况下,一个时间序列只能被一个View拦截且消耗,这一条的原因可以参考第三点,因为一旦一个元素拦截了此事件,那么同一个事件序列内的所有事件都会直接交给它处理,因此同一个事件序列中的事件不能分别由两个View同时处理,但是通过特殊手段可以做到,比如一个View将本该自己处理的事件通过onTouchEvent强行传递给其他View处理。

3.某个View一旦决定拦截,那么这一个事件序列都只能由它来处理,并且它的onInterceptTouchEvent不会再被调用。也就是说当一个View决定拦截一个事件后,系统会把同一个事件序列内的其他方法都直接交给它来处理,因此就不用再调用onInterceptTouchEvent去询问它是否要拦截了。

4.某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回false),那么同一事件序列的其他事件都不会交给它来处理,并且事件将重新交由它的父元素去处理,即父元素的onTouchEvent会被调用,意思是事件一旦交给一个View处理,那么它就必须消耗掉,否则同一事件序列中剩下的事件就不再交给它来处理了。

5.如果View不消除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理。

6.ViewGroup默认不拦截任何事件,其源码中的onInterceptTouchEvent方法默认返回false。

7.View没有onInterceptTouchEvent方法,一旦由点击事件传递给它,它的onTouchEvent就会被调用。

8.View的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时为false)。View的longClickable属性默认为false,clickable属性要分情况,比如Button的clickable属性默认为true,而Textview默认为false。

9.View的enable属性不影响onTouchEvent的默认返回值。哪怕一个View是disable状态,只要它的clickable或者longClickable有一个为true,它的onTouchEvent就返回true。

10.onClick会发生的前提是当前View是可点击的,并且它收到了down和up事件。

11.事件传递过程是由外向内的,事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素干预父元素的事件分发过程,但是ACTION_DOWN除外。

事件分发的源码分析

1.Activity对点击事件的分发过程

点击事件用MotionEvent来表示,当一个点击操作发生时,事件最先传递给当前Activity,由Activity的dispatchTouchEvent来进行事件分发,具体工作由Activity内部的Window完成。Window会将事件传递给decorview,decorview一般是当前界面的底层容器(即setContentView所设置的View的父容器),通过Activity.getWindow.getDecorView()可以获得。
image.png
首先事件开始交给Activity所附属的Window进行分发,如果返回true,整个事件循环就结束了,返回false意味着事件没人处理,所有View的onTouchEvent都返回了false,,那么Activity的onTouchEvent就会被调用。

接下来看Window是如何将事件传递给ViewGroup的,通过源码可得出Window是个抽象类,而Window的superDispatchTouchEvent方法也是个抽象方法。而Window的实现类其实是PhoneWindow。Window类可以控制顶级View的外观和行为策略,它的唯一实现位于android.policy.PhoneWindow中,当你要实例化这个Window类的时候,你并不知道它的细节,因为这个类会被重构,只有一个工厂方法可以使用。 image.png
从上图可以看到PhoneWindow将事件直接传递给了DecorView
image.png
通过((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0)这种方式可以获取Activity所设置的View,这个mDecor显然就是getWindow().getDecorView()返回的View,而我们通过setContentView设置的View是它的一个子View。目前事件传递到了DecorView这里,由于DecorView继承自FrameLayout且是父View,所以最终时间会传递给View。换句话说,时间肯定会传递到View,不然应用如何相应点击事件呢?重点是事件到了View之后应该如何传递。从这里开始事件已经传递到顶级View了,即在Activity中通过setContentView所设置的View,顶级View也叫根View,一般来说都是ViewGroup。

顶级View对点击事件的分发过程

概述:点击事件到达顶级View,会调用ViewGroup的dispatchTouchEvent方法,ruoguodingjiViewGroup拦截事件即onInterceptTouchEvent返回true,则事件由ViewGroup处理,如果此时ViewGroup的mOnTouchListener被设置,则onTouch会调用,否则onTouchEvent被调用。也就是说都提供的话onTouch会屏蔽onTouchEvent。在onTouchEvent中,如果设置了mOnClickListener,则onClick会被调用。如果顶级ViewGroup不拦截事件,事件会传递到它所在的点击事件链上的子View,此时子View的dispatchTouchEvent会被调用。到此位置,事件已经从顶级View传递给了下一层View,如此循环,完成整个事件的分发。
ViewGroup对点击事件的分发过程主要实现在ViewGroup的dispatchTouchEvent方法中。方法较长,截取部分分析 见下图 image.png
当事件类型为ACTION_DOWN或mFirstTouchTarget!=null时ViewGroup会判断是否要拦截当前事件。当ViewGroup的子元素成功处理时,mFirstTouchTarget会被赋值并指向子元素。换种方式说,当ViewGroup不拦截事件并将事件交由子元素处理时mFirstTouchTarget!=null。反过来,一旦事件由当前ViewGroup拦截时,mFirstTouchTarget!=null不成立,那么当DOWN事件到来时ViewGroup不会再被调用,并且同一序列的其他事件默认交由它处理。

特殊情况

FLAG_DISALLOW_INTERCEPT标记位,通过requestDisallowInterceptTouchEvent方法来设置,一般用于子View中。一旦设置后,ViewGroup将无法拦截除了DOWN以外的事件。(因为DOWN会重置该标记位,导致View中设置的这个标记位无效)所以当面对ACTION_DOWN事件时,ViewGroup总是会调用自己的onInterceptTouchEvent方法来询问自己是否要拦截事件。 image.png 上图遇到DOWN事件时进行了重置。

结论

当ViewGroup决定拦截事件后,那么后续的点击事件将会默认交给它处理并且不再调用它的onInterceptTouchEvent方法,这证实了FLAG_DISALLOW_INTERCEPT的作用是让ViewGroup不再拦截事件,前提是ViewGroup不拦截DOWN事件。onInterceptTouchEvent不是每次时间都会被调用的,如果我们想提前处理所有的点击事件,要选择dispatchTouchEvent方法,只有这个方法能确保每次都会被调用。前提是事件能够传递到当前的ViewGroup。 image.png
上图中当ViewGroup不拦截事件时,首先遍历ViewGroup的所有子元素,然后判断子元素是否能够接收到点击事件。判断是否能接受点击事件主要有两点来衡量1.子元素是否在播放动画和点击事件的左边是否落在子元素的区域内。如果某个子元素满足这两个条件,事件就会传递给它来处理。dispatchTransformedTouchEvent实际上调用的就是子元素的dispatchTouchEvent方法。其参数的child在不是null的情况下会调用child.dispatchTouchEvent方法,这样事件就交由子元素处理了,从而完成了一轮事件分发。

image.png
当子元素的dispatchTouchEvent返回true,mFirstTarget就会被赋值然后跳出循环

image.png
这几行代码完成了mFirstTouchTarget的赋值并终止对子元素的遍历。如果子元素的dispatchTouchEvent返回false,ViewGroup就会把事件分发给下一个子元素(如果有下一个子元素) 其实mFirstTarget真正的复制过程是在addTouchTarget内部完成,从它的内部结构可以看出,它是一种单链表结构。mFirstTarget是否被赋值直接影响到ViewGroup的拦截策略,如果它为null,那么ViewGroup就默认拦截接下来同一序列的所有点击事件 image.png
如果遍历所有子元素后事件都没有被合适地处理原因
1.ViewGroup没有子元素
2.子元素处理了点击事件,但是在dispatchTouchEvent中返回了false,这一般是因为子元素在onTouchEvent中返回了false。
在这两种情况下ViewGroup会自己处理点击事件。

image.png

上图child为null之后,会转到View的dispatchTouchEvent方法,即点击事件交由View来处理

View对点击事件的处理过程(这里的View不包含ViewGroup)

这里的View是一个单独的元素,没有子元素,因此无法向下传递事件,只能自己处理事件。

image.png

上图,首先它会判断有没有设置OnTouchListener,如果onTouchListener中的onTouch方法返回true,那么onTouchEvent就不会被调用,可见onTouchListener的优先级高于onTouchEvent,这样做的好处是方便在外界处理点击事件。 接着分析onTouchEvent的实现

image.png 上图当View处于不可用状态下时View照样会消耗点击事件,尽管它看起来不可用。

image.png
下图,接着如果View设置有代理,那么还会执行TouchDelegate的onTouchEvent方法,其工作机制与OnTouchListener类似。

image.png

image.png
上图当View的CLICKABLE和LONG_CLICKABLE有一个为true时,那么它就会消耗这个事件,返回true,不管它是不是DISABLE状态。当UP事件发生时,会触发performClick方法,如果View设置了OnClickListener,那么performClick方法内部会调用它的onClick方法。看下图

image.png

View的LONG_CLICKABLE属性默认为false,而CLICKABLE属性是否为false和具体View有关,通过setClickable和setLongClickable可以改变它们的属性。setOnClickListener会自动将View的CLICKABLE设为true,setOnLongClickListener会自动将View的LONG_CLICKABLE设为true。看下图 image.png

Supongo que te gusta

Origin juejin.im/post/7073887276924665863
Recomendado
Clasificación