想知道手指触摸屏幕的时候发生了什么吗?
1 前言
滑动对于android来说,是一个必不可少;它不复杂,大家都知道在onTouchEvent中,让它滑动就完事了,说它复杂,其嵌套处理复杂;在本系列文章,最终是为了熟悉嵌套滑动机制;对于滑动,分为下面几篇文章来完成解读:
- 滑动基础
- ScrollView滑动源码解读
- NestedScrollView嵌套滑动源码解读
- CoordinatorLayout-AppBarLayout-CollapsingToolbarLayout复杂滑动逻辑源码解读
在本章内,主要介绍实现的一些相关基础框架逻辑
- 平滑处理、滑翔处理
- View中对滑动的处理效果以及逻辑
- androidx中的滑动接口、嵌套滑动接口的理解
看到这里,你不再觉得仅仅是在OnTouchEvent中处理滑动事件吧,其实这样想也可以,不过效果什么的全自定义了
2 滑动常量
介绍滑动前,我们需要了解一些滑动常量,这些常量有利于我们实现更流畅的滑动效果
这些常量都是通过ViewConfiguration来获取的,其实例通过下面来获取
ViewConfiguration configuration = ViewConfiguration.get(mContext)
复制代码
- 最小滑动距离:getScaledTouchSlop()
- 最小滑翔速度:getScaledMinimumFlingVelocity(),像素每秒
- 最大滑翔速度:getScaledMaximumFlingVelocity(),像素每秒
- 手指滑动越界最大距离:getScaledOverscrollDistance()
- 滑翔越界最大距离:getScaledOverflingDistance()
这里滑翔的速度,是为了处理惯性的快慢,这个做过的深有体会,总是感觉,快慢不是很舒服;所以我们一一般在滑翔时,获取滑翔距离时,要在最大和最小之间;
3 平滑滑动、滑翔
平滑滑动根据时间进行平缓的滑动,而滑翔需要对移动事件进行跟踪分析之后,再根据时间计算状态进而进行分析;而根据时间进行状态处理,使用Scroller或者OverScroller来处理,OverScroller可以处理回弹效果;对事件跟踪分析,使用VelocityTracker类处理
3.1 VelocityTracker类
这个类有下面用法
实例获取
mVelocityTracker = VelocityTracker.obtain()
复制代码
跟踪事件
mVelocityTracker.addMovement(ev)
复制代码
获取滑翔初始速度
mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int xVelocity = (int)mVelocityTracker.getXVelocity(mActivePointerId);
int yVelocity = (int)mVelocityTracker.getYVelocity(mActivePointerId);
复制代码
两个方法要跟着使用,减少误差;另外计算时;computeCurrentVelocity参数意义
- 多少毫秒,假设n
- 速度,单位由参数1的数值来确定,也即是像素每n毫秒
数据临时清理
mVelocityTracker.clear();
复制代码
当切换手指时,之前的数据就没有意义了,所以需要清理重新计算
对象回收
mVelocityTracker.recycle();
复制代码
3.2 OverScroller类
Scroller也可以处理,只是不能处理回弹而已;这里就只是解释OverScroller类,它仅仅只是一个状态计算的类,对view并没有进行操作;下面就是一些使用
初始化
mScroller = new OverScroller(getContext());
复制代码
滑动
public void startScroll(int x, int y, int dx, int dy, int duration)
复制代码
单位时间内,x增加dx,y增加dy;默认时间250ms
计算
public boolean computeScrollOffset()
复制代码
计算当前时间对应状态,返回true表示,仍在进行,可通过下面获取当前状态
- getCurrX():当前x位置
- getCurrY(): 当前y位置
- getCurrVelocity():当前速度
回弹
public boolean springBack(int startX, int startY, int minX, int maxX, int minY, int maxY)
复制代码
- 当前x值
- 当前y值
- x最小值
- y最小值
- x最大值
- y最大值
如果运用在滑动中,则表示已滑动距离,滑动的最小距离,滑动的最大距离;
滑翔
fling(int startX, int startY, int velocityX, int velocityY,int minX, int maxX, int minY, int maxY, int overX, int overY)
复制代码
- 当前x位置
- 当前y位置
- 当前x速度,像素每秒
- 当前y速度,像素每秒
- x最小取值
- y最小取值
- x最大取值
- y最大取值
- x最大越界距离
- y最大越界距离
有越界范围才有回弹效果
丢弃
mScroller.abortAnimation();
复制代码
完成判断
mScroller.isFinished()
复制代码
3.3 平滑移动
这个只需要调用OverScroller的startScroll方法进行触发,在View的computeScroll方法获取滑动状态调用scrollTo方法即可;
3.4 滑翔
滑翔就分为两种情况了
- 在手指离开时,未越界,则进行滑翔,如果可以回弹,也会进行回弹,调用OverScroller的fling方法
- 在手指离开时,已经越界,则进行回弹,调用OverScroller的springBack方法
同样需要在computeScroll根据计算状态,进行具体滑动
4 View类
View类中对于滑动,提供了滑动执行机制、滑动时指示条、滑动时fade蒙层、长按事件处理还有滑动的一些数据判断,这些和androidx中滑动接口ScrollingView
4.1 滑动具体执行
具体执行是通过View的变量mScrollX、mScrollY来完成的,这两个变量在绘制的时候,会对画布进行平移(详见View类中draw方法被调用的地方),进而导致其内绘制内容发生了变化;这个平移对当前view的背景并没有影响,由于在处理背景时再次进行了反方向平移(详见View类中drawBackground方法);而对这两个变量的操作方法有
- scrollTo(int x, int y):移动到x、y
- scrollBy(int x, int y):移动范围增加x、y
- overScrollBy方法,此方法会自动处理越界时的处理,并调用onOverScrolled进行实际的移动处理
我称这两个方法为执行者;但是很多滑动控件中都有平滑移动,平滑移动基本都是利用OverScroller或Scroller的滑动方法来完成的;需要回弹用OverScroller,否则使用Scroller即可
protected boolean overScrollBy(int deltaX, int deltaY,
int scrollX, int scrollY,
int scrollRangeX, int scrollRangeY,
int maxOverScrollX, int maxOverScrollY,
boolean isTouchEvent)
复制代码
overScrollBy方法,返回结果,true标识越界了需要回弹,参数意思如下:
- x增量值
- y增量值
- x当前移动值
- y当前移动值
- x当前最大值
- y当前最大值
- x最大回弹值
- y最大回弹值
- 是手指移动还是滑翔
protected void onOverScrolled(int scrollX, int scrollY,
boolean clampedX, boolean clampedY)
复制代码
onOverScrolled方法,参数意义如下:
- 当前x滑动
- 当前y滑动
- x是否越界,true表示越界了
- y是否越界,true表示越界
4.2 长按事件
源码见View类中onTouchEvent方法、isInScrollingContainer方法
长按事件有一定的规则:
- 是在down事件中进行触发发送延时回调长按处理任务,回调执行并不一定需要手指抬起
- 在cancel、move、up事件中取消的
而对于普通非滑动容器内的view,长按事件的延迟时间为ViewConfiguration.getLongPressTimeout();而如果是滑动容器中,此时会再次触发发送一个触发长按的延期任务,这个延时为ViewConfiguration.getTapTimeout();我觉得这是考虑到滑动的特殊性增加一点时间,可以更精准的判断是否为长按事件;
是否滑动容器的判断方法,是由ViewGroup的shouldDelayChildPressedState方法来处理的;也就是滑动容器中此方法需要返回true
4.3 fade蒙层
源码详见View.draw方法,绘制分为两种情况,是根据mViewFlags标志来判断的;也即是否需要绘制水平的fade蒙层或者竖直的蒙层;
这个标志可以进行设置,两种方法改变,默认是none
- xml中参数配置
android:requiresFadingEdge="horizontal|vertical|none"
复制代码
- 代码设置
setHorizontalFadingEdgeEnabled(true);
setVerticalFadingEdgeEnabled(true);
复制代码
并不是这个设置了,水平或者竖直,这些地方就以一定出现蒙层,还有其它限制,蒙层分为4个,这四个方法,逻辑是一致的,方法略有区别;
蒙层有一个高度设置,同样有两种方法改变,默认是ViewConfiguration.getScaledFadingEdgeLength()
- xml设置
android:fadingEdgeLength="16dp"
复制代码
- 通过方法设置
setFadingEdgeLength(int length)
复制代码
具体绘制的高度基本是这个高度,除非高度超过了控件本身高度,其变控件高度的一半
蒙层还有一个每个边缘的参数比例,这个在0-1之间;返回的值不在区间会被忽略掉;方法默认实现如下:
protected float getTopFadingEdgeStrength() {
return computeVerticalScrollOffset() > 0 ? 1.0f : 0.0f;
}
protected float getBottomFadingEdgeStrength() {
return computeVerticalScrollOffset() + computeVerticalScrollExtent() <
computeVerticalScrollRange() ? 1.0f : 0.0f;
}
protected float getLeftFadingEdgeStrength() {
return computeHorizontalScrollOffset() > 0 ? 1.0f : 0.0f;
}
protected float getRightFadingEdgeStrength() {
return computeHorizontalScrollOffset() + computeHorizontalScrollExtent() <
computeHorizontalScrollRange() ? 1.0f : 0.0f;
}
复制代码
那第二个条件就是:蒙层的比例 * 蒙层高度 > 1.0f 则这个位置边缘会绘制处理
蒙层是一个矩形的线性渐变蒙层,通过线性shade来处理的;渐变是从颜色的完全不透明到完全透明
shader = new LinearGradient(0, 0, 0, 1, color | 0xFF000000, color & 0x00FFFFFF, Shader.TileMode.CLAMP)
复制代码
这个颜色可以通过重写下面方法进行改变,默认是黑色
public int getSolidColor() {
return 0;
}
复制代码
其实这个蒙层在android所有标准控件中,只有时间的控件直接采用了,其它的保留了特性;而且从系统的默认实现来看,这个就是为滑动实现的
4.4 滚动条
由两部分组成,一个是Track(滑道),一个是Thumb(滑块);滑道可以认为是可以滑动整体,固定的,而滑块只是其中一部分,位置可变动;
有显示和隐藏控制,源码见awakenScrollBars()、onDrawScrollBars方法;
4.4.1 显示
显示受参数控制,即显示位置,且显示位置方向是可以滑动的才可以显示;有两种方式
- xml中设置
android:scrollbars="vertical|horizontal"
复制代码
- 代码设置
public void setHorizontalScrollBarEnabled(boolean horizontalScrollBarEnabled)
public void setVerticalScrollBarEnabled(boolean verticalScrollBarEnabled)
复制代码
4.4.2 隐藏
受参数控制,可以通过xml布局中配置,也可以设置;默认为true,如下
android:fadeScrollbars="true"
public void setScrollbarFadingEnabled(boolean fadeScrollbars)
复制代码
淡出效果,是在显示操作后提交的延迟操作,延时时长,默认为ViewConfiguration.getScrollDefaultDelay(),可以通过两种方式改变
- onDrawScrollBars方法调用不传递时间时,xml中配置可改变
android:scrollbarDefaultDelayBeforeFade="10"
复制代码
- 代码中onDrawScrollBars传递时间控制
淡出效果为alpha变换,时长默认为ViewConfiguration.getScrollBarFadeDuration();同样可以通过两种方法改变
- xml配置
android:scrollbarFadeDuration="1000"
复制代码
- 方法设置
public void setScrollBarFadeDuration(int scrollBarFadeDuration)
复制代码
4.4.3 样式控制
样式也有两种形式的控制
- 圆形屏幕设备:主要针对是android手表等设备,这个我看不了效果,就不说它的显示控制了
- 其它设备:绘制的是ScrollBarDrawable图片类型
ScrollBarDrawable是个不对开发者公开的类,那么这里我们只介绍下其属性
- android:scrollbarSize: 竖直时宽度,水平时高度
- scrollbarThumbHorizontal/scrollbarThumbVertical:滑块颜色
- scrollbarTrackVertical/scrollbarTrackHorizonta:滑道颜色
- scrollbarStyle:滑块样式,默认值insideOverlay,还有三个值insideInset,outsideOverlay,outsideInset;insideXXX不考虑padding,也就是会覆盖在padding上,而outside不考虑margin,会覆盖在margin上
4.5 指示条
源码见onDrawScrollIndicators方法
其是否可见,由3个方面控制
- 指示条显示位置不为none;两种方法设置,xml和代码
android:scrollIndicators="none"
public void setScrollIndicators(@ScrollIndicators int indicators[, @ScrollIndicators int mask])
复制代码
- 指示条显示位置相应方向可滑动;top:向上滑动,bottom-向下滑动,left向左滑动,right-向右滑动
左右滑动判断;参数为负,表示左,正为右
public boolean canScrollHorizontally(int direction) {
final int offset = computeHorizontalScrollOffset();
final int range = computeHorizontalScrollRange() - computeHorizontalScrollExtent();
if (range == 0) return false;
if (direction < 0) {
return offset > 0;
} else {
return offset < range - 1;
}
}
复制代码
上下滑动判断;参数为父表示上,为正表示下
public boolean canScrollVertically(int direction) {
final int offset = computeVerticalScrollOffset();
final int range = computeVerticalScrollRange() - computeVerticalScrollExtent();
if (range == 0) return false;
if (direction < 0) {
return offset > 0;
} else {
return offset < range - 1;
}
}
复制代码
指示条图标,为R.drawable.scroll_indicator_material,不可改变;这是我查找到的图片情况:
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:tint="?attr/colorForeground">
<solid android:color="#1f000000" />
<size
android:height="1dp"
android:width="1dp" />
</shape>
复制代码
指示条位置:以所在位置为其一边,垂直的两边,以及位置[-|+]为图片另外一边
唯一用途,指示你此时可以往哪个方向滑动;我觉得很不实用
4.6 回弹
默认是可以回弹,但是未进行回弹效果处理;对于回弹的开启关闭,可以通过两种方式
- xml中处理
android:overScrollMode="always"
复制代码
- 代码设置
public void setOverScrollMode(int overScrollMode)
复制代码
通过下面方法可以获取,为OVER_SCROLL_NEVER时不可回弹
public int getOverScrollMode()
复制代码
回弹长度,按照 2章节中 获取的相应常量设置即可
回弹效果,可以在手指移动、滑翔两个过程中出现;需要通过上述方法判断,进行进行处理;系统提供了默认的回弹效果类EdgeEffect;下面介绍下此类运用
EdgeEffect类
EdgeEffect mEdgeGlowTop = new EdgeEffect(getContext()); // 实例化
mEdgeGlowTop.setColor(color); // 改变回弹颜色
mEdgeGlowTop.onPull(deltaDistance, displacement) // 回弹参数,均为0-1,变化距离以及位置比例
mEdgeGlowTop.isFinished() // 状态判断
mEdgeGlowTop.onRelease() // 释放
mEdgeGlowTop.onAbsorb(velocity) // 放弃
mEdgeGlowTop.setSize(width, height) // 设置绘制的矩形范围,上面onPull传的参数比例,就是依据这个来绘制回弹的图形的
mEdgeGlowTop.draw(canvas) // 绘制,结果表示是否还需要继续处理
复制代码
需要特殊说明的是,这个类绘制的时候,默认绘制方向,以当前视图左上角为起点进行绘制的;所以要onPull的参数传递以及绘制时,要考虑坐标以及旋转的关系,进而达到正确的效果
4.7 嵌套滑动启动关闭配置
这个可以通过xml配置,或者代码设置
android:nestedScrollingEnabled="true"
public void setNestedScrollingEnabled(boolean enabled)
public boolean isNestedScrollingEnabled()
复制代码
4.8 测量
这里就是重写onMeasure方法,有两种情况
- 继承ViewGroup;需要完全自己重写逻辑
- 继承ViewGroup子类;可以依赖父类的测量逻辑,在其测量关键方法重写,也可以先进行父类测量
这两种情况都需要对子布局测量传递不限制模式MeasureSpec.UNSPECIFIED,以达到有滑动距离的可能
更具体的逻辑就需要自己来操作;不过在操作的时候,需要特殊注意一个对象,那就是ViewGroup.LayoutParams,也就是容器的布局参数,这个类是容器规定了一些功能,也是子view通过属性来通知父容器的一种重要途径
5 ScrollingView接口
如果你能理解上面的内容,那么这个接口方法就比较好理解了
- computeHorizontalScrollRange()/computeVerticalScrollRange():相应方向滑动范围,[0, 此方法结果]
- computeHorizontalScrollOffset()/computeVerticalScrollOffset():相应方向已滑动的距离
- computeHorizontalScrollExtent()/computeVerticalScrollExtent():滑道的长度,也即容器的宽度或者高度
这些方法,都是进行滑动判断、fade蒙版、指示条、滑动条用到的核心方法;如果不实现,就无法拥有View已实现的效果,并且相应方法肯定是不可用了,比如:
- 是否可滑动判断:canScrollHorizontally,canScrollVertically
- 滚动条隐藏:awakenScrollBars
6 嵌套接口
接口也分为子视图方法和父容器方法;子视图方法用来通知父容器进行处理的,而父容器方法是高速子滑动视图其是否去处理以及处理的结果状态;
6.1 NestedScrollingParent3接口
其继承NestedScrollingParent2,NestedScrollingParent2又继承了NestedScrollingParent;方法如下
- onStartNestedScroll方法:父容器是否需要处理子view的滑动事件,true表示接受处理
- onNestedScrollAccepted方法:接受子视图的滑动事件询问
- onStopNestedScroll方法:得知子视图停止滑动时的通知
- onNestedScroll方法:子view已经处理滑动后,父容器进行滑动处理
- onNestedPreScroll方法:子view处理滑动前,父容器进行滑动处理
- onNestedFling方法:子view需要滑翔时,子view处理,父view进行处理
- onNestedPreFling方法:子view需要滑翔时,父view进行处理;返回结果表示是否处理
- getNestedScrollAxes方法:当前父容器在子view滑动时,处理滑动的维度
需要注意的是,嵌套时,手指滑动是可接力完成的,而滑翔一定是互斥完成的
其中涉及一下参数,说明如下:
- type:表示滑动或者滑翔,ViewCompat.TYPE_TOUCH滑动,ViewCompat.TYPE_NONE_TOUCH滑动
- consumed:包含x、y两个方向的数组;一般为输出变量,表明当前处理时,消费了多少
- dxConsumed/dyConsumed:表明传递到父容器时,子视图已经消耗了多少滑动距离
- dxUnconsumed/dyUnconsumed:表明传递到父容器时,还有多少滑动距离待消耗
- target:表明从那个子view传递而来
- dx/dy:此次事件滑动的距离
- child:包含target的,当前容器的直接子容器
- axes:滑动的方向,ViewCompat.SCROLL_AXIS_HORIZONTAL,ViewCompat.SCROLL_AXIS_VERTICAL两个值
- velocityX/velocityY: 滑翔时初始速度
6.2 NestedScrollingChild3
继承了NestedScrollingChild2, NestedScrollingChild2又继承了NestedScrollingChild;方法如下:
- setNestedScrollingEnabled/isNestedScrollingEnabled: 嵌套滑动是否支持
- startNestedScroll:通知嵌套滑动开始
- stopNestedScroll:通知嵌套滑动结束
- hasNestedScrollingParent:是否存在嵌套处理的直系长辈容器
- dispatchNestedScroll:自己处理后继续通知滑动事件
- dispatchNestedPreScroll:自己未处理滑动,通知滑动事件
- dispatchNestedFling:自己处理后,通知滑翔事件
- dispatchNestedPreFling:优先自己处理,通知滑翔事件
参数就不解释了,和6.1类似
6.3 辅助类
这两章中的方法在View和ViewGroup均有使用,androidx也提供了辅助类进行默认实现,这两个类就是NestedScrollingParentHelper、NestedScrollingChildHelper;这两个类主要是为了解决版本兼容问题
作者:众少成多积小致巨
链接:https://juejin.cn/post/6960876681892462623
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。