深入理解Android中Scroller的滚动原理

View的平滑滚动效果

什么是实现View的平滑滚动效果呢,举个简单的例子,一个View从在我们指定的时间内从一个位置滚动到另外一个位置,我们利用Scroller类可以实现匀速滚动,可以先加速后减速,可以先减速后加速等等效果,而不是瞬间的移动的效果,所以Scroller可以帮我们实现很多滑动的效果。

首先我们先来看一下Scroller的用法,基本可概括为“三部曲”:

1、创建一个Scroller对象,一般在View的构造器中创建:

public ScrollViewGroup(Context context) {
  this(context, null);
}

public ScrollViewGroup(Context context, AttributeSet attrs) {
  this(context, attrs, 0);
}

public ScrollViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
  super(context, attrs, defStyleAttr);
  mScroller = new Scroller(context);
}

2、重写View的computeScroll()方法,下面的代码基本是不会变化的:

@Override
public void computeScroll() {
  super.computeScroll();
  if (mScroller.computeScrollOffset()) {
    scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
    postInvalidate();
  }
}

3、调用startScroll()方法,startX和startY为开始滚动的坐标点,dx和dy为对应的偏移量:

mScroller.startScroll (int startX, int startY, int dx, int dy);
invalidate();

上面的三步就是Scroller的基本用法了。

那接下来的任务就是解析Scroller的滚动原理了。

而在这之前,我们还有一件事要办,那就是搞清楚scrollTo()scrollBy()的原理。scrollTo()scrollBy()的区别我这里就不重复叙述了,不懂的可以自行google或百度。

下面贴出scrollTo()的源码:

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();
    }
  }
}

设置好mScrollXmScrollY之后,调用了onScrollChanged(mScrollX, mScrollY, oldX, oldY);  ,View就会被重新绘制。这样就达到了滑动的效果。

下面我们再来看看scrollBy()  :

public void scrollBy(int x, int y) {
  scrollTo(mScrollX + x, mScrollY + y);
}

这样简短的代码相信大家都懂了,原来scrollBy()内部是调用了scrollTo()的。但是scrollTo() / scrollBy()的滚动都是瞬间完成的,怎么样才能实现平滑滚动呢。

不知道大家有没有这样一种想法:如果我们把要滚动的偏移量分成若干份小的偏移量,当然这份量要大。然后用scrollTo() / scrollBy()每次都滚动小份的偏移量。在一定的时间内,不就成了平滑滚动了吗?没错,Scroller正是借助这一原理来实现平滑滚动的。

下面我们就来看看源码吧!

根据“三部曲”中第一部,先来看看Scroller的构造器:

public Scroller(Context context, Interpolator interpolator, boolean flywheel) {
  mFinished = true;
  if (interpolator == null) {
    mInterpolator = new ViscousFluidInterpolator();
  } else {
    mInterpolator = interpolator;
  }
  mPpi = context.getResources().getDisplayMetrics().density * 160.0f;
  mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());
  mFlywheel = flywheel;

  mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning
}

在构造器中做的主要就是指定了插补器,如果没有指定插补器,那么就用默认的ViscousFluidInterpolator

我们再来看看Scroller的startScroll()

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;
}

我们发现,在startScroll()里面并没有开始滚动,而是设置了一堆变量的初始值,那么到底是什么让View开始滚动的?我们应该把目标集中在startScroll()的下一句invalidate();身上。我们可以这样理解:首先在startScroll()设置好了一堆初始值,之后调用了invalidate();让View重新绘制,这里又有一个很重要的点,在draw()中会调用computeScroll()这个方法!

源码太长了,在这里就不贴出来了。想看的童鞋在View类里面搜boolean draw(Canvas canvas, ViewGroup parent, long drawingTime)这个方法就能看到了。通过ViewGroup.drawChild()方法就会调用子View的draw()方法。而在View类里面的computeScroll()是一个空的方法,需要我们去实现:

/**
 * Called by a parent to request that a child update its values for mScrollX
 * and mScrollY if necessary. This will typically be done if the child is
 * animating a scroll using a {@link android.widget.Scroller Scroller}
 * object.
 */
public void computeScroll() {
}

而在上面“三部曲”的第二部中,我们就已经实现了computeScroll()  。首先判断了computeScrollOffset() ,我们来看看相关源码:

/**
 * Call this when you want to know the new location. If it returns true,
 * the animation is not yet finished.
 */
public boolean computeScrollOffset() {
  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;
    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 <= mMaxX
      mCurrX = Math.min(mCurrX, mMaxX);
      mCurrX = Math.max(mCurrX, mMinX);

      mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
      // Pin to mMinY <= mCurrY <= mMaxY
      mCurrY = Math.min(mCurrY, mMaxY);
      mCurrY = Math.max(mCurrY, mMinY);

      if (mCurrX == mFinalX && mCurrY == mFinalY) {
        mFinished = true;
      }

      break;
    }
  }
  else {
    mCurrX = mFinalX;
    mCurrY = mFinalY;
    mFinished = true;
  }
  return true;
}

这个方法的返回值有讲究,若返回true则说明Scroller的滑动没有结束;若返回false说明Scroller的滑动结束了。再来看看内部的代码:先是计算出了已经滑动的时间,若已经滑动的时间小于总滑动的时间,则说明滑动没有结束;不然就说明滑动结束了,设置标记mFinished = true;  。而在滑动未结束里面又分为了两个mode,不过这两个mode都干了差不多的事,大致就是根据刚才的时间timePassed和插补器来计算出该时间点滚动的距离mCurrXmCurrY。也就是上面“三部曲”中第二部的mScroller.getCurrX()  , mScroller.getCurrY()的值。

然后在第二部曲中调用scrollTo()方法滚动到指定点(即上面的mCurrX, mCurrY)。之后又调用了postInvalidate(); ,让View重绘并重新调用computeScroll()以此循环下去,一直到View滚动到指定位置为止,至此Scroller滚动结束。

其实Scroller的原理还是比较通俗易懂的。我们再来理清一下思路,以一张图的形式来终结今天的Scroller解析:

总结

好了,本文介绍Android中Scroller的滚动原理的内容到这就结束了,如果有什么问题可以在下面留言。希望本文的内容对大家开发Android能有所帮助。

时间: 2016-08-16

详解Android应用开发中Scroller类的屏幕滑动功能运用

今天给大家介绍下Android中滑屏功能的一个基本实现过程以及原理初探,最后给大家重点讲解View视图中scrollTo 与scrollBy这两个函数的区别 .   首先 ,我们必须明白在Android View视图是没有边界的,Canvas是没有边界的,只不过我们通过绘制特定的View时对Canvas对象进行了一定的操作,例如 : translate(平移).clipRect(剪切)等,以便达到我们的对该Canvas对象绘制的要求 ,我们可以将这种无边界的视图称为"视图坐标"----

android使用 ScrollerView 实现 可上下滚动的分类栏实例

如果不考虑更深层的性能问题,我个人认为ScrollerView还是很好用的.而且单用ScrollerView就可以实现分类型的RecyclerView或ListView所能实现的效果. 下面我单单从效果展示方面考虑,使用ScrollerView实现如下图所示的可滚动的多条目分类,只是为了跟大家一起分享一下新思路.(平时:若从复用性等方面考虑,这显然是存在瑕疵的~) 特点描述: 1.可上下滚动 2.有类似于网格布局的样式 3.子条目具有点击事件 刚看到这个效果时,首先想到的是使用分类型的Recyc

详解Android Scroller与computeScroll的调用机制关系

Android ViewGroup中的Scroller与computeScroll的有什么关系? 答:没有直接的关系 知道了答案,是不是意味着下文就没必要看了,如果说对ViewGroup自定义控件不感兴趣,可以不用看了. 1.Scroller到底是什么? 答:Scroller只是个计算器,提供插值计算,让滚动过程具有动画属性,但它并不是UI,也不是滑动辅助UI运动,反而是单纯地为滑动提供计算. 无论从构造方法还是其他方法,以及Scroller的属性可知,其并不会持有View,辅助ViewGrou

Android Scroller完全解析

在Android中,任何一个控件都是可以滚动的,因为在View类当中有scrollTo()和scrollBy()这两个方法,如下图所示: 这两个方法的主要作用是将View/ViewGroup移至指定的坐标中,并且将偏移量保存起来.另外: mScrollX 代表X轴方向的偏移坐标 mScrollY 代表Y轴方向的偏移坐标 这两个方法都是用于对View进行滚动的,那么它们之间有什么区别呢?简单点讲,scrollBy()方法是让View相对于当前的位置滚动某段距离,而scrollTo()方法则是让Vi

Android程序开发之UIScrollerView里有两个tableView

一,效果图. 二,工程图. 三,代码. RootViewController.h #import <UIKit/UIKit.h> @interface RootViewController : UIViewController <UIScrollViewDelegate,UITableViewDelegate,UITableViewDataSource> { UIScrollView *_scrolView; UITableView *_tableView; UITableView

Android Scroller及下拉刷新组件原理解析

Android事件拦截机制 Android中事件的传递和拦截和View树结构是相关联的,在View树中,分为叶子节点和普通节点,普通节点有子节点只能是ViewGroup,叶子节点可以是View或者ViewGroup.Android和事件分发拦截相关的方法有 dispatchTouchEvent(MotionEvent ev) 事件分发相关的方法,沿着View树将一个用户的触摸事件向下分发. onInterceptTouchEvent(MotionEvent ev) 在dispatchTouchE

Android Scroller大揭秘

在学习使用Scroller之前,需要明白scrollTo().scrollBy()方法. 一.View的scrollTo().scrollBy() scrollTo.scrollBy方法是View中的,因此任何的View都可以通过这两种方法进行移动.首先要明白的是,scrollTo.scrollBy滑动的是View中的内容(而且还是整体滑动),而不是View本身.我们的滑动控件如SrollView可以限定宽.高大小,以及在布局中的位置,但是滑动控件中的内容(或者里面的childView)可以是无

PHP生成条形码大揭秘

1.什么是条形码? 百度百科定义:条形码(barcode)是将宽度不等的多个黑条和空白,按照一定的编码规则排列,用以表达一组信息的图形标识符.常见的条形码是由反射率相差很大的黑条(简称条)和白条(简称空)排成平行线的图案.在日常生活中,条形码可以标出物品的生产国.制造厂家.商品名称.生产日期.图书分类号.邮件地点起止.类别.日期等许多信息.条形码编码格式具体请参考 打印出来的优惠券,商家需要用验证器读取条形码,来获得其有效性. 2.如何生成条形码? 首先找到强大的开源资料,在barcode官网下

IOS安装包比Android容量大的原因

昨天ios的硕士实习小伙伴,咳咳.在疑惑这个问题,于是就各种找资料.最后只找到两个权威的链接,根据这两个权威的链接,整理如下: ios的app包含所有版本. 在ios9之前,应用程序的所有版本我们都进行了下载和安装.这意味着不管你的手机需要app的什么图片和资源,所有屏幕尺寸的图片和资源都会被下载. 想想那是多少: Non retina 3.5" Retina 3.5" Retina 4", 4.7", 5.5" Non retina iPad full

Unicode编码大揭秘

如果你是一个生活在2003年的程序员,却不了解字符.字符集.编码和Unicode这些基础知识.那你可要小心了,要是被我抓到你,我会让你在潜水艇里剥六个月洋葱来惩罚你. 这个邪恶的恐吓是Joel Spolsky在十年前首次发出的.不幸的是,很多人认为他只是在开玩笑,因此,现在仍有许多人不能完全理解Unicode,以及Unicode.UTF-8.UTF-16之间的区别.这就是我写这篇文章的原因. 言归正传,设想在一个晴朗的下午,你收到一封电子邮件,它来自一个你高中之后就失去联系的朋友,并带有一个tx

木马静态变动态 DLL木马程序大揭秘

相信经常玩木马的朋友们都会知道一些木马的特性,也会有自己最喜爱的木马,不过,很多朋友依然不知道近年兴起的"DLL木马"为何物.什么是"DLL木马"呢?它与一般的木马有什么不同? 一.从DLL技术说起 要了解DLL木马,就必须知道这个"DLL"是什么意思,所以,让我们追溯到几年前,DOS系统大行其道的日子里.在那时候,写程序是一件繁琐的事情,因为每个程序的代码都是独立的,有时候为了实现一个功能,就要为此写很多代码,后来随着编程技术发展,程序员们把很

克隆系统 Ghost常用参数大揭秘

用过DOS的人对参数并不陌生,DOS下的很多程序都有参数,尽管是枯燥的英文字母,但功能却非常强大.Ghost是一个典型的支持参数的DOS程序,充分利用它的参数,我们可以更好地控制Ghost.让它们更好地为我们工作,前面几个例子,我们就使用了Ghost的参数做出了一张自动备份和恢复硬盘数据的自启动光盘.正是因为Ghost参数众多,功能强大,我们才有必要把一些最最常用的参数列出,供大家平时参考使用. 小提示 ★参数(Parameter)是程序提供给我们一些隐藏选项,通过添加参数,可以实现正常启动程序

很详细的Ghost所有运行错误代码完全大揭秘

原因或对策:不正确的路径/文件语法.请确保路径及文件名是否正确,同时确定如果你正在网络上建立映像文件你是否有写权限. 错误代码: 10001 原因或对策:使用者放弃了操作. 错误代码: 10060 原因或对策:读取了坏的来源文件或磁盘.检查磁盘或映像文件是否有问题,网络是否有冲突,光驱是否有了问题. 错误代码: 10082 原因或对策:Ghost的共享版本已过期,必须购买才能够使用. 错误代码: 10170 原因或对策:拒绝检查映像文件或磁盘.请使用更新的版本以解决此问题. 错误代码: 1018