View弹性滑动详解

之前写了一个滚动选择控件
WheelView,在这个控件中我设计了弹性滚动的实现机制,再了解View弹性滚动之前,我们先来学习一下View滚动机制的实现.


View的scrollTo/scrollBy

这里基于Android5.0版本的源码介绍View类中这两个函数的具体实现.

scrollTo源码如下:

/*** 对View设置滚动的x和y轴坐标.* @param x x轴滚动的终点坐标* @param y y轴滚动的终点坐标*/
public void scrollTo(int x, int y) {if (mScrollX != x || mScrollY != y) {int oldX = mScrollX;int oldY = mScrollY;mScrollX = x;mScrollY = y;invalidateParentCaches();onScrollChanged(mScrollX, mScrollY, oldX, oldY);if (!awakenScrollBars()) {postInvalidateOnAnimation();}}
}

scrollBy源码如下:

/*** 设置View的x轴和y轴的滚动增量.* @param x x轴的滚动增量* @param y y轴的滚动增量*/
public void scrollBy(int x, int y) {scrollTo(mScrollX + x, mScrollY + y);
}

从上述源码可以看出,scrollBy依赖于scrollTo的实现.他俩的区别是:

scrollBy的x和y是滚动增量,即在上次滚动的终点坐标上增加x和y,是相对滑动.(注意:x和y可能为负数)
scrollTo的x和y是滚动的终点坐标,是绝对滑动.

而且,scrollTo的实现也仅仅是修改了mScrollX和mScrollY的值,然后调用了invalidate方法重绘了View.那么,mScrollX和mScrollY的含义是什么呢?

  • mScrollX:View的左边缘和View内容左边缘在x轴上的距离,即View左边缘x轴坐标-View内容左边缘的x轴坐标.
  • mScrollY:View的上边缘和View内容上边缘在y轴上的距离,即View上边缘y轴坐标-View内容上边缘的y轴坐标.

同时,需要明确很重要的一点:View的滑动并非是View的滑动,而是View内容的滑动.

提供一个图示来理解View的滑动:

缺陷:

虽然调用View的scrollBy和scrollTo方法可以很方便的实现View的滚动,但是这种滚动是瞬间完成的(调用invalidate方法),没有弹性滑动的效果,为了达到弹性滑动的目的,我们开始介绍本篇文章的主角:Scroller.


Scroller

在介绍Scroller之前,我们需要明确知道:
Scroller代码和View代码完全解耦,Scroller代码本身不会引起View的滑动,通过Scroller代码,我们可以平滑的获取当前View需要滑动的位置,然后调用View的scrollTo/scrollBy进行移动.

构造函数

我们先来看一下Scroller的构造函数注释源码:

/*** 使用默认的滑动时间和插值器构造Scroller.*/
public Scroller(Context context) {this(context, null);
}/*** 使用给定的插值器来构造Scroller.*/
public Scroller(Context context, Interpolator interpolator) {this(context, interpolator,context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);
}/*** 使用给定的插值器来构造Scroller.Android3.0以上的版本支持"flywheel"的行为.*/
public Scroller(Context context, Interpolator interpolator, boolean flywheel) {// 设置滑动停止标识位为truemFinished = true;// 构造插值器if (interpolator == null) {mInterpolator = new ViscousFluidInterpolator();} else {mInterpolator = interpolator;}// 获取屏幕的密度(每英寸的像素数)mPpi = context.getResources().getDisplayMetrics().density * 160.0f;// 计算摩擦力mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());// 标记是否支持flying模式mFlywheel = flywheel;mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning
}

两种模式

Scroller支持两种模式的滑动,分别是:

  • SCROLL_MODE:调用startScroll,正常滚动模式.
  • FLING_MODE:调用fling,抛掷模式.

接下来,针对这两种模式进行分别讲解.

SCROLL_MODE

我们直接看一下startScroll的源码做了哪些事情:

/*** 给定滚动起始点坐标,在指定的时间内滚动指定的偏移量.* 距离计算:* dx=view左边缘-view内容左边缘;dx为正,代表内容向左移动;dx为负,代表内容向右移动.* dy=view上边缘-view内容上边缘;dy为正,代表内容向上移动;dx为负,代表内容向下移动.** @param startX   x轴方向滚动起始点坐标.* @param startY   y轴方向滚动起始点坐标.* @param dx       x轴方向滚动距离.* @param dy       y轴方向滚动距离.* @param duration 滚动持续的时间(默认滚动时间为250ms).*/
public void startScroll(int startX, int startY, int dx, int dy, int duration) {mMode = SCROLL_MODE;mFinished = false;mDuration = duration;mStartTime = AnimationUtils.currentAnimationTimeMillis();mStartX = startX;mStartY = startY;mFinalX = startX + dx;mFinalY = startY + dy;mDeltaX = dx;mDeltaY = dy;// 持续时间的倒数,用来得到的插值器返回的值mDurationReciprocal = 1.0f / (float) mDuration;
}

通过注释的源码,我们可以验证最初的结论:Scroller和View完全解耦,Scroller并不会直接控制View的滑动,它只是为View提供滑动的参数.具体参数包括:

  • mMode: 设置为滑动模式.
  • mFinished: 设置滑动结束标识为false.
  • mDuration: 设置滑动时间间隔.
  • mStartTime: 设置滑动的起始时间.
  • mStartX: 设置x轴的起始点坐标.
  • mStartY: 设置Y轴的起始点坐标
  • mFinalX: 设置x轴的终点坐标.
  • mFinalY: 设置y轴的终点坐标.
  • mDeltaX: 设置x轴的滑动距离.
  • mDeltaY: 设置y轴的滑动距离.
  • mDurationReciprocal: 设置时间的倒数.

computeScrollOffset

之所以这里提前介绍computeScrollOffset函数,是因为View只有配合computeScrollOffset函数,才能实现真正的滑动.源码中跟SCROLL_MODE相关代码如下:

public boolean computeScrollOffset() {// 如果已经结束,直接返回false.if (mFinished) {return false;}// 计算已经度过的时间.int timePassed = (int) (AnimationUtils.currentAnimationTimeMillis() - mStartTime);if (timePassed < mDuration) {switch (mMode) {// 处理滚动模式case SCROLL_MODE:// 根据过度的时间计算偏移比例final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);mCurrX = mStartX + Math.round(x * mDeltaX);mCurrY = mStartY + Math.round(x * mDeltaY);break;// 处理fling模式......}} else {// 当时间结束时,直接将x和y坐标置为终止状态的x和y坐标,同时将终止标志位置为true.mCurrX = mFinalX;mCurrY = mFinalY;mFinished = true;}return true;
}

可以看出,computeScrollOffset也只是根据时间偏移计算x轴和y轴应该到达的坐标.

SCROLL_MODE实战

介绍了SCROLL_MODE的具体实现,接下来就通过代码演示一下Scroller是如何和View进行互动的.这里提供一个例子,在40秒内将TextView的内容在x轴向右移动400:

private void initScrollCase() {mImageView = (ImageView) findViewById(R.id.id_img_tv);// 获取起始滑动点坐标int startX = mImageView.getScrollX();int startY = mImageView.getScrollY();mScroller = new Scroller(getApplicationContext());Log.e("zhengyi.wzy", "startX=" + startX + ", startY=" + startY);mScroller.startScroll(startX, startY, -400, 0, 40000);mImageView.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {mHandler.postDelayed(new Runnable() {@Overridepublic void run() {boolean isFinished = mScroller.computeScrollOffset();if (!isFinished) {return;}// 获取当前滑动点坐标int x = mScroller.getCurrX();int y = mScroller.getCurrY();mImageView.scrollTo(x, y);mHandler.postDelayed(this, 25);}}, 25);}});
}

千万注意:起始点是View.getScrollX()和View.getScrollY(),而不是View.getLeft()或者View.getTop()

FLING_MODE

Scroller还提供一种FLING模式,我认为它的中文翻译应该叫“抛掷模式”.fling的英文注释源码如下:

/*** Start scrolling based on a fling gesture. The distance travelled will* depend on the initial velocity of the fling.** @param startX    x轴的起始坐标.* @param startY    y轴的起始坐标.* @param velocityX x轴方向的初始速率.* @param velocityY y轴方向的初始速率.* @param minX      x轴终点最小值.* @param maxX      x轴终点最大值.* @param minY      y轴终点最小值.* @param maxY      y轴终点最大值.*/
public void fling(int startX, int startY, int velocityX, int velocityY,int minX, int maxX, int minY, int maxY) {// 如果上次滑动也是FLING_MODE并且滑动没有结束if (mFlywheel && !mFinished) {// 获取之前的总速率float oldVel = getCurrVelocity();float dx = (float) (mFinalX - mStartX);float dy = (float) (mFinalY - mStartY);float hyp = (float) Math.sqrt(dx * dx + dy * dy);float ndx = dx / hyp;float ndy = dy / hyp;// 通过距离比例计算出x轴和y轴的速率float oldVelocityX = ndx * oldVel;float oldVelocityY = ndy * oldVel;// 如果速率方向相同,则进行速率累加if (Math.signum(velocityX) == Math.signum(oldVelocityX) &&Math.signum(velocityY) == Math.signum(oldVelocityY)) {velocityX += oldVelocityX;velocityY += oldVelocityY;}}// 设置模式为FLING_MODEmMode = FLING_MODE;// 设置结束标志位为false.mFinished = false;// 根据勾股定理获取总的速率float velocity = (float) Math.sqrt(velocityX * velocityX + velocityY * velocityY);mVelocity = velocity;// 通过速率获取滑动的持续时间mDuration = getSplineFlingDuration(velocity);// 获取滑动起始时间mStartTime = AnimationUtils.currentAnimationTimeMillis();// 获取起始x轴和y轴坐标mStartX = startX;mStartY = startY;float coeffX = velocity == 0 ? 1.0f : velocityX / velocity;float coeffY = velocity == 0 ? 1.0f : velocityY / velocity;// 获取最大滑动距离double totalDistance = getSplineFlingDistance(velocity);mDistance = (int) (totalDistance * Math.signum(velocity));// 计算终点的x轴和y轴坐标mMinX = minX;mMaxX = maxX;mMinY = minY;mMaxY = maxY;mFinalX = startX + (int) Math.round(totalDistance * coeffX);// Pin to mMinX <= mFinalX <= mMaxXmFinalX = Math.min(mFinalX, mMaxX);mFinalX = Math.max(mFinalX, mMinX);mFinalY = startY + (int) Math.round(totalDistance * coeffY);// Pin to mMinY <= mFinalY <= mMaxYmFinalY = Math.min(mFinalY, mMaxY);mFinalY = Math.max(mFinalY, mMinY);
}

computeScrollOffset

跟FLING_MODE模式相关源码如下:

/*** 用来返回当前View需要移动到的x轴和y轴坐标.*/
public boolean computeScrollOffset() {// 如果已经结束,直接返回false.if (mFinished) {return false;}// 计算已经度过的时间.int timePassed = (int) (AnimationUtils.currentAnimationTimeMillis() - mStartTime);if (timePassed < mDuration) {switch (mMode) {// 处理fling模式case FLING_MODE:// 获取当前滑动时间与总时间的比例final float t = (float) timePassed / mDuration;final int index = (int) (NB_SAMPLES * t);float distanceCoef = 1.f;float velocityCoef = 0.f;if (index < NB_SAMPLES) {final float t_inf = (float) index / NB_SAMPLES;final float t_sup = (float) (index + 1) / NB_SAMPLES;final float d_inf = SPLINE_POSITION[index];final float d_sup = SPLINE_POSITION[index + 1];velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);distanceCoef = d_inf + (t - t_inf) * velocityCoef;}mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));// Pin to mMinX <= mCurrX <= mMaxXmCurrX = Math.min(mCurrX, mMaxX);mCurrX = Math.max(mCurrX, mMinX);mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));// Pin to mMinY <= mCurrY <= mMaxYmCurrY = Math.min(mCurrY, mMaxY);mCurrY = Math.max(mCurrY, mMinY);if (mCurrX == mFinalX && mCurrY == mFinalY) {mFinished = true;}break;}} else {// 当时间结束时,直接将x和y坐标置为终止状态的x和y坐标,同时将终止标志位置为true.mCurrX = mFinalX;mCurrY = mFinalY;mFinished = true;}return true;
}

FLING_MODE在通过速率计算当前的位置的代码我还不是特别清楚,主要是算法的实现,可能我物理太久没碰生疏了.但是用法都是统一的,至于FLING模式的使用场景,大家可以结果手势检测类(GestureDetector)去进行使用.

Scroller源码详解相关推荐

  1. 【Live555】live555源码详解(九):ServerMediaSession、ServerMediaSubsession、live555MediaServer

    [Live555]live555源码详解系列笔记 继承协作关系图 下面红色表示本博客将要介绍的三个类所在的位置: ServerMediaSession.ServerMediaSubsession.Dy ...

  2. 【Live555】live555源码详解系列笔记

    [Live555]liveMedia下载.配置.编译.安装.基本概念 [Live555]live555源码详解(一):BasicUsageEnvironment.UsageEnvironment [L ...

  3. 【Live555】live555源码详解(八):testRTSPClient

    [Live555]live555源码详解系列笔记 继承协作关系图 下面红色表示本博客将要介绍的testRTSPClient实现的三个类所在的位置: ourRTSPClient.StreamClient ...

  4. 【Live555】live555源码详解(七):GenericMediaServer、RTSPServer、RTSPClient

    [Live555]live555源码详解系列笔记 继承协作关系图 下面红色表示本博客将要介绍的三个类所在的位置: GenericMediaServer.RTSPServer.RTSPClient 14 ...

  5. 【Live555】live555源码详解(六):FramedSource、RTPSource、RTPSink

    [Live555]live555源码详解系列笔记 继承协作关系图 下面红色表示本博客将要介绍的三个类所在的位置: FramedSource.RTPSource.RTPSink 11.FramedSou ...

  6. 【Live555】live555源码详解(五):MediaSource、MediaSink、MediaSession、MediaSubsession

    [Live555]live555源码详解系列笔记 继承协作关系图 下面红色表示本博客将要介绍的四个类所在的位置: MediaSource.MediaSink.MediaSession.MediaSub ...

  7. 【Live555】live555源码详解(四):Medium媒体基础类

    [Live555]live555源码详解系列笔记 7.Media Medai所依赖关系图 依赖Medai关系图 Media和UsageEnvironment关联图

  8. 【Live555】live555源码详解(二):BasicHashTable、DelayQueue、HandlerSet

    [Live555]live555源码详解系列笔记 3.BasicHashTable 哈希表 协作图: 3.1 BasicHashTable BasicHashTable 继承自 HashTable 重 ...

  9. 【Live555】live555源码详解(一):BasicUsageEnvironment、UsageEnvironment

    [Live555]live555源码详解系列笔记 类关系图 1.UsageEnvironment 详解 1.1 BasicUsageEnvironment BasicUsageEnvironment ...

最新文章

  1. Thinkphp5 开发 OA 办公系统 - 数据库设计
  2. 龙剑服务器为什么总是维修,《龙剑》2014年3月13日更新维护公告
  3. 利用python计算偏差-方差权衡
  4. java虚拟机学习-JVM调优总结-新一代的垃圾回收算法(11)
  5. python 提升效率_@Python 程序员,如何最大化提升编码效率?
  6. 论得失。。。技术方向
  7. 防止sql注入的方法
  8. python3.7安装tensorflow-gpu_tensorflow-gpu安装的常见问题及解决方案
  9. 02 ZooKeeper分布式集群安装
  10. 学生食堂信息管理系统
  11. windows无法格式化u盘_U盘无法格式化的解决方法
  12. 服务器KVM虚拟键盘怎么打开,KVM虚拟机键盘布局问题的解决
  13. RT-thread 环境下使用 HASH hwcrypto 配置使用底层硬件HAH库问题记录
  14. 白蛋白纳米粒|莫西沙星小鼠血清白蛋白MSA纳米粒|利多卡因大鼠血清白蛋白RSA纳米粒
  15. 摄像头拍照及解析QR二维码
  16. apache和nginx对比
  17. 转载:揭秘内容付费的三种商业模式(原作者:小马宋)
  18. Java核心编程(22)
  19. newman执行测试_newman执行postman脚本
  20. java rsi_高频交易算法研发心得--RSI指标及应用

热门文章

  1. 【全套资料.zip下载】数电课设-数字频率计Multisim仿真设计【Multisim仿真+报告+讲解视频.zip下载】
  2. oracle数据库怎么创建数据库 oracle数据库工作流程
  3. 第12章 光盘存储器的格式
  4. 关于大数据思维的一些思考
  5. GDAL中的地理坐标系、投影坐标系及其相互转换
  6. 单例模式及其线程安全问题
  7. R语言GD包基于栅格图像实现地理探测器与连续参数的自动离散化
  8. Java Robocode 以示例wall为基准的一个坦克
  9. 《eNSP - OSPF 查看命令》
  10. html5 序列帧播放器,H5序列图片视频化播放