让控件如此丝滑Scroller和VelocityTracker的API讲解与实战——Android高级UI

作者 : 开心源码 本文共9453个字,预计阅读时间需要24分钟 发布时间: 2022-05-12 共187人阅读

目录
一、前言
二、Scroller
三、VelocityTracker
四、实战——带惯性滑动的柱状图
五、写在最后

一、前言

自己设置控件中,难免会遇到需要滑动的场景。而Canvas提供的scrollTo和scrollBy方法只能达到移动的效果,需要达到真正的滑动便需要我们今天分享的两把基础利器ScrollerVelocityTracker。老规矩,先上实战图,再进行分享。

带惯性滑动的柱状图

image

二、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&mdash;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()

方法形容:

计算滚动中的新坐标,会配合着 getCurrXgetCurrY 方法使用,达到滚动效果。值得注意的是,假如返回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_DOWNACTION_MOVEACTION_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 简单明了,我们可以用记住一个套路。

  1. 在触摸事件为 ACTION_DOWN 或者是进入 onTouchEvent 方法时,通过 obtain 获取一个 VelocityTracker ;
  2. 在触摸事件为 ACTION_UP 时,调用 recycle 进行释放 VelocityTracker;
  3. 在进入 onTouchEvent 方法或者将 ACTION_DOWNACTION_MOVEACTION_UP 的事件通过 addMovement 方法增加进 VelocityTracker;
  4. 在需要获取速度的地方,先调用 computeCurrentVelocity 方法,而后通过 getXVelocitygetYVelocity 获取对应方向的速度;

四、实战——带惯性滑动的柱状图

1、效果图

image

github 地址:传送门

尽管我们是 ScrollerVelocityTracker 的实战,但我们还是有必要先略提一下柱子和点的绘制,以及其动画的大致思路。而后再加入 ScrollerVelocityTracker

2、绘制思路

我们来看下面这张小盆友手绘的解析图??,黑色的框代表CANVAS,蓝色的框代表客户看到的手机屏幕,深蓝色的框是我们真正每次需要绘制的区域。

image
从上图中,我们其实会发现一个规律,就是每隔一个 BarInterval 就绘制一个下图所示的柱子,循环的次数则由传入的数据量的个数决定。
image
但是,(敲黑板啦!!)值得注意的,在屏幕之外的柱子,其实对于客户来说是看不到的,我们也就没必要耗费这部分的资源来进行绘制,可以通过下面这段代码,判断柱子能否在可视区域内,可视区域的范围为屏幕的宽度各自往左和往右扩一个柱子的间隔 mBarInterval。这样做的起因是,形容的文字或者小红点恰好在屏幕的左边界或者右边界时,不会出现没有绘制的情况。

/** * 能否在可视的范围内 * * @param x * @return true:在可视的范围内;false:不在可视的范围内 */private boolean isInVisibleArea(float x) {    float dx = x - getScrollX();    return -mBarInterval <= dx && dx <= mViewWidth + mBarInterval;}

至此,图像的绘制问题就处理了,代码就不粘贴出来了,童鞋们可以进入传送门 跟着思路捋一捋。

还有一个问题,就是如何让画面跟着手指 移动 起来,这就需要重写 onTouchEvent 方法了,计算出手指的水平移动距离,而后通过 scrollBy 方法让内容移动起来。

值得一提,scrollToscrollBy 方法,都是针对 内容 或者是说 canvas 进行移动。

至于如何让小红点动起来,这里使用了 ValueAnimator 进行从零至一的添加,达到不断接近目标坐标的效果。

对属性动画源码感兴趣的童鞋,可以移步小盆友的另一片博文:带有活力的属性动画源码分析与实战

3、如何惯性滑动起来

经过上一小节,我们已经知道如何绘制这一简单却又常见的柱形图了,但美中不足的就是没有 fling 的效果。所以我们需要先借住 VelocityTracker 进行获取我们当前手指的滑动速度,但这里需要注意的是,要限制其最大和最小速度。由于速渡过快和过慢,都会导致交互效果不佳。获取代码如下

mMaximumVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity();mMinimumVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();

而后根据我们在 VelocityTracker小结 中的套路,进行获取手指离屏时的水平速度。以下是只保留 VelocityTracker 相关代码

/** * 控制屏幕不越界 * * @param event * @return */@Overridepublic 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

image
最后开启线程,便达到了我们看到的效果。

完整代码的github 地址:传送门

五、写在最后

ScrollerVelocityTracker 的搭配使用,能让我们的控件使用起来更加丝滑,交互感更强,当然客户体验就越好。最后假如你从这篇文章有所收获,请给我个赞??,并关注我吧。文章中如有了解错误或者是晦涩难懂的语句,请评论区留言,我们进行探讨共同进步。你的鼓励是我前进的最大动力。假如需要更为深入的讨论,加我微信吧??。

image

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 让控件如此丝滑Scroller和VelocityTracker的API讲解与实战——Android高级UI

发表回复