站在源码的肩膀上全解Scroller工作机制

点击上方“程序员大咖”,选择“置顶公众号”

关键时刻,第一时间送达!


前言


在Android开发中有多种方式实现View的滑动,常见的有三种如下:


  1. 不断地修改View的LayoutParams

  2. 采用动画向View施加位移效果

  3. 调用View的scrollTo( )、scrollBy( )


前两种方式我们还是挺熟悉的,不但见得挺多的而且还经常使用;至于最后一种方式,可能就要相对陌生些了。


其实,在Android中我们常见到的ListView、Launcher、SlidingMenu、ViewPager等等这些具有弹性滑动的View的背后都隐藏着一个机智又乖巧的小精灵——Scroller。这些控件的使用场景和作用各不相同,但在它们的内部均广泛又深刻地使用了Scroller的scrollTo()和scrollBy(),如此的实现不但丰富了操作方式而且极大提升了用户体验。


在此,我们从源码到实例,由简单到复杂,从表象到机制,一步步走进既陌生却又有点熟悉的Scroller


scrollTo( )和scrollBy( )


在View的源码中,系统提供了scrollTo()和scrollBy()这两个方法用于实现View的滚动。这两个方法又有什么联系呢,我们先来瞅瞅scrollTo()的源码:




scrollTo()是实现View滚动的核心,调用该方法使得View相对于其初始位置滚动某段距离。在该方法内部将输入参数x,y分别赋值给用于表示View在X方向滚动距离的mScrollX和表示View在Y方向滚动距离的mScrollY,然后调用onScrollChanged()并且刷新重绘View。在后续的操作中调用view.getScrollX()或view.getScrollY()可以很容易地得到mScrollX和mScrollY,关于这两个值我们再看看源码是怎么说的。


关于mScrollX,官方文档描述如下:




关于mScrollY,官方文档描述如下:



mScrollX和mScrollY用于描述View的内容在水平方向或垂直方向滚动的距离。


什么是View的内容呢?比如,对于一个TextView而言,文本就是它的内容;对于一个ViewGroup而言,子View就是它的内容。


故在此,我们请务必注意:scrollTo()和scrollBy()滚动的是View的内容,而不是将View做整体的移动。


嗯哼,继续看scrollBy()的源码:




哇哈,看到了吧:scrollBy()的源码非常简洁,它仅仅是再次调用了scrollTo()。


直白地说:它只是把输入参数x,y累加到了mScrollX和mScrollY上而已。


所以,scrollBy()方法是在mScrollX和mScrollY的基础上滚动的。


小结:


  1. mScrollX和mScrollY分别表示View在X、Y方向的滚动距离。

  2. scrollTo( )表示View相对于其初始位置滚动某段距离。
    由于View的初始位置是不变的,所以如果利用相同输入参数多次调用scrollTo()方法,View只会出现一次滚动的效果而不是多次。

  3. scrollBy( )表示在mScrollX和mScrollY的基础上继续滚动。


现在,已经对这两个方法有了基本的了解,我们再看看它们的用法。




在这个示例中对TextView分别调用scrollTo( )和scrollBy( ),代码如下:




当我们调用scrollBy()时,TextView的中的文本逐渐往其左侧滚动,当执行scrollTo()时TextView的中的文本会滚动到其右侧。嗯哼,在这是不是又印证了我们刚才的描述呢:执行scrollTo()和scrollBy()后View的内容发生了滚动,但是View本身是没有发生移动的。关于这点已经得到了验证,但是View的内容滚动的方向怎么和我们预想的不一样呢?平常我们不是说坐标是左负右正,上负下正么,为什么这里执行mTextView.scrollBy(30,0)时TextView的文本却是往X的负轴移动呢?


其实,许多人都是有类似的疑问,现在我们一起来探究其产生的原因。


在scrollTo()的源码中我们看到,该方法最后会调用postInvalidateOnAnimation()对View进行重绘从而执行到invalidate()。在此以Android 6.0 API Level 23为例,对其进行剖析:




嗯哼,看到第4行代码的时候,是不是就恍然大悟了呢?


在进行重绘的时候在会利用l - scrollX, t - scrollY, r - scrollX, b - scrollY计算出新的l,t,r,b。


如果在调用scrollTo()和scrollBy()时传入的x,y为正值,那么新的l,t,r,b均会变小,从而导致View的内容向左且向上滚动。


如果在调用scrollTo()和scrollBy()时传入的x,y为负值,那么新的l,t,r,b均会变大,从而导致View的内容向右且向下滚动。


刚才我们通过scrollTo()和scrollBy()作用于某个View,如果要想让多个View同时发生滚动,可以怎么办呢?很简单,只需要把这些View放到同一个ViewGroup中然后再调用这两个方法即可,例如mLinearLayout.scrollBy(50, 0)、mLinearLayout.scrollTo(100, 20)


Scroller原理解析


其实,在实际的开发中我们真正地使用scrollTo()和scrollBy()来实现View的滑动的时候并不多。因为这两个方法产生的滑动是不连贯的,跳跃的,闪烁的,最终的效果也不够平滑。所以,我们多采用系统提供的工具类Scroller来实现View的滚动效果。


关于Scroller的使用我们来瞅瞅Android官方的文档:




Scroller类封装了滑动操作,常用于实现View的平滑滚动并且可使用插值器(Interpolator)设定先加速后减速,先减速后加速等等滑动效果。


依据以上文档的描述可把Scroller的使用概括为以下五个主要步骤:


  1. 初始化Scroller

  2. 调用startScroll()开始滚动

  3. 执行invalidate()刷新界面

  4. 重写View的computeScroll()并在其内部实现与滚动相关的业务逻辑

  5. 再次执行invalidate()刷新界面


现在我们就按照这五个步骤来实现一个小例子:利用Scroller让图片发生滑动。




先来看一下它的布局




该布局文件不算复杂,主要的是在自定义的ViewGroup中放入了一个ImageView。既然这样,那就继续来看LinearLayoutSubClass的实现:



至此,该示例的主要代码就已经编写完成了,我们只需要在Activity中执行mLinearLayoutSubClass.beginScroll()就可以让图片发生滚动了。


好了,我们依据官方的文档照猫画虎地完成了这个示例,其余的”虎”也和这是非常类似的了。但是,这个Scroller到底是如何让View滚动起来的?为了探个究竟,我们结合文档对刚才的示例做一个详尽的分析。


  • 第一步:初始化Scroller

    在初始化Scroller时可为其设置一个Interpolator,如果没有设置那么系统会采用默认的插值器ViscousFluidInterpolator


  • 第二步:调用startScroll()开始滚动



其实,当看到如上的startScroll()源码时我们才发现这个方法也没有让一个View发生滚动,它不过是在给一些字段赋值罢了,比如:动画的开始时间,滑动的开始位置,滑动的距离,滑动的持续时间等等。


  • 第三步:执行invalidate()刷新界面


    在调用invalidate()后会导致View的重绘从而调用computeScroll()


  • 第四步:重写View的computeScroll()并在其内部实现与滚动相关的业务逻辑

    关于该方法的作用,我们还是先来看看源码:




咦,这是个空方法!这就是说我们需要根据自己的业务逻辑重写该方法。其实,该方法的注释已经告诉我们了:如果使用Scroller使得View发生滚动,那么可以在该方法中处理与滑动相关的业务和数据,比如调用scrollTo()或者scrollBy()使得View发生滚动;比如获取变量mScrollX、mScrollY、mCurrX、mCurrY的值。在此有一点需要注意,在处理这些业务和数据之前我们通常需要先利用computeScrollOffset()判断一下滑动是否停止然后再进行相关操作。


  • 第五步:再次执行invalidate()刷新界面

    在处理完与滑动相关的业务和数据后,再次调用invalidate()刷新界面。既然刷新了界面,那么又将导致View的重绘,故又将调用到第四步的computeScroll()方法。所以只要View的滚动没有完成或者未被人为的终止,那么第四步和第五步会一直循环进行。


嗯哼,现在明白了么:Scroller是怎么让View滚动起来的呢?


  1. 利用startScroll()指定了与滑动密切相关的东西,比如时间,距离,Interpolator等。有了这些东西我们是不是就可以知道在某个时刻滑动的偏移量和具体的位置了呢?

  2. 在computeScroll()中处理与滑动相关的业务逻辑及其数据。最常见的操作为利用scrollTo实现View的滚动。


scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();


这里获取到的mCurrX、mCurrY就是系统依据startScroll()中设置的时间,距离,Interpolator计算出了当前View的滚动数据。比如,你告诉我:你家1000米外有个大保健,你计划用5分钟匀速地走到那里。但是你出门后,我发现你忘记带钱包了,于是我就根据你刚才告知我的信息计算出你现在所处的位置,然后用大姜无人机给你送过去就行了。


3.调用invalidate()刷新界面,从而再次回到computeScroll(),回到在computeScroll()继续处理滑动事件。假如View的滑动已经停止了那就没有必要再次执行invalidate()了。

4.说到底,不是Scroller让View发生了滚动而是View自己在滚动。只不过在这个过程中Scroller在不停地追踪View的滚动,而且提供了许多的辅助而已,比如:可以提供偏移量,耗时,当前位置等等信息。


结合以上的分析,我们再用一张图来梳理一下整个流程。





Scroller应用示例

在了解了Scroll的工作机制之后,我们来看看Scroller的实际应用。


在拉动页面后松手,页面弹回到原来的位置。
我们来一起分析该效果的代码实现。



结合之前对于Scroller的详细分析,我们可以很容易地梳理出该示例的主要的实现步骤:


  1. 在ACTION_UP时将布局还原到初始位置,请参见第37行代码

  2. 除ACTION_UP以外的事件均由GestureDetector处理,请参见第40行代码

  3. 在GestureDetector的onScroll()处理View的滚动,请参见第64-66行代码

  4. 重写computeScroll()中实现View的滚动,请参见第24-31行代码


在完成了上面的这个例子,我们再来瞅瞅用Scroller实现一个简易版的ViewPager




我们来看一下它的具体实现



在该示例中使用到了自定义View,其中关于onMeasure()和onLayout()以及Touch事件的拦截和分发不再详细分析。

好了,看完效果我们一起来整理这个示例中的注意事项和关键步骤。


  1. 处理滑动的越界:一般情况下ViewPager只是展示几张图片而已,所以要确定其左右边界;请参见代码第61-62行

  2. 在onInterceptTouchEvent()中拦截Touch事件:当发生ACTION_MOVE事件时,假若在X方向滑动的距离大于了系统的TouchSlop则拦截Touch事件将其截留下来由ViewGroup的onTouchEvent()处理;请参见代码第73-80行

  3. 在onTouchEvent中注意滑动越界的处理:当发生ACTION_MOVE事件时,不论是向左滑还是向右滑,都要防止滑动越界;请参见代码第97-105行

  4. 在onTouchEvent中合理处理ACTION_UP:当手指抬起时(ACTION_UP)需要判断当前应该滚动到哪一个页。比如,滑动的距离超过了屏幕宽度的一半那么就需要翻页,否则回滚到之前的页面;请参见代码第109-115行

  5. 重写computeScroll( ):在该方法中实现View的滚动。当然,在scrollTo()不要忘记刷新View;请参见代码第29-37行


后语


在这篇博客中,我们从scrollTo()和scrollBy()入手,从源码角度分析了View的滚动的具体实现;并在此基础上对Scroller的工作机制作了完整的介绍和梳理。文中的几个示例力求以最简洁的代码阐明其背后的稍显晦涩的缘由,以达深刻认识之目的。

望小伙伴们在读完此篇博客之后,有所思,有所悟,有所获。



  • 来自CSDN-谷哥的小弟

  • http://blog.csdn.net/lfdfhl/article/details/53143114

  • 程序员大咖整理发布,转载请联系作者获得授权

【点击成为Python大神】

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值