起初认识Srcoller还是在ViewPager的源码中,当看到ViewPager源码了解它是如何滑动的时候,发现了Scroller的作用。Scroller是一个辅助类,根据x、y坐标,还有滑动的时间,得到滑动到某个时间的坐标,这样可以使滑动的过程显的更加平滑,而不会出现一段段位移的效果。
如何使用,源码中也列出了几句话
举例 下面是开始一个滑动的代码
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();
}
举例 计算当前位移的坐标
if (mScroller.computeScrollOffset()) {
// Get current x and y positions
int currX = mScroller.getCurrX();
int currY = mScroller.getCurrY();
... ...
}
开始滚动的方法
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;
}
startX startY 代表开始滚动的一个点的坐标(使用滚动坐标系)
dx 水平滚动的距离
dy 垂直滚动的距离
startX 和 dx 为正代表向左 为负代表向右
startY 和 dy 为正代表向上 为负代表向下
duration 滚动需要的时长
实时拿到控件移动的位置,并且移动的姿势都在computeScrollOffset方法中封装好了
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;
}
无论是滚动模式 还是 快速滑动模式 这里都是要得到mCurrX 和 mCurrY的值,View通过 int currX = mScroller.getCurrX() int currY = mScroller.getCurrY() 就能得到当前View的内容应该滚动到的位置。
拿到了滚动的位置,但是Scroller又不承担滚动的责任,那么View是如何滚动起来的呢?上面源码的举例说明提到了,开始滚动调用startScroll方法,都是初始化的操作。接着startScroll执行完又调用了invalidate方法,猜测invalidate方法很有可能跟View 的滚动有关系。继续查看invalidate的源码:
说明
* 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()}.
绘制整个View,如果View是可见的那么以后会调用onDraw方法。这个方法一定是要运行在UI线程的,如果是从非UI线程调用那么应该用postInvalidate方法。
View调用invalidate方法
--->invalidate(true)
--->invalidateInternal
--->mParent.invalidateChildinvalidateChild(this, damage) 调用父容器的invalidateChild方法,作用是通过do while循环不断将向上层遍历,最终拿到最外层的parent
... ...
do {
View view = null;
if (parent instanceof View) {
view = (View) parent;
}
if (drawAnimation) {
if (view != null) {
view.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
} else if (parent instanceof ViewRootImpl) {
((ViewRootImpl) parent).mIsAnimating = true;
}
}
... ...
parent = parent.invalidateChildInParent(location, dirty);
... ...
} while (parent != null);
由于最顶层的View是DecorView,而通过invalidateChildInParent拿到的parent就是ViewRootImpl;非顶层通过invalidateChildInParent拿到的parent就是ViewGroup。如果parent是ViewGroup,那么就直接返回parent,如果parent是ViewRootImpl,操作如下
--->invalidateChildInParent 如果parent是ViewRootImpl那么它调用这个方法如下
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
checkThread();
... ...
invalidateRectOnScreen(dirty);
return null;
}
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
由于创建VieRootImpl的时候mThread就是UI线程,因此这里的checkThread会判断当前是否是UI线程,如果不是那么会抛出异常,这也就是说明了Invalidate方法必须在UI线程中调用。
--->invalidateRectOnScreen
--->scheduleTraversals
mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); 发送消息要执行mTraversalRunnable的run方法的内容
--->doTraversal
--->performTraversals
--->performDraw
--->draw(fullRedrawNeeded)
--->drawSoftware
final Canvas canvas;
... ...
canvas = mSurface.lockCanvas(dirty);
... ...
canvas.translate(-xoff, -yoff);
if (mTranslator != null) {
mTranslator.translateCanvas(canvas);
}
... ...
mView.draw(canvas);
... ...
这里的mView.draw(canvas)就是DecorView的draw方法,DecorView本质上也是一个View,那么我们看View的draw方法
--->draw(Canvas canvas)
draw 的流程
1. 绘制背景
2.保存画布的图层为了渐隐效果做准备(如果需要)
3. 绘制内容
4. 绘制 children
5. 如果有需要,绘制渐隐(fading) 效果
6. 绘制装饰物 (scrollbars)
--->dispatchDraw 我们直接看这个方法,它的作用是绘制children,当然肯定是ViewGroup调用它
--->drawChild
--->child.draw(canvas, this, drawingTime) 这里又调用了View的draw方法,不过参数有所不同,该方法是ViewGroup调用 drawChild方法后执行的,意在使每个child都被绘制出来。制的view专门负责描述视图和硬件加速方面的类型。
... ...
int sx = 0;
int sy = 0;
if (!drawingWithRenderNode) {
computeScroll();
sx = mScrollX;
sy = mScrollY;
}
... ...
if (offsetForScroll) {
canvas.translate(mLeft - sx, mTop - sy);
} else {
if (!drawingWithRenderNode) {
canvas.translate(mLeft, mTop);
}
if (scalingRequired) {
if (drawingWithRenderNode) {
// TODO: Might not need this if we put everything inside the DL
restoreTo = canvas.save();
}
// mAttachInfo cannot be null, otherwise scalingRequired == false
final float scale = 1.0f / mAttachInfo.mApplicationScale;
canvas.scale(scale, scale);
}
}
... ...
这里我们看到了computeScroll方法,这个方法是要求子类自己去实现的,然后我们看到canvas被移动了,而移动的距离跟mScrollX和mScrollY有关系。
而mScrollX和mScrollY就是关键所在,在这之前看两个方法:scrollTo 和 scrollBy
/**
* 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();
}
}
}
/**
* Move 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 amount of pixels to scroll by horizontally
* @param y the amount of pixels to scroll by vertically
*/
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
顾名思义 这两个方法正是View滚动的源头。根据源码我们看到要使View滚动就得改变mScrollX和mScrollY的值,然后重绘,显示View滚动后的位置。所以说,View滚动跟canvas移动了有关,移动了多少跟mScrollX和mScrollY有关,mScrollX、mScrollY又通过调用了scrollTo或者scrollBy方法得到,这两个方法的参数x、y就影响了移动距离的数值。如果想移动一下传一对x、y就行,如果想按某个规则移动就得依靠Scroller,通过Scroller实时将一组组x、y传递过来,那么View就会是平滑的进行一段移动。因此View的draw方法中留了一个computeScroll方法让我们重写,使View当前的mScrollX和mScrollY跟Scroller的mCurrX和mCurrY做了映射,然后重绘,根据Scroller之前讲的移动规则,实时返回移动后的位置,computeScroll拿到赋值给mScrollX和mScrollY,重绘调用draw方法的时候cavas就会移动mScrollX和mScrollY,然后调用ViewCompat.postInvalidateOnAnimation(this),使得这一个过程达到平滑移动的效果。
拓:是如何做到所有View都调用了onDraw方法呢?
我们看到上面提到了调用DecorView的draw方法,就是调用View的draw方法,draw分为六个步骤,在第三个步骤中,如果本身是不透明的就调用onDraw方法,如下:
public void draw(Canvas canvas) {
... ...
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);
}
// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
// we're done...
return;
}
... ...
}
dirtyOpaque 代表是否是不透明的效果(是否是实心控件),然后调用onDraw(canvas)方法。
遍历所有子View进行绘制,根据字面意思应该在dispatchDraw方法中进行:
protected void dispatchDraw(Canvas canvas) {
... ...
for (int i = 0; i < childrenCount; i++) {
while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
final View transientChild = mTransientViews.get(transientIndex);
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
transientChild.getAnimation() != null) {
more |= drawChild(canvas, transientChild, drawingTime);
}
transientIndex++;
if (transientIndex >= transientCount) {
transientIndex = -1;
}
}
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
... ...
}
遍历了当前ViewGroup(顶层即DecorView)所有子View然后调用drawChild方法:
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
drawChild绘制了ViewGroup中的一个View,并且这个方法可以获得正确的画布状态,正确状态包括裁剪,移动坐标的转化以便child移动原点在0,0处,还有一些动作转变。
看源码,实际上这些操作都在child的draw(canvas,this,drawingTime)方法中实现了。进入View的draw(canvas,this,drawingTime)
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
... ...
if (!drawingWithDrawingCache) {
if (drawingWithRenderNode) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
((DisplayListCanvas) canvas).drawRenderNode(renderNode);
} else {
// Fast path for layouts with no backgrounds
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
dispatchDraw(canvas);
} else {
draw(canvas);
}
}
} else if (cache != null) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
if (layerType == LAYER_TYPE_NONE) {
// no layer paint, use temporary paint to draw bitmap
Paint cachePaint = parent.mCachePaint;
if (cachePaint == null) {
cachePaint = new Paint();
cachePaint.setDither(false);
parent.mCachePaint = cachePaint;
}
cachePaint.setAlpha((int) (alpha * 255));
canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint);
} else {
// use layer paint to draw the bitmap, merging the two alphas, but also restore
int layerPaintAlpha = mLayerPaint.getAlpha();
mLayerPaint.setAlpha((int) (alpha * layerPaintAlpha));
canvas.drawBitmap(cache, 0.0f, 0.0f, mLayerPaint);
mLayerPaint.setAlpha(layerPaintAlpha);
}
}
... ...
}
这里的draw方法本身就是DecorView的child发起的,先判断是否有缓存,如果有缓存证明绘制过一次,则会跳过绘制它自己直接调用dispatchDraw。一般ViewGroup都会跳过绘制自己直接绘制child,即
mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW 是为ture,但是如果你想ViewGroup也要进行onDraw方法的话,需要为这个ViewGroup设置背景或者setWillNotDraw(false)。
所以如果我们自定义ViewGroup想让该ViewGroup执行onDraw方法,那么我们可以为这个ViewGroup设置一个背景或者setWillNotDraw(false),该ViewGroup就走draw(canvas)方法,即那六个步骤,顺带
自己也要执行onDraw(canvas)方法。无论哪种方法DecorView的child此时也会绘制他们自己的child,这样遍历下去,所有的子View(View树)都会得到绘制。