上一篇:scrollTo与scrollBy用法以及TouchSlop与VelocityTracker解析
通过上一篇内容对scrollTo与scrollBy用法以及TouchSlop与VelocityTracker解析后,接下来我们再来聊聊scroller类,首先最好要有上一篇内容作为基础(建议先看看上篇的内容),才能比较容易理解本篇内容。以下为本篇内容概要:
1.scroller类的工作原理以及使用方法
在《朱子语录》中写着一句我们都耳熟能详的语句:“知其然知其所以然”。而我本身也很喜欢这样的学习方式,相信大家也都一样哈。所以呢,首先我们来搞明白为什么需要scroller类呢?这个类是android系统为我们提供的平滑过渡类,而在上一篇中我们介绍到了scrollerTo()与scrollBy()方法,通过分析我们也知道了这两个方法是可以实现view的滑动效果,而且最后我们也给出了一个view滑动的实例,既然scrollerTo()与scrollBy()方法也可实现滑动,哪为什么还要scroller类呢?事出必有因,其实事情的经过是这样的,虽然scrollerTo()与scrollBy()方法可实现view的滑动效果,但是却有一个很大的缺陷,这种滑动操作是瞬间完成的,就是说你为scrollTo提供终点坐标,该方法只要一调用,我们就会发现瞬间滚动到目的地了,显然这种方式用户体验是十分不友好滴,因此scroller类的出现就是为了解决这个问题滴。
弄清楚了Scroller类的作用后,我们再聊聊scroller是如何实现平滑过渡滴呢?我们在这里先做一下简要概括(要脱衣服了,是不是很激动,突然想起费玉清的少女团,嘿嘿嘿........),Scroller类本身并没有实现view的滑动,它实际上需要配合View的computeScroller方法才能完成弹性滑动的效果,它不断地让View重绘,而每一次重绘距滑动的起始时间会有一个时间间隔,通过这个时间间隔Scroller类就可以得出View当前滑动的位置,知道了滑动位置就可以通过scrollTo()方法来完成View的滑动(这里也说明view的滑动最终还是要靠scrollTo()方法来实现,Scroller类本身本身也只是个辅助类而已),就这样,View每一次重绘都会导致View进行小幅度的滑动,而多次小幅度滑动的就组成了弹性滑动,这就是Scroller类的工作原理。关于前面提到的方法,不理解不要紧,后面都会一一分析,我们只需要先大概了解一下Scroller类整个工作流程与原理。
哎啊,玛呀!你在说啥?好吧,我表达能力太差劲了!下面进行将功补过,请包涵!
我们在这里举个简单例子。
快过年的,大家,车票都买好了嘛?不好意思,搓到大家的痛处,毕竟大天朝,总是如此美好!回家的我们选择的交通工具总有不同,有的人选择开车,有的人选择高铁,有的人选择飞机,比如我的老家-广东,现在我在北京,也就是北京-广东广州(假设路程为2000km),于是出现了如下的情况:
坐飞机,真是泥玛快啊,3个小时就到了,一觉都还没睡醒呢,而这趟路程只用了3个小时,是不是太过快了,我风景都没看呢?如果我们每一个小时调用一次scrollto(),相当于每小时跑2000/3=666km,感觉沿途风景闪得太快啊。不行呀,没看清楚就过了,这个不够平滑。
坐高铁,沿路风景不错嘛!,可以看8-10个小时啊!还是一样,我们每个1小时调用一次scrollTo(),也就是每小时2000/10=200km,速度刚刚好,没有错过好风景,不错,不错,是不是很平滑呢!因为分段分得太
开车,呵呵,没钱没房没车,说个毛线。
到这里大家是不是有点感觉了?是滴,scroller类确实就是这样工作的,每隔一段小时时间重绘一次view(去调用scrollerTo()滑动),从而导致View进行了小幅度的滑动,次数多了,也就形成了平滑过渡,这样也方便我们在沿途看风景,难道不是很好么?
好吧,说了这么多,相信大家对于为什么要使用 Scroller类以及Scroller类的工作原理有一定的了解了。那么下面,我们就一起来研究研究Scroller类的具体用法。
先看看Scroller类几个比较重要的方法:
Scroller.startScroll(int startX, int startY, int dx, int dy, int duration)
参数解析:
startX:滑动的起始x轴坐标值
startY:滑动的起始y轴坐标值
dx:滑动的结束x轴坐标值
dy:滑动的结束y轴坐标值
duration:滑动的执行时间
这个方法主要作用是提供一个开始滑动的坐标点以及滑动结束的位置和执行时间,提供给Scroller类内容计算使用。也可以理解为(startX , startY)在duration时间内前进(dx,dy)个单位,到达坐标为(startX+dx , startY+dy)处。其中duration还与Scroller.computeScrollOffset()方法有关,这个方法内部会根据duration去判断滑动是否已经结束。如果结束,Scroller.computeScrollOffset()返回false,未结束返回true。
Scroller.computeScrollOffset()
这个方法没有任何参数。这个作用就是用于判断移动过程是否已经完成,完成返回false,还未完成返回true。而我们在使用过程中会根据该值去判断是否继续调用scrollTo()方法进行小幅度滑动。
下面再来介绍一个重量级的方法(请注意这个方法是在 View类里面的):
/** * 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() { }很明显,空方法,所以这个方法肯定需要我们自己去实现。这个方法作用是什么呢?根据文档解释当invalidate()或postInvalidate()都会导致这个方法的执行。也就是说view重绘时都会导致这个方法执行,还记得我们前面分析的Scroller类的工作原理么? 弹性滑动的实现是通过 View每一次重绘进而导致View进行小幅度的滑动,而多次小幅度滑动的就组成了弹性滑动。因此我们思考一下,可不可以在computeScroll()方法中调用invalidate()或postInvalidate()来实现这样的小幅度滑动场景呢?那么现在问题又来如果这样做了,又该如何跳出这个死循环?Scroller.computeScrollOffset()还记得吧,刚分析过的,没错,我们确实要通过Scroller.computeScrollOffset()来判断是否跳出该循环。因此有了如下代码:
/** * 覆写computeScroll * 在computeScroll()方法中 * 在View的源码中可以看到public void computeScroll(){}是一个空方法. * 具体的实现需要自己来写.在该方法中我们可调用scrollTo()或scrollBy() * 来实现移动(动画).该方法才是实现移动的核心. * 4.1 利用Scroller的mScroller.computeScrollOffset()判断移动过程是否完成 * 注意:该方法是Scroller中的方法而不是View中的!!!!!! * public boolean computeScrollOffset(){ } * Call this when you want to know the new location. * If it returns true,the animation is not yet finished. * loc will be altered to provide the new location. * 返回true时表示还移动还没有完成. * 4.2 若动画没有结束,则调用:scrollTo(By)(); * 使其滑动scrolling * * 5 再次调用invalidate()或者postInvalidate();. * 调用invalidate()方法那么又会重绘View树. * 从而跳转到第3步,如此循环,便形成了动画移动的效果,直到computeScrollOffset返回false * * * invalidate()与postInvalidate() 区别: * Invalidate the whole view. If the view is visible, * {@link #onDraw(android.graphics.Canvas)} will be called at some point in * the future. * <p> * This must be called from a UI thread. To call from a non-UI thread, call * {@link #postInvalidate()}. * 这段话的意思是: * invalidate()只能在UI线程调用而不能在非UI线程调用 * postInvalidate() 可以在非UI线程调用也可以在UI线程调用 */ @Override public void computeScroll() { if(scroller.computeScrollOffset()){ scrollTo(scroller.getCurrX(),scroller.getCurrY()); //重绘 postInvalidate(); } }
就这几句话?别惊讶,就这么简单。postInvalidate执行后,会去调computeScroll 方法,而这个方法里再去调postInvalidate,这样就可以不断地去调用scrollTo方法了,直到Scroller动画结束,当然第一次时,我们需要手动去调用一次postInvalidate才会去调用 。下面我们用实战案例来实现上述分析Scroller的用法
2.scroller类实战案例
package com.zejian.scrollerapp.view; import android.content.Context; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.widget.Scroller; /* * Created by zejian * Time 16/1/20 下午4:05 * Email [email protected] * Description: */ public class ScrollerViewGroup extends ViewGroup { private Scroller scroller;// 滑动控制 private Context context; private VelocityTracker mVelocityTracker; // 用于判断甩动手势 private static final int SNAP_VELOCITY = 500; // X轴速度基值,大于该值时进行切换 private int mCurScreen; // 当前页面为第几屏 private float mLastMotionX;// 记住上次触摸屏的位置 private int deltaX; public ScrollerViewGroup(Context context) { this(context, null); } public ScrollerViewGroup(Context context, AttributeSet attrs) { this(context, attrs, 0); } public ScrollerViewGroup(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } /** * 初始操作 */ private void init(Context context) { this.context=context; this.scroller=new Scroller(context); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); // 设置该ViewGroup的大小 int width = MeasureSpec.getSize(widthMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); //在onMeasure(int, int)中,必须调用setMeasuredDimension(int width, int height) // 来存储测量得到的宽度和高度值,如果没有这么去做,下文使getMeasuredHeight(), // 会触发异常IllegalStateException。 setMeasuredDimension(width, height); int count = getChildCount(); for (int i = 0; i < count; i++) { //测量子类宽高,方法内部通过childView的 measure(newWidthMeasureSpec, heightMeasureSpec) // 函数将子view获取到到宽高,存储到childView中,以便childView的getMeasuredWidth() // 和getMeasuredHeight() 的值可以被后续工作得到。 measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec); } scrollTo(mCurScreen * width, 0);// 移动到第一页位置 } /** * 覆写computeScroll * 在computeScroll()方法中 * 在View的源码中可以看到public void computeScroll(){}是一个空方法. * 具体的实现需要自己来写.在该方法中我们可调用scrollTo()或scrollBy() * 来实现移动(动画).该方法才是实现移动的核心. * 4.1 利用Scroller的mScroller.computeScrollOffset()判断移动过程是否完成 * 注意:该方法是Scroller中的方法而不是View中的!!!!!! * public boolean computeScrollOffset(){ } * Call this when you want to know the new location. * If it returns true,the animation is not yet finished. * loc will be altered to provide the new location. * 返回true时表示还移动还没有完成. * 4.2 若动画没有结束,则调用:scrollTo(By)(); * 使其滑动scrolling * * 5 再次调用invalidate()或者postInvalidate();. * 调用invalidate()方法那么又会重绘View树. * 从而跳转到第3步,如此循环,便形成了动画移动的效果,直到computeScrollOffset返回false * * * invalidate()与postInvalidate() 区别: * Invalidate the whole view. If the view is visible, * {@link #onDraw(android.graphics.Canvas)} will be called at some point in * the future. * <p> * This must be called from a UI thread. To call from a non-UI thread, call * {@link #postInvalidate()}. * 这段话的意思是: * invalidate()只能在UI线程调用而不能在非UI线程调用 * postInvalidate() 可以在非UI线程调用也可以在UI线程调用 */ @Override public void computeScroll() { if(scroller.computeScrollOffset()){ scrollTo(scroller.getCurrX(),scroller.getCurrY()); //重绘 postInvalidate(); } } /** * 布局子view * @param changed * @param l * @param t * @param r * @param b */ @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int margeLeft = 0; for (int i=0;i<getChildCount();i++){ View view =getChildAt(i); if(view.getVisibility()!=View.GONE){ int childWidth = view.getMeasuredWidth(); //横排排列 view.layout(margeLeft, 0, margeLeft + childWidth, view.getMeasuredHeight()); //累计初始值 margeLeft +=childWidth; } } } @Override public boolean onTouchEvent(MotionEvent event) { int action = event.getAction(); float x = event.getX(); switch (action) { case MotionEvent.ACTION_DOWN: //收集速率追踪点数据 obtainVelocityTracker(event); //如果屏幕的动画还没结束,你就按下了,我们就结束该动画 if (!scroller.isFinished()) { scroller.abortAnimation(); } //记录按下时的X轴坐标 mLastMotionX = x; break; case MotionEvent.ACTION_MOVE: //计算两次间手指移动距离 deltaX = (int) (mLastMotionX - x); obtainVelocityTracker(event); //记录最好一次移动的x轴坐标 mLastMotionX = x; // 正向或者负向移动,屏幕跟随手指移动 scrollBy(deltaX, 0); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: // 当手指离开屏幕时,记录下mVelocityTracker的记录,并取得X轴滑动速度 obtainVelocityTracker(event); /** * computeCurrentVelocity (int units) * computeCurrentVelocity (int units, float maxVelocity): * 基于你所收集到的点计算当前的速率,当你确定要获得速率信息的时候,在调用该方法, * 因为使用它需要消耗很大的性能。然后,你可以通过getXVelocity() * 和getYVelocity()获得横向和竖向的速率。 * * 参数:units 你想要指定的得到的速度单位,如果值为1,代表1毫秒运动了多少像素。 * 如果值为1000,代表1秒内运动了多少像素。 * * 参数:maxVelocity 该方法所能得到的最大速度,这个速度必须和你指定的units使用同样的单位,而且 * 必须是整数。(也就是,你指定一个速度的最大值,如果计算超过这个最大值, * 就使用这个最大值,否则,使用计算的的结果) */ mVelocityTracker.computeCurrentVelocity(1000); float velocityX = mVelocityTracker.getXVelocity(); float velocityY=mVelocityTracker.getXVelocity(); // 当X轴滑动速度大于SNAP_VELOCITY // velocityX为正值说明手指向右滑动,为负值说明手指向左滑动 if (velocityX > SNAP_VELOCITY && mCurScreen > 0) { // Fling enough to move left snapToScreen(mCurScreen - 1); } else if (velocityX < -SNAP_VELOCITY && mCurScreen < getChildCount() - 1) { // Fling enough to move right snapToScreen(mCurScreen + 1); } else { snapToDestination();//弹性滑动 } //释放资源 releaseVelocityTracker(); break; } // super.onTouchEvent(event); return true;// 返回true,不然只接受down } /** * 使屏幕移动到第whichScreen+1屏 * * @param whichScreen */ public void snapToScreen(int whichScreen) { int scrollX = getScrollX(); /** * 调用snapToDestination()时,判断,避免出界。 */ if(whichScreen > getChildCount() - 1) { whichScreen = getChildCount() - 1; }else if(whichScreen <0){ whichScreen=0; } // if (scrollX != (whichScreen * getWidth())) { int delta = whichScreen * getWidth() - scrollX; scroller.startScroll(scrollX, 0, delta, 0, 500); mCurScreen = whichScreen; //手动调用重绘 // invalidate(); postInvalidate(); } } /** * 当不需要滑动时,会调用该方法,弹性滑动效果 * 条件: * 滑动超过当前view的宽度一半,则滑动到下一个界面 * 滑动未超过当前view的宽度一半,则停留在原来界面 */ private void snapToDestination() { //获取当前view宽度 int screenWidth = getWidth(); //计算是否滑动到一个界面或者停留在原来的界面 int whichScreen = (getScrollX() + (screenWidth / 2)) / screenWidth; snapToScreen(whichScreen); } /** * VelocityTracker帮助你追踪一个touch事件(flinging事件和其他手势事件)的速率。 * 当你要跟踪一个touch事件的时候,使用obtain()方法得到这个类的实例, * 然后用addMovement(MotionEvent)函数将你接受到的Motion event * 加入到VelocityTracker类实例中。当你使用到速率时, * 使用computeCurrentVelocity(int) * 初始化速率的单位,并获得当前的事件的速率,然后使用getXVelocity() * 或getXVelocity()获得横向和竖向的速率 * @param event */ private void obtainVelocityTracker(MotionEvent event) { /** * obtain()的方法介绍 * 得到一个速率追踪者对象去检测一个事件的速率。确认在完成的时候调用recycle()方法。 * 一般情况下,你只要维持一个活动的速率追踪者对象去追踪一个事件,那么,这个速率追踪者 * 可以在别的地方重复使用。 */ if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(event); } /** * 使用完VelocityTracker,必须释放资源 */ private void releaseVelocityTracker() { if (mVelocityTracker != null) { mVelocityTracker.clear(); mVelocityTracker.recycle(); mVelocityTracker = null; } } }代码不多,也不难,注释也相当明了。这里主要说一下思路:这是一个自定义ViewGroup实现了类似ViewPager的滑动效果,在这里我们是通过3个linearLayout实现的,布局文件如下:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:showIn="@layout/activity_main"> <com.zejian.scrollerapp.view.ScrollerViewGroup android:id="@+id/screenParent" android:layout_width="match_parent" android:layout_height="match_parent" > <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/colorAccent" > </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:background="#8BC24D" > </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/colorPrimaryDark" > </LinearLayout> </com.zejian.scrollerapp.view.ScrollerViewGroup> </RelativeLayout>
在自定义ViewGroup,首先在onMeasure()进行测量自身的宽高和子类的宽高,然后在onLayout()中进行布局,等等,在onLayout()方法中好像有点不对劲,布局都可以布到屏幕外?确实可以,实际上Android View视图是没有边界的,Canvas是没有边界的,只不过我们通过绘制特定的View时对Canvas对象进行了一定的操作罢了(translate(平移)、clipRect(剪切)),因此View视图是不受物理屏幕限制。通常我们所定义的Layout布局文件只是该视图的显示区域罢了,其他超过了这个显示区域将不能显示到屏幕中。
同时在自定义ViewGroup中我们还使用到了VelocityTracker类,在上一篇内容中我们分析过这个类,VelocityTracker主要是用来计算手指触摸速度的,因此在这里我们该类来计算手指滑动的速度,只有滑动速度>SNAP_VELOCITY=500时,我们才认为滑动操作发生了,否则不响应。满足响应条件的话,我们再通过snapToScreen(int whichScreen)来滑动屏幕,其实在个方法中我们就调用了scroller.startScroll(scrollX, 0, delta, 0, 500),把滑动所需要的参数传递给了scroller类,同时手动调用了postInvalidate()方法,此时ViewGroup就会开始重绘了,也就是说postInvalidate执行后,会去调computeScroll 方法,而这个方法里再去调postInvalidate,这样就可以不断地去调用scrollTo方法,直到动画结束,也就实现弹性滑动效果。下面给出效果图:
3.scroller类的源码分析
既然是分析源码,那就先来了解一下Scroller类中的一些比较重要的变量与常量:
public class Scroller { //动画加减速器,accelerated(加速),decelerated(减速),repeated(重复)... private final Interpolator mInterpolator; //滑动模式:SCROLL_MODE和FLING_MODE private int mMode; private int mStartX;//滑动起始坐标点,X轴方向 private int mStartY;//滑动起始坐标点,Y轴方向 private int mFinalX;//滑动的最终位置,X轴方向 private int mFinalY;//滑动的最终位置,Y轴方向 private int mMinX; private int mMaxX; private int mMinY; private int mMaxY; //当前X轴坐标点,即调用startScroll函数后,经过一定时间所达到的值 private int mCurrX; //当前Y轴坐标点,即调用startScroll函数后,经过一定时间所达到的值 private int mCurrY; private long mStartTime;//开始滑动的时间 private int mDuration;//滑动所需总时间 private float mDurationReciprocal;//滑动所需总时间的倒数 private float mDeltaX;//X轴方向还需要继续滑动的距离 private float mDeltaY;//Y轴方向还需要继续滑动的距离 //主要用于判断是否已经完成本次滑动操作,true表示本次滑动已完成 private boolean mFinished; private boolean mFlywheel; private float mVelocity; private float mCurrVelocity; private int mDistance; private float mFlingFriction = ViewConfiguration.getScrollFriction(); private static final int DEFAULT_DURATION = 250; private static final int SCROLL_MODE = 0; private static final int FLING_MODE = 1;为了方便理解我们画一个配图吧:
然后我们再来看看这些参数在哪里赋值以及滑动是在那里触发滴?还记得我们前面分析过的Scroller.startScroll(int startX, int startY, int dx, int dy, int duration)函数嘛?现在我们就来看看它的庐山真面目(此刻我正在怀疑命名这个方法的工程师是不是脑子被驴子踢了)
/** * Start scrolling by providing a starting point, the distance to travel, * and the duration of the scroll. * * @param startX Starting horizontal scroll offset in pixels. Positive * numbers will scroll the content to the left. * @param startY Starting vertical scroll offset in pixels. Positive numbers * will scroll the content up. * @param dx Horizontal distance to travel. Positive numbers will scroll the * content to the left. * @param dy Vertical distance to travel. Positive numbers will scroll the * content up. * @param duration Duration of the scroll in milliseconds. */ 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(),之前我还以为滚动操作是从这里开始的呢!结果呢,整个方法,都在进行赋值操作。也罢,这样我们前面分析的变量的赋值也在这里基本完成,源码很明了,也不过多啰嗦。但到底是在哪里触发滑动效果滴呢? 如果不是在 startScroll(),那会不会是scroller.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; }首先通过mFinished判断滑动是否已经完成,如果mFinished=true,该函数结束。若 mFinished=false,则未完成,函数继续往下执行,接着计算timePassed,这个时间是这样计算的,用现在的时间减去我们开始调用startScroller所记录的时间mStartTime,则得到到现在为止所消耗的时间段,然后再通过timePassed < mDuration判断这个时间是否超我们自己所设置的执行时间,如果条件成立,则会去计算mCurrX和mCurrY的值,最后该方法返回true。返回true能干嘛?还记得我们覆写View.computeScroll()方法嘛?
@Override public void computeScroll() { if(scroller.computeScrollOffset()){ scrollTo(scroller.getCurrX(),scroller.getCurrY()); //重绘 postInvalidate(); } }
这时View.computeScroll()内部会去调用scrollTo(scroller.getCurrX(),scroller.getCurrY())去滑动和postInvalidate()去重绘,从而形成循环。我们会发现刚才在scroller.computeScrollOffset()方法中计算mCurrX和mCurrY的值在这里就使用上了!而我们通过前面的分析也知道了mCurrX和mCurrY只是执行过程中的一小段距离而已。这也进一步印证了Scroller类的工作原理,即View每一次都只进行小幅度的距离滑动,而多次小幅度距离滑动的就组成了弹性滑动,也就形成了平滑过渡。
但是核心问题还是没解决啊!现在我们只是找到了滑动是在View.computeScroll()内部通过scrollTo()触发的,而还不知道View.computeScroll()到底是在哪里触发的.....还记得我们前面分析的情况吗?就是无论是postInvalidate执行还是invalidate执行最终都会去触发computeScroll()。由于执行postInvalidate(invalidate)都会引起view重绘,而view重绘肯定跟draw(Canvas)有关,我们马上看看:
/** * Manually render this view (and all of its children) to the given Canvas. * The view must have already done a full layout before this function is * called. When implementing a view, implement * {@link #onDraw(android.graphics.Canvas)} instead of overriding this method. * If you do need to override this method, call the superclass version. * * @param canvas The Canvas to which the View is rendered. */ @CallSuper public void draw(Canvas canvas) { final int privateFlags = mPrivateFlags; final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE && (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState); mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN; /* * Draw traversal performs several drawing steps which must be executed * in the appropriate order: * * 1. Draw the background * 2. If necessary, save the canvas' layers to prepare for fading * 3. Draw view's content * 4. Draw children * 5. If necessary, draw the fading edges and restore layers * 6. Draw decorations (scrollbars for instance) */ // Step 1, draw the background, if needed int saveCount; if (!dirtyOpaque) { drawBackground(canvas); } // skip step 2 & 5 if possible (common case) final int viewFlags = mViewFlags; boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0; boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0; if (!verticalEdges && !horizontalEdges) { // Step 3, draw the content if (!dirtyOpaque) onDraw(canvas); // Step 4, draw the children dispatchDraw(canvas); // Overlay is part of the content and draws beneath Foreground if (mOverlay != null && !mOverlay.isEmpty()) { mOverlay.getOverlayView().dispatchDraw(canvas); } ........... ........... }我们只截取一部分源码,由方法分析可以知道这个方法总共分6步,而我们只需关注第4步:Draw children,为什么要关注第4步呢?这是因为我们通过scrollTo()移动不是内容就肯定是子view,而这里我们一般都是移动ViewGroup的子view,而子view移动,肯定是要重绘滴,所以我们要关注子view的绘制方法,也就是dispatchDraw(canvas),我再看看该方法的实现:
/** * Called by draw to draw the child views. This may be overridden * by derived classes to gain control just before its children are drawn * (but after its own view has been drawn). * @param canvas the canvas on which to draw the view */ protected void dispatchDraw(Canvas canvas) { }在View中这个方法为空,不过也很正常,View肯定没有子view嘛!那谁有?肯定是ViewGroup,马上看看:
protected void dispatchDraw(Canvas canvas) { ...... for (int i = 0; i < childrenCount; i++) { ...... more |= drawChild(canvas, child, drawingTime); ...... } ...... }果然有,不过又跑去调用 drawChild(canvas, child, drawingTime),再看看实现呗:
/** * Draw one child of this View Group. This method is responsible for getting * the canvas in the right state. This includes clipping, translating so * that the child's scrolled origin is at 0, 0, and applying any animation * transformations. * * @param canvas The canvas on which to draw the child * @param child Who to draw * @param drawingTime The time at which draw is occurring * @return True if an invalidate() was issued */ protected boolean drawChild(Canvas canvas, View child, long drawingTime) { return child.draw(canvas, this, drawingTime); }啥也别说了,心已累,这家伙又跑去调用了View的draw的方法,不过是这次是3个参数的。心中草泥马又在开始鸡冻起来了
draw(Canvas canvas, ViewGroup parent, long drawingTime) { ...... if (!drawingWithRenderNode) { computeScroll(); sx = mScrollX; sy = mScrollY; } ...... }
草泥马开始狂奔了啊!!!!终于找到了computeScroll(),当然这就解释了为何View调运invalidate()就会触发computeScroll()方法了。同时也明白了为什么一开始我们需要手动去触发postInvalidate(invalidate)方法了。
好吧,虽然草泥马还在折腾,但还是小结一下:
我们在自定义ViewGroup中调用了scroller.startScroll()方法设置了一些参数,如滑动起点和滑动终点以及滑动的时间,然后我们手动去去触发postInvalidate(invalidate)方法,postInvalidate(或invalidate)方法执行后,就会去触发View.draw()方法,通过View.draw()方法内部调用ViewGroup.dispatchDraw()方法绘制子视图,dispatchDraw()方法内部会调用drawChild()方法,而drawChild()方法会调用该子View的draw()方法(三个参数),在draw()方法中会调用computeScroll方法进行滚动,而我们重写了computeScroll()方法,在computeScroll()方法中调用了scroller.computeScrollOffset来判断是否需要滑动,如果需要滑动,就调用scrollTo()方法,并传入通过scroller.getCurrX()和scroller.getCurrY()获取到需要滑动的xy值来实现滑动效果,接着通过 postInvalidate()来重绘,如此循环直到结束,以此便形成了我们所需要的滑动效果。
案例源码:http://download.csdn.net/detail/javazejian/9413283
该结束的还是得结束........
主要参考文章:
http://blog.csdn.net/qinjuning/article/details/7419207?utm_source=tuicool&utm_medium=referral