目录java
1、前言git
2、Scrollergithub
3、VelocityTrackercanvas
4、实战——带惯性滑动的柱状图markdown
5、写在最后app
自定义控件中,不免会遇到须要滑动的场景。而Canvas提供的scrollTo和scrollBy方法只能达到移动的效果,须要达到真正的滑动便须要咱们今天分享的两把基础利器Scroller和VelocityTracker。老规矩,先上实战图,再进行分享。ide
带惯性滑动的柱状图 函数
童鞋们能够先看下下面这段官方的英文类注释。小盆友以本身的理解给出这个类的做用是,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>
复制代码
这一小节是对 Scroller 的 构造方法 和 经常使用的公有方法 进行讲解,若是您已经对这些方法很熟悉,能够跳过。oop
public Scroller(Context context) 复制代码
方法描述:
建立一个 Scroller 实例。
参数解析:
第一个参数 context: 上下文;
public Scroller(Context context, Interpolator interpolator) 复制代码
方法描述:
建立一个 Scroller 实例。
参数解析:
第一个参数 context: 上下文;
第二个参数 interpolator: 插值器,用于在 computeScrollOffset 方法中,而且是在 SCROLL_MODE 模式下,根据时间的推移计算位置。为null时,使用默认 ViscousFluidInterpolator 插值器。
public Scroller(Context context, Interpolator interpolator, boolean flywheel) 复制代码
方法描述:
建立一个 Scroller 实例。
参数解析:
第一个参数 context: 上下文;
第二个参数 interpolator: 插值器,用于在 computeScrollOffset 方法中,而且是在 SCROLL_MODE 模式下,根据时间的推移计算位置。为null时,使用默认 ViscousFluidInterpolator 插值器。
第三个参数 flywheel: 支持渐进式行为,该参数只做用于 FLING_MODE 模式下。
public final void setFriction(float friction) 复制代码
方法描述:
用于设置在 FLING_MODE 模式下的摩擦系数
参数解析:
第一个参数 friction: 摩擦系数
public final boolean isFinished() 复制代码
方法描述:
滚动是否已结束,用于判断 Scroller 在滚动过程的状态,咱们能够作一些终止或继续运行的逻辑分支。
public final void forceFinished(boolean finished) 复制代码
方法描述:
强制的让滚动状态置为咱们所设置的参数值 finished 。
public final int getDuration() 复制代码
方法描述:
返回 Scroller 将持续的时间(以毫秒为单位)。
public final int getCurrX() 复制代码
方法描述:
返回滚动中的当前X相对于原点的偏移量,即当前坐标的X坐标。
public final int getCurrY() 复制代码
方法描述:
返回滚动中的当前Y相对于原点的偏移量,即当前坐标的Y坐标。
public float getCurrVelocity() 复制代码
方法描述:
获取当前速度。
public boolean computeScrollOffset() 复制代码
方法描述:
计算滚动中的新坐标,会配合着 getCurrX 和 getCurrY 方法使用,达到滚动效果。值得注意的是,若是返回true,说明动画还未完成。相反,返回false,说明动画已经完成或是被终止了。
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: 滚动的时长
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
public void abortAnimation() 复制代码
方法描述:
中止动画,值得注意的是,此时若是调用 getCurrX() 和 getCurrY() 移动到的是最终的坐标,这一点和经过 forceFinished 直接将动画中止是不相同的。
从上面的 API 讲解中,咱们会发现,至始至终都没有对咱们须要做用的View有任何的关联,而是经过计算,而后获取当前时间点对应的坐标,如此而已。这也就印证了前面的定义,至于怎么真正的使用,咱们留到实战篇。
一样先给出官方的英文类注释。小盆友以本身的理解给出这个的定义,VelocityTracker 是一个根据咱们手指的触摸事件,计算出滑动速度的工具类,咱们能够根据这个速度自行作计算进行视图的移动,达到粘性滑动之类的效果。
* Helper for tracking the velocity of touch events, for implementing
* flinging and other such gestures.
复制代码
这一小节是对 VelocityTracker 公有方法 进行讲解,若是您已经对这些方法很熟悉,能够跳过。
static public VelocityTracker obtain() 复制代码
方法描述:
获取一个 VelocityTracker 对象。VelocityTracker的构造函数是私有的,也就是不能经过new来建立。
public void recycle() 复制代码
方法描述:
回收 VelocityTracker 实例。
public void clear() 复制代码
方法描述:
重置 VelocityTracker 回其初始状态。
public void addMovement(MotionEvent event) 复制代码
方法描述:
为 VelocityTracker 传入触摸事件(包括ACTION_DOWN
、ACTION_MOVE
、ACTION_UP
等),这样 VelocityTracker 才能在调用了 computeCurrentVelocity
方法后,正确的得到当前的速度。
public void computeCurrentVelocity(int units) 复制代码
方法描述:
根据已经传入的触摸事件计算出当前的速度,能够经过getXVelocity
或 getYVelocity
进行获取对应方向上的速度。值得注意的是,计算出的速度值不超过Float.MAX_VALUE
。
参数解析:
第一个参数 units: 速度的单位。值为1表示每毫秒像素数,1000表示每秒像素数。
public void computeCurrentVelocity(int units, float maxVelocity) 复制代码
方法描述:
根据已经传入的触摸事件计算出当前的速度,能够经过getXVelocity
或 getYVelocity
进行获取对应方向上的速度。值得注意的是,计算出的速度值不超过maxVelocity
。
参数解析:
第一个参数 units: 速度的单位。值为1表示每毫秒像素数,1000表示每秒像素数。
第二个参数 maxVelocity: 最大的速度,计算出的速度不会超过这个值。值得注意的是,这个参数必须是正数,且其单位就是咱们在第一参数设置的单位。
public float getXVelocity() 复制代码
方法描述:
获取最后计算的水平方向速度,使用此方法前须要记得先调用computeCurrentVelocity
public float getYVelocity() 复制代码
方法描述:
获取最后计算的垂直方向速度,使用此方法前须要记得先调用computeCurrentVelocity
public float getXVelocity(int id) 复制代码
方法描述:
获取对应的手指id最后计算的水平方向速度,使用此方法前须要记得先调用computeCurrentVelocity
参数解析:
第一个参数 id: 触碰的手指的id
public float getYVelocity(int id) 复制代码
方法描述:
获取对应的手指id最后计算的垂直方向速度,使用此方法前须要记得先调用computeCurrentVelocity
参数解析:
第一个参数 id: 触碰的手指的id
VelocityTracker 的 API 简单明了,咱们能够用记住一个套路。
ACTION_DOWN
或是进入 onTouchEvent
方法时,经过 obtain
获取一个 VelocityTracker ;ACTION_UP
时,调用 recycle
进行释放 VelocityTracker;onTouchEvent
方法或将 ACTION_DOWN
、ACTION_MOVE
、ACTION_UP
的事件经过 addMovement
方法添加进 VelocityTracker;computeCurrentVelocity
方法,而后经过 getXVelocity
、getYVelocity
获取对应方向的速度;github 地址:传送门
虽然咱们是 Scroller 和 VelocityTracker 的实战,但咱们仍是有必要先略提一下柱子和点的绘制,以及其动画的大体思路。而后再加入 Scroller 和 VelocityTracker。
咱们来看下面这张小盆友手绘的解析图😂,黑色的框表明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
进行从零至一的增长,达到不断接近目标坐标的效果。
对属性动画源码感兴趣的童鞋,能够移步小盆友的另外一片博文:带有活力的属性动画源码分析与实战
通过上一小节,咱们已经知道如何绘制这一简单却又常见的柱形图了,但美中不足的就是没有 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();
}
// 开始 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
。 最后开启线程,便达到了咱们看到的效果。
完整代码的github 地址:传送门
Scroller 和 VelocityTracker 的搭配使用,能让咱们的控件使用起来更加丝滑,交互感更强,固然用户体验就越好。最后若是你从这篇文章有所收获,请给我个赞❤️,并关注我吧。文章中若有理解错误或是晦涩难懂的语句,请评论区留言,咱们进行讨论共同进步。你的鼓励是我前进的最大动力。
高级UI系列的Github地址:请进入传送门,若是喜欢的话给我一个star吧😄