1 前言
滑动对于android来说,是一个必不可少;它不复杂,大家都知道在onTouchEvent中,让它滑动就完事了,说它复杂,其嵌套处理复杂;在本系列文章,最终是为了熟悉嵌套滑动机制;对于滑动,分为下面几篇文章来完成解读:
- 滑动基础
- ScrollView滑动源码解读
- NestedScrollView嵌套滑动源码解读
- CoordinatorLayout-AppBarLayout-CollapsingToolbarLayout复杂滑动逻辑源码解读
在本章内,主要介绍实现的一些相关基础框架逻辑
- 布局机制以及处理
- 事件机制
- ScrollView中非嵌套滑动处理(其中虽然也存在嵌套机制的,但是由于兼容性问题,我们使用NestedScrollView,下一章节介绍)
上一章基础中未对布局机制、以及事件分发机制进行介绍,这里会进行详细介绍下,因为只有把这些了然于胸,才可以对滑动处理问题顺利精准的追根溯源
2、布局机制
视图布局包括三个过程
- 测量:measure方法,供父容器调用进行测量过程,内部通过onMeasure方法进行实际测量
- 放置:layout方法,供父容器调用,在父容器进行摆放,内部通过onLayout进行后代视图摆放
- 绘制:draw方法,供父容器调用,进行背景色、自己本身绘制、子view绘制等过程,onDraw为本身绘制
2.1 measure过程
这里主要讲解onMeasure方法;此方法就是测量自身宽高的一个过程,这时分为两种情况
- 本身不是容器,那么就是自身宽高,这时仅仅依靠自身控件限制来设置宽高即可
- 是容器,则需要考虑其孩子视图的测量,依据孩子的宽高测量情况,进而对自身宽高设置
采用方法setMeasuredDimension进行宽度和高度设置,但这个过程,又需要其它的一些依赖,通过下面三个条件最终确定结果
- 分发时,传递的长度以及模式限制
- 孩子视图的布局属性
- 视图的逻辑考量:比如容器要考虑孩子容器放置方式、子view要不要对宽度和高度做某些限制处理
上面说的也很直白,作者也不想贴过多的源码,为了讲明白,我们需要讲述下面一些内容
- 一些成员变量
- MeasureSpec类使用
- LayoutParams类
- 一些常见使用方法,有利于我们进行测量
2.1.1 成员变量
- 测量宽度/高度:测量过程时,设置的宽度和高度值;相关方法
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight)//设置
public final int getMeasuredWidth() // 获取测量宽度
public final int getMeasuredHeight() //获取测量高度
- 宽度/高度:放置过程时产生结果,根据相对于父布局中的上下左右的长度来决定的;获取方法如下
public final int getWidth()
public final int getHeight()
一般来说,测量宽度等于宽度,测量高度等于高度
2.1.2 MeasureSpec类
这个类是对一个整数进行合成、拆分处理的辅助类;高两位表示模式,低30位表示长度;有三种模式
- UNSPECIFIED:不限制模式,就是父容器对当前视图测量结果,都接收;
- EXACTLY:精确模式,也就是开发者自己定义了长度
- AT_MOST:最大模式,父容器能够给子类提供的最大长度
模式获取
MeasureSpec.getMode(widthMeasureSpec)
长度获取
MeasureSpec.getSize(widthMeasureSpec)
合成
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY)
2.1.3 LayoutParams类
View类中提供了此类,这个类广义来说,是父容器对直接孩子视图的一些限制或者交互手段;但系统中实现的此类,都是限制测量的相关信息;因此,这个类是自定义容器时才需要考虑的;对孩子视图的限制,有两种途径
- 通过addView添加;下面容器方法,对应参数提取
protected LayoutParams generateDefaultLayoutParams()
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p)
- 通过xml解析;下面容器方法进行参数提取
public LayoutParams generateLayoutParams(AttributeSet attrs)
LayouParams的子类一般来说应实现三种构造器,这三种构造器对应上述三个解析方法
public LayoutParams(Context c, AttributeSet attrs)
public LayoutParams(int width, int height)
public LayoutParams(LayoutParams source)
这里的宽度、高度,是下面的值
- MATCH_PARENT、FILL_PARENT:负值,表示和父容器一样的宽度或者高度
- WRAP_CONTENT:负值,表示能够容纳自身测量的宽高即可
- 正值:表示开发者对其设置的精确值
LayouParams一个有一个通用子类MarginLayoutParams,其增加四个方向的margin属性;LayoutParams中的宽高和MeasureSpec中的模式、长度,才是测量中不变部分的逻辑的核心体现;
2.1.4 标准测量规则
子视图标准规则如下方法内容
/**
* 标注规则如下:返回模式和长度合成值,写成(长度,模式)
* 1. childDimension为正数,则返回(childDimension, EXACTLY)
* 2. spec中模式为UNSPECIFIED,则返回(spec中长度,UNSPECIFIED)
* 3. spec中模式为AT_MOST,childDimension为WRAP_CONTENT,则返回(spec长度,AT_MOST)
* 4. spec中模式为EXACTLY,childDimension为WRAP_CONTENT,则返回(spec长度,AT_MOST)
* 5. spec中模式为EXACTLY,childDimension为MATCH_PARENT,则返回(spec长度,EXACTLY)
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension)
- spec: MeasureSpec合成的整数值;特别注意,若模式为UNSPECIFIED,长度在23版本以下,其返回0,不是有效的长度值
- padding:包括margin在内的,已经被占用的长度;注意margin在父容器中被处理
- childDimension:LayoutParams中宽高的取值
自身视图标准测量见下面方法
/**
* 有下面情况
* 1. measureSpace模式为UNSPECIFIED,返回所size
* 2. measureSpace模式为EXACTLY,返回所measureSpace中的长度
* 3. measureSpace模式为AT_MOST,返回min(size,measureSpace中的长度)
*/
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState)
- size:自身规则得到的最小长度
- measureSpec:自身测量时的限制,长度宽度合成值
- childMeasuredState:测量状态,小于所需最小长度为MEASURED_STATE_TOO_SMALL,否则为MEASURED_STATE_MASK
2.2 layout过程
public void layout(int l, int t, int r, int b)
这个方法表示了放置过程,这里面会把参数记录下来;参数意义为:
- l: view左边界距离父布局左边界的距离;对应成员变量mLeft
- t:view上边界距离父布局上边界的距离;对应成员变量mTop
- r: view右边界距离父布局左边界的距离;对应成员变量mRight;
- b: view下边界距离父布局上边界的距离;对应成员变量mBottom
这个四个参数,在绘制的时候,通过这些参数正确找到相对屏幕的位置
如果测量视图不是容器,则在onLayout做任何处理,这时因为参数记录下来,已经能保证当前view绘制到正确位置;而如果是容器,则需要告诉子视图相对自身的摆放位置
2.3 draw过程
绘制过程:
- 绘制背景色:私有方法,不可重写
private void drawBackground(Canvas canvas)
- 本身绘制:一般来说容器此方法不需要做任何处理
protected void onDraw(Canvas canvas)
- 子视图绘制:通知子视图进行绘制
protected void dispatchDraw(Canvas canvas)
- 前景色绘制:绘制的最上层图层;系统提供的指示条、滚动条,就是在这里处理的,所以我觉得边缘特效在这里处理比较合适
public void onDrawForeground(Canvas canvas)
2.4 小结
布局过程中,在可滑动方向的测量应该是不限制模式的,这样才能存在可滑动距离;
布局中,测量由于本身处理涉及方面较多,而且还依赖于具体的需求,所以比较复杂;而放置过程则比较简单,利用了测量的结果进而放置;绘制过程则仅仅依赖用户实现的效果,其复杂度决定于需求
3、事件分发机制
这里讲的事件分发,是touch事件分发;之前也看过很多关于这个讲解,对他们只讲了大致流程,这估计都是所谓的写作借鉴吧,作者应该是理解了的;虽然是那三个方法,起到了作用,但是我觉得事件还是分2个流程来说,而不是来那三个方法的流程图,那些流程图看了把人都带偏了
- View事件处理
- ViewGroup对View事件分发
下面就从三种流程出发介绍;不过还是要稍微介绍一些基础
3.1 事件基础知识
触摸事件是一系列事件,事件是以ACTION_DOWN开始,以ACTION_UP或者ACTION_CANCEL事件结束;情况基本如下
- ACTION_DOWN : 第一个手指按下事件
- ACTION_UP: 最后一个手指抬起事件
- ACTION_CANCEL:取消事件,也即是不能得到后续事件
- ACTION_MOVE : 移动事件
- ACTION_POINTER_DOWN:多个手指时按下事件
- ACTION_POINTER_UP:多个手指时,抬起事件
ACTION_DOWN ----> 其它事件 -----> ACTION_UP/ACTION_CANCEL
相关的三个方法,也是我们考虑重写的方法
- 分发源头方法
dispatchTouchEvent(MotionEvent ev) //事件处理开始方法
- 父容器拦截事件,不向后代分发方法;方法是否执行,依赖当前容器的状态,允许拦截,默认允许
onInterceptTouchEvent(MotionEvent ev) //在 dispatchTouchEvent方法内调度,拦截事件,ViewGroup内方法,默认不拦截
- 视图自身处理方法
onTouchEvent(MotionEvent event) // 在 dispatchTouchEvent方法内调度;其中默认包括了点击和长按事件的处理
有存在允许父容器拦截分发的方法,那么就存在后代视图告诉父容器,请不要拦截,方法如下:
requestDisallowInterceptTouchEvent(boolean disallowIntercept) // 参数true表示不允许拦截
多指滑动处理的方法也有很多种,一般要分情况来说;在嵌套滑动时采用了接力的形式,我们在嵌套滑动章节中,再说这个的一些处理方法;
3.2 View事件处理
View中dispatchTouchEvent相关代码这里就不贴了;从中解读几个关键点,这几个关键点也在执行上是顺序关系
- 对于事件处理,如果处于enable状态,滑动条会处理滑动(上一篇文章提到的滑动条),这个记住处理结果
- 处于enable状态且触摸监听者存在,则调用处理,并且记住处理结果
- 如果之前滑动处理结果为false,进行触摸回掉处理,并记住处理结果
这三个流程执行后,处理结果,即为View事件处理结果;需要提醒下,有默认状态不可点击,所以,默认不处理事件
触摸回调
这个也就是:
public boolean onTouchEvent(MotionEvent event) // true表示当前view处理此事件
同样,按照执行顺序,说出对自定义滑动影响的关键点
- disable状态时,返回点击状态
- 存在触摸代理者,则返回触摸代理处理的结果
- 若处于click状态或者开启了默认的长按事件,则一定返回true(也是这段代码中处理的点击时间监听、长按事件监听)
- 默认返回false
View的处理流程还是很简单的,只是检查自身是不是存在处理
3.3 ViewGroup对View事件分发
ViewGroup中对dispatchTouchEvent进行了重写;我个人观点啊,它考虑的是,谁来处理;也就是
- 后代视图是否处理
- 自己要不要处理
后代视图处理情况
- 后代不允许拦截时,可以找到事件处理的后代
- 后代允许拦截时,当前容器不拦截,可以找到事件处理的view
当后代不处理事件时,容器就是发起super.dispatchTouchEvent进行自身处理,也就是当成一个View对事件进行处理
关于onInterceptTouchEvent方法,大家可能会存在一定的误解,这里还是说明一下,不然在阅读大家自己阅读嵌套滑动时,可能会忽略一些或者对一些处理进行迷糊
- 拦截并不只是onInterceptTouchEvent方法的结果,如果没有找到可处理的后代一样会拦截
- onInterceptTouchEvent调用的情况,其满足下面条件
- 当前为允许拦截模式
- down事件或者找到了可处理事件的的后代
- 拦截之前,有可能会根据onInterceptTouchEvent来做结果判断;当前不拦截,则会寻找可处理事件的后代,其根据为时间点在视图之内
- 当前事件,若是拦截,则分发cancel给可处理后代,并且重置当前处理后代对象为null;这个事件过后,则不回执行onInterceptTouchEvent方法,而直接执行自身的onTouchEvent方法作为结果
4、ScrollView中非嵌套滑动机制
从上一篇滑动基础中,我们可以知道,我们准守一些规则
- 继承View架构的一些功能,
- 实现滑动逻辑:这里又可以分为布局过程、事件拦截处理、滑动处理
4.1 重写方法
ScrollView继承了FrameLayout,没有实现任何接口;而在对滑动需要处理的6个方法中:其中四个默认实现没有基本可以使用;一般情况下不需要重写的方法:
public int computeVerticalScrollOffset() // 已经滑动距离
public int computeVerticalScrollExtent() // 可见长度
public int computeHorizontalScrollOffset() // 已经滑动距离
public int computeHorizontalScrollExtent() // 可见长度
而需要重写的方法,是需要显示内容的总长度
public int computeHorizontalScrollRange()
public int computeVerticalScrollRange()
这个理应有如下逻辑
0 <= offset <= range - extent
默认实现的extent为宽度或者高度,也就是没有考虑padding的实际可见区域;而如果可以越过边界一小段距离时,range范围要加上越过边界的距离
当前控件,重写了竖直方向的逻辑,也是此方向可以滑动;但是有些地方需要注意
- 未考虑子视图的margin值
重写方法后,有下面两个方法大大减少开发
- overScrollBy:自动计算当前有效滑动值,并得到过界状态,以onOverScrolled方法进行回掉
- canScrollVertically/canScrollHorizontally:是否可以水平或者竖直继续滑动的判断
4.2 布局过程
测量过程
利用FrameLayout测量逻辑,并在其关键逻辑方法进行改动;改动的为测量孩子容器方法
- measureChild:
- measureChildWithMargins 由于当前容器为上下滑动,所以宽度的要求,保持标准逻辑,而高度的测量模式修改为了MeasureSpec.UNSPECIFIED
绘制过程 重写了draw方法,只是在原有View绘制的基础上,在画布最上层增加了边缘特效
4.3 事件拦截
事件拦截需要进行如下处理:
- onInterceptTouchEvent在down事件中不拦截
- 检测到滑动后,需要在onInterceptTouchEvent或者onTouchEvent进行拦截,并且不允许父布局再进行拦截; 为何要在两个方法内都进行拦截处理呢,因为onInterceptTouchEvent可能只在down事件中调用一次;调用情形如下:
- down事件时调用一次,返回true,直接拦截,这时onTouchEvent直接处理down事件以及若处理结果为true时的后续事件
- down事件时调用一次,未找到处理事件的孩子视图,这时onTouchEvent也直接处理down事件以及若处理结果为true时的后续事件
- move事件调用0次或者多次后,进行了拦截,这此事件分发子视图cancel事件;下次事件则由onTouchEvent处理,返回true时后续事件其继续处理,否则结束
是否滑动判断:滑动距离大于最小滑动长度
4.4 多手指处理
在多手指的event中,里面的位置包含多个手指事件信息;多手指时一些处理必须使用下面方法
- 事件类型通过getActionMasked方法获取
- 事件附带坐标,必须指定手指索引:getY(index)
每个手指有两个身份变量,索引以及id;这是由于历史版本处理问题而进行兼容而产生的;
- index和id可互相查找:findPointerIndex(id) = index, id = getPointerId(index)
- index是从0开始的,0移除后,下次的事件手指索引依然从0开始
- 在down或者point-down事件中通过getActionIndex方法获取索引
4.5 移动处理
移动时,可以分为手指在屏幕上时的移动以及离开屏幕后的移动
注意:OverScroller只是个计算工具,要想把数据应用需要进行刷新,并在computeScroll方法中处理
4.5.1 computeScroll方法
平滑移动、以及滑翔都会用到次方法进行滑动更新
- mScroller.computeScrollOffset()进行计算,并返回是否计算更新
- 通过计算的x、y以及当前滑动变量对比,变化采用overScrollBy进行实质滑动
4.5.2 跟手移动
这个过程在move事件中处理;overScrollBy方法和onOverScrolled方法一起处理移动;
onOverScrolled方法,实际有下面两种方式进行了移动
- 通过scrollTo方法进行移动
- 通过直接修改scroll变量,并且刷新画布进而移动
但是我们能用的也就是scrollTo方法;这就google家和它家的区别;
其中如果是过界的滑动,且当前OverScroller计算还没有结束,则进行回弹处理
4.5.3 滑翔
在up事件中处理;但也分两种情况
- 滑动的速度大于最小值进行滑翔
- 若不能滑翔,此时越界,则进行回弹动效
滑翔的逻辑在flingWithNestedDispatch方法中,通过判断是否可滑翔,进而调用fling方法;判断逻辑
- 滑动在0-滑动范围之间,不包括0与最大值
- 当前滑动值大于0,速度小于0
- 当前滑动值小于最大滑动距离,速度大于0
5、小结
这篇文章是从自我的角度来解读了,没有贴出源码,也没有每句源码的意义,是希望读者在读相关章节时,自己对照源码,找到的自己的理解,以免由于每个人不同抽象理解而造成问题。
如果在此文章中您有所收获,请给作者一个鼓励,点个赞,谢谢支持
技术变化都很快,但基础技术、理论知识永远都是那些;作者希望在余后的生活中,对常用技术点进行基础知识分享;如果你觉得文章写的不错,请给与关注和点赞;如果文章存在错误,也请多多指教!
上一篇:滑动基础