让控件如此丝滑Scroller和VelocityTracker的API讲解与实战——Android高级UI
目录
一、前言
二、Scroller
三、VelocityTracker
四、实战——带惯性滑动的柱状图
五、写在最后
一、前言
自定义控件中,难免会遇到需要滑动的场景。而Canvas提供的scrollTo和scrollBy方法只能达到移动的效果,需要达到真正的滑动便需要我们今天分享的两把基础利器Scroller和VelocityTracker。老规矩,先上实战图,再进行分享。
带惯性滑动的柱状图
二、Scroller
1、作用
童鞋们可以先看下下面这段官方的英文类注释。小盆友以自己的理解给出这个类的作用是,Scroller 是一个让视图 滚动起来的工具类,负责根据我们提供的数据计算出相应的坐标,但是具体的滚动逻辑还是由我们程序猿来进行 移动内容 实现(?为啥说是移动内容,我们在实战一节中便知道了,稍安勿躁)。
* <p>This class encapsulates scrolling. You can use scrollers ({@link Scroller}
* or {@link OverScroller}) to collect the data you need to produce a scrolling
* animation—for example, in response to a fling gesture. Scrollers track
* scroll offsets for you over time, but they don't automatically apply those
* positions to your view. It's your responsibility to get and apply new
* coordinates at a rate that will make the scrolling animation look smooth.</p>
2、API讲解
这一小节是对 Scroller 的 构造方法 和 常用的公有方法 进行讲解,如果您已经对这些方法很熟悉,可以跳过。
构造方法
(1)Scroller(Context context)
public Scroller(Context context)
方法描述:
创建一个 Scroller 实例。
参数解析:
第一个参数 context: 上下文;
(2) Scroller(Context context, Interpolator interpolator)
public Scroller(Context context, Interpolator interpolator)
方法描述:
创建一个 Scroller 实例。
参数解析:
第一个参数 context: 上下文;
第二个参数 interpolator: 插值器,用于在 computeScrollOffset 方法中,并且是在 SCROLL_MODE 模式下,根据时间的推移计算位置。为null时,使用默认 ViscousFluidInterpolator 插值器。
(3) Scroller(Context context, Interpolator interpolator, boolean flywheel)
public Scroller(Context context, Interpolator interpolator, boolean flywheel)
方法描述:
创建一个 Scroller 实例。
参数解析:
第一个参数 context: 上下文;
第二个参数 interpolator: 插值器,用于在 computeScrollOffset 方法中,并且是在 SCROLL_MODE 模式下,根据时间的推移计算位置。为null时,使用默认 ViscousFluidInterpolator 插值器。
第三个参数 flywheel: 支持渐进式行为,该参数只作用于 FLING_MODE 模式下。
常用公有方法
(1) setFriction(float friction)
public final void setFriction(float friction)
方法描述:
用于设置在 FLING_MODE 模式下的摩擦系数
参数解析:
第一个参数 friction: 摩擦系数
(2) isFinished()
public final boolean isFinished()
方法描述:
滚动是否已结束,用于判断 Scroller 在滚动过程的状态,我们可以做一些终止或继续运行的逻辑分支。
(3) forceFinished(boolean finished)
public final void forceFinished(boolean finished)
方法描述:
强制的让滚动状态置为我们所设置的参数值 finished 。
(4) getDuration()
public final int getDuration()
方法描述:
返回 Scroller 将持续的时间(以毫秒为单位)。
(5) getCurrX()
public final int getCurrX()
方法描述:
返回滚动中的当前X相对于原点的偏移量,即当前坐标的X坐标。
(6) getCurrY()
public final int getCurrY()
方法描述:
返回滚动中的当前Y相对于原点的偏移量,即当前坐标的Y坐标。
(7) getCurrVelocity()
public float getCurrVelocity()
方法描述:
获取当前速度。
(8) computeScrollOffset()
public boolean computeScrollOffset()
方法描述:
计算滚动中的新坐标,会配合着 getCurrX 和 getCurrY 方法使用,达到滚动效果。值得注意的是,如果返回true,说明动画还未完成。相反,返回false,说明动画已经完成或是被终止了。
(9) startScroll
public void startScroll(int startX, int startY, int dx, int dy) public void startScroll(int startX, int startY, int dx, int dy, int duration)
方法描述:
通过提供起点,行程距离和滚动持续时间,进行滚动的一种方式,即 SCROLL_MODE。该方法可以用于实现像ViewPager的滑动效果。
参数解析:
第一个参数 startX: 开始点的x坐标
第二个参数 startY: 开始点的y坐标
第三个参数 dx: 水平方向的偏移量,正数会将内容向左滚动。
第四个参数 dy: 垂直方向的偏移量,正数会将内容向上滚动。
第五个参数 duration: 滚动的时长
(10) fling
public void fling(int startX, int startY, int velocityX, int velocityY,int minX, int maxX, int minY, int maxY)
方法描述:
用于带速度的滑动,行进的距离将取决于投掷的初始速度。可以用于实现类似 RecycleView 的滑动效果。
参数解析:
第一个参数 startX: 开始滑动点的x坐标
第二个参数 startY: 开始滑动点的y坐标
第三个参数 velocityX: 水平方向的初始速度,单位为每秒多少像素(px/s)
第四个参数 velocityY: 垂直方向的初始速度,单位为每秒多少像素(px/s)
第五个参数 minX: x坐标最小的值,最后的结果不会低于这个值;
第六个参数 maxX: x坐标最大的值,最后的结果不会超过这个值;
第七个参数 minY: y坐标最小的值,最后的结果不会低于这个值;
第八个参数 maxY: y坐标最大的值,最后的结果不会超过这个值;
值得一说:
minX <= 终止值的x坐标 <= maxX
minY <= 终止值的y坐标 <= maxY
(11) abortAnimation()
public void abortAnimation()
方法描述:
停止动画,值得注意的是,此时如果调用 getCurrX() 和 getCurrY() 移动到的是最终的坐标,这一点和通过 forceFinished 直接将动画停止是不相同的。
3、小结
从上面的 API 讲解中,我们会发现,至始至终都没有对我们需要作用的View有任何的关联,而是通过计算,然后获取当前时间点对应的坐标,如此而已。这也就印证了前面的定义,至于怎么真正的使用,我们留到实战篇。
三、VelocityTracker
1、作用
同样先给出官方的英文类注释。小盆友以自己的理解给出这个的定义,VelocityTracker 是一个根据我们手指的触摸事件,计算出滑动速度的工具类,我们可以根据这个速度自行做计算进行视图的移动,达到粘性滑动之类的效果。
* Helper for tracking the velocity of touch events, for implementing* flinging and other such gestures.
2、API讲解
这一小节是对 VelocityTracker 公有方法 进行讲解,如果您已经对这些方法很熟悉,可以跳过。
(1) obtain()
static public VelocityTracker obtain()
方法描述:
获取一个 VelocityTracker 对象。VelocityTracker的构造函数是私有的,也就是不能通过new来创建。
(2) recycle()
public void recycle()
方法描述:
回收 VelocityTracker 实例。
(3) clear()
public void clear()
方法描述:
重置 VelocityTracker 回其初始状态。
(4) addMovement(MotionEvent event)
public void addMovement(MotionEvent event)
方法描述:
为 VelocityTracker 传入触摸事件(包括ACTION_DOWN
、ACTION_MOVE
、ACTION_UP
等),这样 VelocityTracker 才能在调用了 computeCurrentVelocity
方法后,正确的获得当前的速度。
(5) computeCurrentVelocity(int units)
public void computeCurrentVelocity(int units)
方法描述:
根据已经传入的触摸事件计算出当前的速度,可以通过getXVelocity
或 getYVelocity
进行获取对应方向上的速度。值得注意的是,计算出的速度值不超过Float.MAX_VALUE
。
参数解析:
第一个参数 units: 速度的单位。值为1表示每毫秒像素数,1000表示每秒像素数。
(6) computeCurrentVelocity(int units, float maxVelocity)
public void computeCurrentVelocity(int units, float maxVelocity)
方法描述:
根据已经传入的触摸事件计算出当前的速度,可以通过getXVelocity
或 getYVelocity
进行获取对应方向上的速度。值得注意的是,计算出的速度值不超过maxVelocity
。
参数解析:
第一个参数 units: 速度的单位。值为1表示每毫秒像素数,1000表示每秒像素数。
第二个参数 maxVelocity: 最大的速度,计算出的速度不会超过这个值。值得注意的是,这个参数必须是正数,且其单位就是我们在第一参数设置的单位。
(7) getXVelocity()
public float getXVelocity()
方法描述:
获取最后计算的水平方向速度,使用此方法前需要记得先调用computeCurrentVelocity
(8) getYVelocity()
public float getYVelocity()
方法描述:
获取最后计算的垂直方向速度,使用此方法前需要记得先调用computeCurrentVelocity
(9) getXVelocity(int id)
public float getXVelocity(int id)
方法描述:
获取对应的手指id最后计算的水平方向速度,使用此方法前需要记得先调用computeCurrentVelocity
参数解析:
第一个参数 id: 触碰的手指的id
(10) getYVelocity(int id)
public float getYVelocity(int id)
方法描述:
获取对应的手指id最后计算的垂直方向速度,使用此方法前需要记得先调用computeCurrentVelocity
参数解析:
第一个参数 id: 触碰的手指的id
3、小结
VelocityTracker 的 API 简单明了,我们可以用记住一个套路。
- 在触摸事件为
ACTION_DOWN
或是进入onTouchEvent
方法时,通过obtain
获取一个 VelocityTracker ; - 在触摸事件为
ACTION_UP
时,调用recycle
进行释放 VelocityTracker; - 在进入
onTouchEvent
方法或将ACTION_DOWN
、ACTION_MOVE
、ACTION_UP
的事件通过addMovement
方法添加进 VelocityTracker; - 在需要获取速度的地方,先调用
computeCurrentVelocity
方法,然后通过getXVelocity
、getYVelocity
获取对应方向的速度;
四、实战——带惯性滑动的柱状图
1、效果图
github 地址:传送门
虽然我们是 Scroller 和 VelocityTracker 的实战,但我们还是有必要先略提一下柱子和点的绘制,以及其动画的大致思路。然后再加入 Scroller 和 VelocityTracker。
2、绘制思路
我们来看下面这张小盆友手绘的解析图?,黑色的框代表CANVAS,蓝色的框代表用户看到的手机屏幕,深蓝色的框是我们真正每次需要绘制的区域。
从上图中,我们其实会发现一个规律,就是每隔一个 BarInterval 就绘制一个下图所示的柱子,循环的次数则由传入的数据量的个数决定。
但是,(敲黑板啦!!)值得注意的,在屏幕之外的柱子,其实对于用户来说是看不到的,我们也就没必要耗费这部分的资源来进行绘制,可以通过下面这段代码,判断柱子是否在可视区域内,可视区域的范围为屏幕的宽度各自往左和往右扩一个柱子的间隔 mBarInterval。这样做的原因是,描述的文字或小红点刚好在屏幕的左边界或右边界时,不会出现没有绘制的情况。
/*** 是否在可视的范围内** @param x* @return true:在可视的范围内;false:不在可视的范围内*/
private boolean isInVisibleArea(float x) {float dx = x - getScrollX();return -mBarInterval <= dx && dx <= mViewWidth + mBarInterval;
}
至此,图像的绘制问题就解决了,代码就不粘贴出来了,童鞋们可以进入传送门 跟着思路捋一捋。
还有一个问题,就是如何让画面跟着手指 移动 起来,这就需要重写 onTouchEvent
方法了,计算出手指的水平移动距离,然后通过 scrollBy
方法让内容移动起来。
值得一提,
scrollTo
和scrollBy
方法,都是针对 内容 或是说 canvas 进行移动。
至于如何让小红点动起来,这里使用了 ValueAnimator
进行从零至一的增加,达到不断接近目标坐标的效果。
对属性动画源码感兴趣的童鞋,可以移步小盆友的另一片博文:带有活力的属性动画源码分析与实战
3、如何惯性滑动起来
经过上一小节,我们已经知道如何绘制这一简单却又常见的柱形图了,但美中不足的就是没有 fling 的效果。所以我们需要先借住 VelocityTracker 进行获取我们当前手指的滑动速度,但这里需要注意的是,要限制其最大和最小速度。因为速度过快和过慢,都会导致交互效果不佳。获取代码如下
mMaximumVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity();
mMinimumVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
然后根据我们在 VelocityTracker小结 中的套路,进行获取手指离屏时的水平速度。以下是只保留 VelocityTracker 相关代码
/*** 控制屏幕不越界** @param event* @return*/
@Override
public boolean onTouchEvent(MotionEvent event) {// 省略无关代码...if (mVelocityTracker == null) {mVelocityTracker = VelocityTracker.obtain();}mVelocityTracker.addMovement(event);if (MotionEvent.ACTION_DOWN == event.getAction()) {// 省略无关代码...} else if (MotionEvent.ACTION_MOVE == event.getAction()) {// 省略无关代码...} else if (MotionEvent.ACTION_UP == event.getAction()) {// 计算当前速度, 1000表示每秒像素数等mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);// 获取横向速度int velocityX = (int) mVelocityTracker.getXVelocity();// 速度要大于最小的速度值,才开始滑动if (Math.abs(velocityX) > mMinimumVelocity) {// 省略无关代码...}if (mVelocityTracker != null) {mVelocityTracker.recycle();mVelocityTracker = null;}}return super.onTouchEvent(event);
}
获取完水平的速度,接下来我们需要进行真正的 fling 效果。通过一个线程来进行不断的 移动 画布,从而达到滚动效果(RecycleView中的滚动也是通过线程达到效果,有兴趣的同学可以进入RecycleView 的源码进行查看,该线程类的名字为 ViewFlinger )。
/*** 滚动线程*/
private class FlingRunnable implements Runnable {private Scroller mScroller;private int mInitX;private int mMinX;private int mMaxX;private int mVelocityX;FlingRunnable(Context context) {this.mScroller = new Scroller(context, null, false);}void start(int initX,int velocityX,int minX,int maxX) {this.mInitX = initX;this.mVelocityX = velocityX;this.mMinX = minX;this.mMaxX = maxX;// 先停止上一次的滚动if (!mScroller.isFinished()) {mScroller.abortAnimation();}// 开始 flingmScroller.fling(initX, 0, velocityX,0, 0, maxX, 0, 0);post(this);}@Overridepublic void run() {// 如果已经结束,就不再进行if (!mScroller.computeScrollOffset()) {return;}// 计算偏移量int currX = mScroller.getCurrX();int diffX = mInitX - currX;// 用于记录是否超出边界,如果已经超出边界,则不再进行回调,即使滚动还没有完成boolean isEnd = false;if (diffX != 0) {// 超出右边界,进行修正if (getScrollX() + diffX >= mCanvasWidth - mViewWidth) {diffX = (int) (mCanvasWidth - mViewWidth - getScrollX());isEnd = true;}// 超出左边界,进行修正if (getScrollX() <= 0) {diffX = -getScrollX();isEnd = true;}if (!mScroller.isFinished()) {scrollBy(diffX, 0);}mInitX = currX;}if (!isEnd) {post(this);}}/*** 进行停止*/void stop() {if (!mScroller.isFinished()) {mScroller.abortAnimation();}}
}
最后就是使用起这个线程,而使用的地方主要有两个点,一个手指按下时(即MotionEvent.ACTION_DOWN
)和手指抬起时(即 MotionEvent.ACTION_UP
),删除了不相关代码,剩余代码如下。
public boolean onTouchEvent(MotionEvent event) {// 省略不相关代码...if (MotionEvent.ACTION_DOWN == event.getAction()) {// 省略不相关代码...mFling.stop();} else if (MotionEvent.ACTION_MOVE == event.getAction()) {// 省略不相关代码...} else if (MotionEvent.ACTION_UP == event.getAction()) {// 省略不相关代码...// 速度要大于最小的速度值,才开始滑动if (Math.abs(velocityX) > mMinimumVelocity) {int initX = getScrollX();int maxX = (int) (mCanvasWidth - mViewWidth);if (maxX > 0) {mFling.start(initX, velocityX, initX, maxX);}}// 省略不相关代码...}return super.onTouchEvent(event);}
当我们 MotionEvent.ACTION_DOWN
时,我们需要停止滚动的效果,达到立马停止到手指触碰的地方。
当我们 MotionEvent.ACTION_UP
时,我们需要计算 fling
方法所需的最小值和最大值。根据我们在线程中的计算方式,所以我们的最小值和初始值为 getScrollX()
的值 而最大值为 mCanvasWidth - mViewWidth
。
最后开启线程,便达到了我们看到的效果。
完整代码的github 地址:传送门
五、写在最后
Scroller 和 VelocityTracker 的搭配使用,能让我们的控件使用起来更加丝滑,交互感更强,当然用户体验就越好。最后如果你从这篇文章有所收获,请给我个赞❤️,并关注我吧。文章中如有理解错误或是晦涩难懂的语句,请评论区留言,我们进行讨论共同进步。你的鼓励是我前进的最大动力。
让控件如此丝滑Scroller和VelocityTracker的API讲解与实战——Android高级UI相关推荐
- Android中自定义农历日历,CalendarView Android 上一个优雅、万能自定义 UI、性能高效的日历控件,热插拔!热插拔!热插拔!重要的事 @codeKK Android开源站...
An elegant CalendarView on Android platform. Freely draw UI with canvas, fast.efficient and low memo ...
- 第一站小红书图片裁剪控件之二,自定义CoordinatorLayout联动效果
本篇续: 第一站小红书图片裁剪控件,深度解析大厂炫酷控件 先来看看几张效果图: emmmm,想感受高清丝滑的动画效果,有以下两种方式: https://github.com/HpWens/MeiWid ...
- 像小红书一样的图片裁剪控件联动效果
今日科技快讯 据CNBC报道,美国法官已经要求特斯拉首席执行官埃隆·马斯克(Elon Musk)在未来两周内设法与美国证券交易委员会(SEC)达成和解协议.否则,法院将决定是否判马斯克犯有藐视法庭罪. ...
- Android 最丝滑的动画--第二篇MotionLayout概述(后面陆续更新)(带效果图)
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 一. MotionLayout概述 MotionLayout 是谷歌推出的一种布局类型,可帮助管理应用中的运动和微件动画.Motion ...
- 粘性控件,滑动停留StickLayout(导航栏滑动停留)
我们平时在使用APP的时候,经常可以见到一些导航栏滑到顶端就停留,而下面的控件可以接着滑动:今天,我就给大家介绍一个非常好用的滑动粘性控件StickLayout,它不仅可以让其任意一个直接子控件滑动停 ...
- android 固定底部 布局_Android系统列表控件
在android系统控件中,有多个控件可以展示列表数据. 一.ListView 该组件是android中最常用的一个UI组件,用于实现在屏幕上显示多个内容,以便于我们用手指进行滑动. ListView ...
- JavaFX UI控件教程(二)之JavaFX UI控件
翻译自 JavaFX UI控件 本章概述了通过API提供的JavaFX UI控件. JavaFX UI控件是使用场景图中的节点构建的.因此,控件可以使用JavaFX平台的视觉丰富功能.由于JavaF ...
- android listview添加数据_Android系统列表控件
在android系统控件中,有多个控件可以展示列表数据. 一.ListView 该组件是android中最常用的一个UI组件,用于实现在屏幕上显示多个内容,以便于我们用手指进行滑动. ListView ...
- Android仿同花顺自选股列表控件
介绍 RecyclerView的开发中,我们通常会遇到一行显示不下内容的情况,产品会要求我们的item是可以滚动的,并且头部是固定的.特别在股票行情类相关的app上,这样的场景是非常多的,所以封装了如 ...
- Android开源控件收集整理
一 .基本控件 TextView HTextView 一款支持TextView文字动画效果的Android组件库.GitHub - hanks-zyh/HTextView: Animation eff ...
最新文章
- jquery操作select取值赋值与设置选中[转]
- 产品诞生过程--导图
- Item 14 In public classes, use accessor methods, not public fields
- RabbitMQ从入门到精通
- java包裹邮费计算_GitHub - honghailiang/FreightSystem: 基于Java Swing编写的简易运费计算工具...
- Qt:Windows编程—代码注入
- 云原生解决了什么问题?
- 执行DBMS_METADATA.get_ddl报ORA-39212的解决方法
- 每日总结 神州数码DCWS
- 深度学习中的 Attention 机制总结与代码实现(2017-2021年)
- 物联网时代的技术迷雾
- 同时开多个独立窗口Visio 2003/2007版本的软件
- 软件测试中的接口分析,软件测试接口测试之管理类—叩丁狼分享
- ABB变频器维修,ABB变频器,ABB变频器配件FS300R12KE3/AGDR-61C 驱动模块APOW-01C 电源板AINP-01C 可控硅触发板
- ARCGIS小工具(插件)免费版_自取_GIS插件_工具_其他
- 基于NNIE神经网络引擎_海思hi3516DV300方案硬件平台适合做哪些开发
- hacker 入门指南
- python中Try的运用及意义
- 计算机excel教程,电脑安装excel教程的方法步骤详解
- 访问ip不在白名单中,请参考FAQ: