一、前言
自定义控件中,难免会遇到需要滑动的场景。而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()
方法描述: 计算滚动中的新坐标,会配合着 getCurrX 和 getCurrY 方法使用,达到滚动效果。值得注意的是,如果返回true,说明动画还未完成。相反,返回false,说明动画已经完成或是被终止了。 方法描述: 通过提供起点,行程距离和滚动持续时间,进行滚动的一种方式,即 SCROLL_MODE。该方法可以用于实现像ViewPager的滑动效果。 参数解析: 第一个参数 startX: 开始点的x坐标 第二个参数 startY: 开始点的y坐标 第三个参数 dx: 水平方向的偏移量,正数会将内容向左滚动。 第四个参数 dy: 垂直方向的偏移量,正数会将内容向上滚动。 第五个参数 duration: 滚动的时长 方法描述: 用于带速度的滑动,行进的距离将取决于投掷的初始速度。可以用于实现类似 RecycleView 的滑动效果。 参数解析:第一个参数 startX: 开始滑动点的x坐标 第二个参数 startY: 开始滑动点的y坐标 第三个参数 velocityX: 水平方向的初始速度,单位为每秒多少像素(px/s) 第四个参数 velocityY: 垂直方向的初始速度,单位为每秒多少像素(px/s) 第五个参数 minX: x坐标最小的值,最后的结果不会低于这个值; 第六个参数 maxX: x坐标最大的值,最后的结果不会超过这个值; 第七个参数 minY: y坐标最小的值,最后的结果不会低于这个值; 第八个参数 maxY: y坐标最大的值,最后的结果不会超过这个值; 值得一说: 方法描述: 停止动画,值得注意的是,此时如果调用 getCurrX() 和 getCurrY() 移动到的是最终的坐标,这一点和通过 forceFinished 直接将动画停止是不相同的。 从上面的 API 讲解中,我们会发现,至始至终都没有对我们需要作用的View有任何的关联,而是通过计算,然后获取当前时间点对应的坐标,如此而已。这也就印证了前面的定义,至于怎么真正的使用,我们留到实战篇。 同样先给出官方的英文类注释。小盆友以自己的理解给出这个的定义,VelocityTracker 是一个根据我们手指的触摸事件,计算出滑动速度的工具类,我们可以根据这个速度自行做计算进行视图的移动,达到粘性滑动之类的效果。 这一小节是对 VelocityTracker 公有方法 进行讲解,如果您已经对这些方法很熟悉,可以跳过。 方法描述: 获取一个 VelocityTracker 对象。VelocityTracker的构造函数是私有的,也就是不能通过new来创建。 方法描述: 回收 VelocityTracker 实例。 方法描述: 重置 VelocityTracker 回其初始状态。 方法描述: 为 VelocityTracker 传入触摸事件(包括 方法描述: 根据已经传入的触摸事件计算出当前的速度,可以通过 参数解析: 第一个参数 units: 速度的单位。值为1表示每毫秒像素数,1000表示每秒像素数。 方法描述: 根据已经传入的触摸事件计算出当前的速度,可以通过 参数解析: 第一个参数 units: 速度的单位。值为1表示每毫秒像素数,1000表示每秒像素数。 第二个参数 maxVelocity: 最大的速度,计算出的速度不会超过这个值。值得注意的是,这个参数必须是正数,且其单位就是我们在第一参数设置的单位。 方法描述: 获取最后计算的水平方向速度,使用此方法前需要记得先调用 方法描述: 获取最后计算的垂直方向速度,使用此方法前需要记得先调用 方法描述: 获取对应的手指id最后计算的水平方向速度,使用此方法前需要记得先调用 参数解析: 第一个参数 id: 触碰的手指的id 方法描述: 获取对应的手指id最后计算的垂直方向速度,使用此方法前需要记得先调用 参数解析: 第一个参数 id: 触碰的手指的id VelocityTracker 的 API 简单明了,我们可以用记住一个套路。 在触摸事件为 在触摸事件为 在进入 在需要获取速度的地方,先调用 github 地址:传送门 虽然我们是 Scroller 和 VelocityTracker 的实战,但我们还是有必要先略提一下柱子和点的绘制,以及其动画的大致思路。然后再加入 Scroller 和 VelocityTracker。 我们来看下面这张小盆友手绘的解析图,黑色的框代表CANVAS,蓝色的框代表用户看到的手机屏幕,深蓝色的框是我们真正每次需要绘制的区域。 至此,图像的绘制问题就解决了,代码就不粘贴出来了,童鞋们可以进入传送门 跟着思路捋一捋。 还有一个问题,就是如何让画面跟着手指 移动 起来,这就需要重写 值得一提, 至于如何让小红点动起来,这里使用了 对属性动画源码感兴趣的童鞋,可以移步小盆友的另一片博文:带有活力的属性动画源码分析与实战 经过上一小节,我们已经知道如何绘制这一简单却又常见的柱形图了,但美中不足的就是没有 fling 的效果。所以我们需要先借住 VelocityTracker 进行获取我们当前手指的滑动速度,但这里需要注意的是,要限制其最大和最小速度。因为速度过快和过慢,都会导致交互效果不佳。获取代码如下 然后根据我们在 VelocityTracker小结 中的套路,进行获取手指离屏时的水平速度。以下是只保留 VelocityTracker 相关代码 获取完水平的速度,接下来我们需要进行真正的 fling 效果。通过一个线程来进行不断的 移动 画布,从而达到滚动效果(RecycleView中的滚动也是通过线程达到效果,有兴趣的同学可以进入RecycleView 的源码进行查看,该线程类的名字为 ViewFlinger )。 最后就是使用起这个线程,而使用的地方主要有两个点,一个手指按下时(即 当我们 当我们 完整代码的github 地址:传送门 Scroller 和 VelocityTracker 的搭配使用,能让我们的控件使用起来更加丝滑,交互感更强,当然用户体验就越好。最后如果你从这篇文章有所收获,请给我个赞❤️,并关注我吧。文章中如有理解错误或是晦涩难懂的语句,请评论区留言,我们进行讨论共同进步。你的鼓励是我前进的最大动力。如果需要更为深入的探讨,加我微信吧。public boolean computeScrollOffset()
(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)
(10) fling
public void fling(int startX, int startY, int velocityX, int velocityY,
int minX, int maxX, int minY, int maxY)
minX <= 终止值的x坐标 <= maxX
minY <= 终止值的y坐标 <= maxY
(11) abortAnimation()
public void abortAnimation()
3、小结
三、VelocityTracker
1、作用
* Helper for tracking the velocity of touch events, for implementing
* flinging and other such gestures.
2、API讲解
(1) obtain()
static public VelocityTracker obtain()
(2) recycle()
public void recycle()
(3) clear()
public void clear()
(4) addMovement(MotionEvent event)
public void addMovement(MotionEvent event)
ACTION_DOWN
、ACTION_MOVE
、ACTION_UP
等),这样 VelocityTracker 才能在调用了 computeCurrentVelocity
方法后,正确的获得当前的速度。(5) computeCurrentVelocity(int units)
public void computeCurrentVelocity(int units)
getXVelocity
或 getYVelocity
进行获取对应方向上的速度。值得注意的是,计算出的速度值不超过Float.MAX_VALUE
。(6) computeCurrentVelocity(int units, float maxVelocity)
public void computeCurrentVelocity(int units, float maxVelocity)
getXVelocity
或 getYVelocity
进行获取对应方向上的速度。值得注意的是,计算出的速度值不超过maxVelocity
。(7) getXVelocity()
public float getXVelocity()
computeCurrentVelocity
(8) getYVelocity()
public float getYVelocity()
computeCurrentVelocity
(9) getXVelocity(int id)
public float getXVelocity(int id)
computeCurrentVelocity
(10) getYVelocity(int id)
public float getYVelocity(int id)
computeCurrentVelocity
3、小结
ACTION_DOWN
或是进入 onTouchEvent
方法时,通过 obtain
获取一个 VelocityTracker ;ACTION_UP
时,调用 recycle
进行释放 VelocityTracker;onTouchEvent
方法或将 ACTION_DOWN
、ACTION_MOVE
、ACTION_UP
的事件通过 addMovement
方法添加进 VelocityTracker;computeCurrentVelocity
方法,然后通过 getXVelocity
、getYVelocity
获取对应方向的速度;四、实战——带惯性滑动的柱状图
1、效果图
2、绘制思路
/**
* 是否在可视的范围内
*
* @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、如何惯性滑动起来
mMaximumVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity();
mMinimumVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
/**
* 控制屏幕不越界
*
* @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);
}
/**
* 滚动线程
*/
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();
}
// 开始 fling
mScroller.fling(initX, 0, velocityX,
0, 0, maxX, 0, 0);
post(this);
}
@Override
public 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
。五、写在最后
作者:猛猛的小盆友