其实关于Scroller这篇文章我早就想写了,起初是因为看Xlistview的源码,接触到了Scroller的使用,之后又在网上读了大量的文章,之后自己写了一个demo,对scroller也算是有了自己的看法。
我学习的道路是曲折的,所以我要把我会的东西写出来,说不定对别人就有用呢!
一、Scroller简介
先通过源码里的注释看下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—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> * * <p>Here is a simple example:</p> * * <pre> private Scroller mScroller = new Scroller(context); * ... * public void zoomIn() { * // Revert any animation currently in progress * mScroller.forceFinished(true); * // Start scrolling by providing a starting point and * // the distance to travel * mScroller.startScroll(0, 0, 100, 0); * // Invalidate to request a redraw * invalidate(); * }</pre> * * <p>To track the changing positions of the x/y coordinates, use * {@link #computeScrollOffset}. The method returns a boolean to indicate * whether the scroller is finished. If it isn't, it means that a fling or * programmatic pan operation is still in progress. You can use this method to * find the current offsets of the x and y coordinates, for example:</p> * * <pre>if (mScroller.computeScrollOffset()) { * // Get current x and y positions * int currX = mScroller.getCurrX(); * int currY = mScroller.getCurrY(); * ... * }</pre> */我做个简单的翻译,Scroller这个类对滑动进行了封装,你可以通过Scroller来收集一些用来执行滑动动画的数据——举个例子,为了响应fling手势,Scrollers会跟随时间来追踪滑动的偏移,但是它们不会自动地在你的view上应用这些位置。获取并且以一个能够使滑动动画看起来流畅的速度来使用位置,这些都是由你来完成的。
mScroller,forceFinished(true);//让所有正在进行的动画都恢复原状,也就是停用所有动画
mScroller.startScroll(0,0,100,0);//前两个参数是起始坐标点,后两个分别是x,y轴要滑动的距离。
invalidate();//请求重新绘制界面,从而触发滑动
使用computeScrollOffset方法来追踪x/y轴的坐标,如果这个方法返回false,表示滑动还没有结束,我们可以通过这个方法来找到当前滑动的x和y的坐标,就像这样:
if(mScroller.computeScrollOffset()){
//获取当前x和y的坐标
int currX = mScroller.getCurrX();
int currY = mScroller.getCurrY();
}
好了,大致就是说,Scoller并不是真的能开始滑动效果的,它只是能够记录滑动动画所需要的实时数据,具体滑动动画还是得由你自己来实现的。说白了,使用Scroller类是为了给做出一个流畅的动画,而不是pia一下,还没开始就已经结束了。
二、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() { }这个方法是被parent调用的,child会根据需要来更新它的值,例如mScrollX和mScrollY,如果child在用Scroller做滚动动画的话,这个方法通常会被执行。
为了实现偏移控制,一般自定义View/ViewGroup都需要重载该方法 。
接下来看看这个方法是怎么调用的
invaliate()会导致界面重绘,上面说过invalidate方法会触发滑动动画。而有经验的朋友都知道,invalidate方法其实会引起界面重绘,所以我们从界面绘制说起,View的绘制包括三个主要过程,分别是measure,layout和draw。下面我们看View类的draw方法源码:
public void draw(Canvas canvas) { ... ... /* * 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); } ... ... // Step 2, save the canvas' layers ... ... // Step 3, draw the content if (!dirtyOpaque) onDraw(canvas); // Step 4, draw the children dispatchDraw(canvas); // Step 5, draw the fade effect and restore layers ... ... }
@Override protected void dispatchDraw(Canvas canvas){ ... for (int i = 0; i < count; i++) { final View child = children[getChildDrawingOrder(count, i)]; if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { more |= drawChild(canvas, child, drawingTime); } } }最后再看drawChild的源码
protected boolean drawChild(Canvas canvas, View child, long drawingTime) { ... child.computeScroll(); ... }
可以看到,最终会调用child的computeScroll(),这里有一点要注意一下:因为invaliate()引起的是整个view树的重绘,而这里的viewgroup也有parent,根据上面的描述,viewgroup的computeScroll()也会被调用。我们需要在computeScroll()中来进行实际的移动操作,就象这样
public void computeScroll() { if (mScroller.computeScrollOffset()&¤tY!=mScroller.getCurrY()) { //真正的滑动 scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); currentY=mScroller.getCurrY(); postInvalidate(); } }这里我来解释一下:1、mScroller.computeScrollOffset()在滑动没有结束的时候,返回的是true,结束以后返回false
2、currentY!=mScroller.getCurrY() currentY是记录当前滑动的y轴偏移量,这里做了一个判断,不让它做重复的被执行,这里虽然重复执行对滑动动画也没影响,至于为什么后面说到scrollto的时候会解释
3、mScroller.getCurrX() 返回这次滑动中在x轴上的偏移量,mScroller.getCurrY()返回的则是y轴上的。
三、scrollTo介绍
/** * Set the scrolled position of your view. This will cause a call to * {@link #onScrollChanged(int, int, int, int)} and the view will be * invalidated. * @param x the x position to scroll to * @param y the y position to scroll to */ 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(); } } }
这里先说下mScrollX和mScrollY两个参数的意思。mScrollX和mScrollY指的是视图内容相对于视图原始起始坐标的偏移量,mScrollX和mScrollY的默认值为0,因为默认是没有偏移的。另外需注意偏移量的正负问题,因为是相对视图起始坐标的,所以如果你是向右偏移那么mScrollX应该是负数,而向左偏移mScrollX为正数。举个例子,比如你定义了一个ImageView,其左上角的坐标为(100,80),此时mScrollX和mScrollY值都为0(没有偏移),现在你要把该ImageView移到(120,100)处,也就是右下方,那么你的mScrollX应该是100-120=-20,mScrollY应该是80-100=-20,这下你该明白了吧。
从该方法中我们可以看出,它先判断传进来的(x, y)值是否和View的X, Y偏移量相等,如果相等,就是说view已经在移动了(x,y),也就没必要移动了,这里就解释了上面的那个为什么重复执行对滑动动画没有影响的问题了。如果不相等,就调用onScrollChanged()方法
/** * This is called in response to an internal scroll in this view (i.e., the * view scrolled its own contents). This is typically as a result of * {@link #scrollBy(int, int)} or {@link #scrollTo(int, int)} having been * called. * * @param l Current horizontal scroll origin. * @param t Current vertical scroll origin. * @param oldl Previous horizontal scroll origin. * @param oldt Previous vertical scroll origin. */ protected void onScrollChanged(int l, int t, int oldl, int oldt) { if (AccessibilityManager.getInstance(mContext).isEnabled()) { postSendViewScrolledAccessibilityEventCallback(); } mBackgroundSizeChanged = true; final AttachInfo ai = mAttachInfo; if (ai != null) { ai.mViewScrollChanged = true; } }看他的注释,说的是这个方法是用来响应view内部滑动的,有一点需要提出来,scrollTo方法,如果当前控件是view,滚动的是它本身的内容,如果是viewgroup,则移动的他的子view。
四、startScroll()方法
最后来说下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; }结合上面说的mScrollX和mScrollY这两个参数在来看这个方法就很简单了,
startX:开始x轴的偏移量,是相对于视图原始坐标的,如果偏移量是正数,就会把视图瞬间滑动到左边,然后把该点作为滑动的起始点
startY:开始y轴的偏移量,是相对于视图原始坐标的,如果偏移量是正数,就会把视图瞬间滑动到上边,然后把该点作为滑动的起始点
dx:x轴需要滑动的距离,以startX是起点,相对于startX的偏移量,如果是正数,就会相对于startX向左滑动
dy:y轴需要滑动的距离,以startY是起点,相对于startY的偏移量,如果是正数,就会相对于startY向上滑动
duration:滑动持续的时间,单位是毫秒,默认是250毫秒
好了,关于Scroller的差不多都说完了,最后留个DEMO,可以看下效果。有什么问题,欢迎留言或者联系我,一起探讨。