记录:这里主要记载最近学习的结合Scoller实现View的滑动,从应用和源码的角度去分析一下滑动实现的过程。
/**
* The offset, in pixels, by which the content of this view is scrolled
* horizontally.这里说的是view的内容滑动的偏移量,不是view本身,准确的说
* 应该是view的内容当前的滑动位置,是一个最终的位置,不是需要滑动的偏移量
* {@hide}
*/
protectedintmScrollX;
/**
* The offset, in pixels, by which the content of this view is scrolled
* vertically.
* {@hide}
*/
protectedintmScrollY;
/**
* Return the scrolled left position of this view. This is the left edge of the displayed part of your view. You do not need to
* draw any pixels farther left, since those are outside of the frame of * your view on screen.获取上面两个值的方法
* @return The left edge of the displayed part of your view, in pixels.
*/
publicfinalintgetScrollX() {
returnmScrollX;
}
/**
* Return the scrolled top position of this view. This is the top edge * of the displayed part of your view. You do not need to
* draw any pixels above it, since those are outside of the frame of your view on screen.
* @return The top edge of the displayed part of your view, in pixels.
*/
publicfinalint getScrollY() {
returnmScrollY;
}
/**
* 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.
* 设置view的滑动的最终位置,这个操作会导致View.onScrollChanged的调用,同
* 时view将会被invalidate
* @param x the x position to scroll to
* @param y the y position to scroll to
*/
publicvoidscrollTo(intx, inty){}
/**
* 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.
* 设置View滑动的滑动距离,也就是在当前滑动位置的基础上想要再滑动的距离
* @param x the amount of pixels to scroll by horizontally
* @param y the amount of pixels to scroll by vertically
*/
publicvoidscrollBy(intx, inty) {}
上面的注释说的比较清楚,这里主要是View.mScrollX和View.mScrollY,这是两个实现View内容滑动的关键,我们所看到的滑动都是以这两个变量的变化为基础的。
首先写一个简单的应用,介绍一下scrollTo和scrollBy的用法:
//mian.xml
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="bt_screen"
android:onClick="onClick"/>
LinearLayout>
//activity
@Override
publicvoid onClick(View v) {
// TODO Auto-generated method stub
switch (v.getId()) {
case R.id.btn:
v.scrollBy(20, 0);//每次向左滑动20个像素
break;
}
}
scrollTo和scrollBy的使用方法很简单,直接给出想要滑动到的位置或者想要滑动位置就可以直接滑动了,注意滑动的是里面的内容,而不是View的本身,如果想要滑动某个View,需要将其放在一个Layout里面,然后调用Layout的scrollTo或者scrollBy方法实现滑动。
上面的图是调用一次scrollTo方法实现的滑动效果,会马上从起始位置滑动到最终位置,下面的图中间多了很多的“滑动中间位置”,这样从起始到最终位置会有一个比较友好的动画效果,具备更好的体验。
“滑动中间位置”帧就是通过修改view的View.mScrollX和View.mScrollY变量,然后反复多次的调用scrollTo方法实现的,为了能够改变View.mScrollX和View.mScrollY,就要用到Scroller这个类,起始完全可以自己来指定每次滑动的策略,手动修改View.mScrollX和View.mScrollY完全是可以实现滑动的,先看一下Scroller这个类:
public class Scroller {
private int mMode;
private int mStartX; //起始坐标点 , X轴方向
private int mStartY; //起始坐标点 , X轴方向
private int mFinalX; //滑动后的最终X轴位置
private int mFinalY; //滑动后的最终Y轴位置
private int mCurrX; //当前坐标点 X轴,即调用startScroll函数后,经过一定时间所达到的值
private int mCurrY; //当前坐标点 Y轴,即调用startScroll函数后,经过一定时间所达到的值
private long mStartTime;
private int mDuration;
private float mDurationReciprocal;
private float mDeltaX; //应该继续滑动的距离, X轴方向
private float mDeltaY; //应该继续滑动的距离, Y轴方向
private boolean mFinished; //是否已经完成本次滑动操作,如果完成则为 true
private Interpolator mInterpolator; //滑动中使用的插值器
//开始一个动画控制,由(startX , startY)在duration时间内前进(dx,dy)个单位,即到达坐标为(startX+dx , startY+dy)处
public void startScroll(int startX, int startY, int dx, int dy, int duration){}
//每次需要新的位置参数的时候调用这个函数,当这个方法返回true说明动画还在进行中,返回false说明滑动动画已经结束
public boolean computeScrollOffset(){}
//获取当前的mCurrX
public final int getCurrX() {
return mCurrX;
}
//获取当前的mCurrY
public final int getCurrY() {
return mCurrY;
}
//强制停止当前滑动
public final void forceFinished(boolean finished) {
mFinished = finished;
}
//中止动画
public void abortAnimation() {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
}
上面列出了Scroller 几个重要的方法和属性,仔细看这个类,并没有继承任何其他的类或者接口,说明这是一个很简单的一般类,看一下这个类的使用方法:
首先,创建一个Scroller对象;
然后,调用Scroller.startScroll(起始x,起始y,滑动dx,滑动dy,滑动持续时间),invalidate请求反复调用computeScrollOffset,然后调用View.scrollTo方法
下面分别来看这三个函数:
(1)Scroller的构造器
/**
* Create a Scroller with the specified interpolator. If the
* interpolator is null, the default (viscous) interpolator
* will be used. Specify whether or not to support progressive
* "flywheel" behavior in flinging.
*/
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
}
这个函数并没有做什么,只是做了一下简单的初始化工作。
(2)Scroller.startScroll(起始x,起始y,滑动dx,滑动dy,滑动持续时间)
/**
* 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.
* 开始一个动画控制,由(startX , startY)在duration时间内前进(dx,dy)个单位,
* 即到达坐标为(startX+dx , startY+dy)处
*/
public void startScroll(intstartX, intstartY, intdx, intdy, intduration) {
//设定SCROLL的mode,在computeScrollOffset很重要
mMode = SCROLL_MODE;
mFinished = false; //动画结束标记设为false,将要开始新的动画
mDuration = duration; //动画的持续时间
//动画的起始时间
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX; //滑动动画的起始位置 X方向
mStartY = startY; //滑动动画的起始位置 Y方向
mFinalX = startX + dx; //滑动动画的结束位置 X方向
mFinalY = startY + dy; //滑动动画的结束位置 Y方向
mDeltaX = dx; //滑动动画的实际的滑动距离 X方向
mDeltaY = dy; //滑动动画的实际的滑动距离 Y方向
//滑动动画的滑动进程单位,1/总时间
mDurationReciprocal = 1.0f / (float) mDuration;
}
做了一些变量的初始化工作,将模式设置为SCROLL_MODE,并将动画结束标记设为false,准备开始新的计算,另外对一些关键的变量的值做了设置。
(3)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) {
returnfalse;
}
inttimePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
//当前时间距离滑动起始时间mStartTime的时间间隔
if (timePassed < mDuration) {
//当前时间距离滑动起始时间mStartTime的时间间隔是否已经超过
//滑动动画设定的时间,动画超时控制
switch (mMode) {
caseSCROLL_MODE:
//重点,timePassed*mDurationReciprocal=timePassed/mDuration,代表目前滑动动画的进程百分比
finalfloatx = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
//计算这一帧中,动画滑动的x方向位置,round四舍五入
mCurrX = mStartX + Math.round(x * mDeltaX);
//计算这一帧中,动画滑动的y方向位置,round四舍五入
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
caseFLING_MODE:
break;
}
} // end if (timePassed < mDuration)
else {
//动画超时的时候,直接将最终的滑动位置设置为最终位置
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
returntrue;
}
这个方法就是根据当前的时间计算出滑动动画已经持续的时间,并利用插值器mInterpolator计算出当前帧应该滑动到的位置,看一下Scroller默认的插值器ViscousFluidInterpolator:
static class ViscousFluidInterpolator implements Interpolator {
/** Controls the viscous fluid effect (how much of it). */
privatestaticfinalfloatVISCOUS_FLUID_SCALE = 8.0f;
privatestaticfinalfloatVISCOUS_FLUID_NORMALIZE;
privatestaticfinalfloatVISCOUS_FLUID_OFFSET;
static {
// must be set to 1.0 (used in viscousFluid())
VISCOUS_FLUID_NORMALIZE = 1.0f / viscousFluid(1.0f);
// account for very small floating-point error
VISCOUS_FLUID_OFFSET = 1.0f - VISCOUS_FLUID_NORMALIZE * viscousFluid(1.0f);
}
//涉及到e的复杂的数学公式
privatestaticfloat viscousFluid(floatx) {
x *= VISCOUS_FLUID_SCALE;
if (x < 1.0f) {
x -= (1.0f - (float)Math.exp(-x));
} else {
floatstart = 0.36787944117f; // 1/e == exp(-1)
x = 1.0f - (float)Math.exp(1.0f - x);
x = start + x * (1.0f - start);
}
returnx;
}
/**
* Maps a value representing the elapsed fraction(分数,小数;
* elapsed fraction:过去部分) of an animation to a value that
* represents the interpolated fraction. This interpolated value is
* then multiplied by the change in value of an animation to
* derive the animated value at the current elapsed animation time.
* @param input A value between 0 and 1.0 indicating our current point
* in the animation where 0 represents the start and 1.0 represents
* the end ,说白了就是目前动画的已经进行的百分比,以时间为标准计算
* @return The interpolation value. This value can be more than 1.0 for
* interpolators which overshoot their targets, or less than 0 for
* interpolators that undershoot their targets.
*/
@Override
public float getInterpolation(floatinput) {
final float interpolated = VISCOUS_FLUID_NORMALIZE * viscousFluid(input);
if (interpolated > 0) {
return interpolated + VISCOUS_FLUID_OFFSET;
}
return interpolated;
}
}
仔细看一下Scroller就是一个简单的数值运算,以动画当前进行的百分比为输入,根据一定的公式(插值器)计算下一帧动画的位置信息:
nextPos = f(progress) 下一帧的位置 = Scroller.computeScrollOffset(滑动进度)
上面分析完了Scroller的作用,就是进行数值运算,下面的问题就是这些计算出来的值该如何运用到View的动画上面?
使用方法:
public class MultiViewGroup extends ViewGroup {
public int mDurationTimeMs = 1000;
Scroller mScroller = new Scroller(); //这里使用默认的插值器
publicvoid startMove(){
mScroller.startScroll(0, 0, 720, 0,mDurationTimeMs);
invalidate();
}
@Override
publicvoid computeScroll() {
boolean needInvalidate = false;
if (mScroller.computeScrollOffset()) { // 如果返回true,表示动画还没有结束,会进行一次新的scrollTo滑动,如果返回true说明动画没有结束,并且下一帧滑动的位置已经在computeScrollOffset计算好
// 产生了动画效果每次滚动一点
scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); // 这里getCurrX或Y取出Scroller.computeScrollOffset计算出来的滑动位置信息,然后进行scrollTo滑动
invalidate();
}
}
}
上面只要调用MultiViewGroup对象的startMove方法就可以将整个屏幕向左滑动移动720px,使用起来还是很简单的,下面提出两个关键的问题:
(1)整个动画具体是怎么完成的,如何驱动的?如何维持的?如何结束的?
(2)根据最前面的scrollTo的注释,可以看到scrollTo会主动去调用invalidate()方法,但是为什么后面又要调用一次invalidate方法,网上的demo里面都说不调用会有误差,根据实际的测试,确实会出现误差,明明已经调用了invalidate,然后重复一次invalidate就没有误差了,真是奇怪,原因到底是什么?
问题1:整个动画具体是怎么完成的,如何驱动的?如何维持的?如何结束的?
这个问题网上很多地方都是有答案的,但是总是将的不清楚,invalidate具体做了什么,下面会再写一篇记录性的文章。总的来说invalidate就是进行了一次重绘事件,最终会导致ViewRootImpl中performTraversals方法的调用,然后将draw事件传递到PhoneWindow$DecorView.draw,最终上传到每一个控件,现在只要知道invalidate方法会导致View进 I/System.out(25777):com.qin.scrollerview.MultiViewGroup.computeScroll(TestTextView.java:49)
I/System.out(25777): android.view.View.updateDisplayListIfDirty(View.java:14043)
I/System.out(25777): android.view.View.getDisplayList(View.java:14079)
I/System.out(25777): android.view.View.draw(View.java:14846)
I/System.out(25777): android.view.ViewGroup.drawChild(ViewGroup.java:3405)
I/System.out(25777): android.view.ViewGroup.dispatchDraw(ViewGroup.java:3199)
I/System.out(25777): android.view.View.updateDisplayListIfDirty(View.java:14051)
I/System.out(25777): android.view.View.getDisplayList(View.java:14079)
I/System.out(25777): android.view.View.draw(View.java:14846)
I/System.out(25777): android.view.ViewGroup.drawChild(ViewGroup.java:3405)
I/System.out(25777): android.view.ViewGroup.dispatchDraw(ViewGroup.java:3199)
I/System.out(25777): android.view.View.updateDisplayListIfDirty(View.java:14051)
I/System.out(25777): android.view.View.getDisplayList(View.java:14079)
I/System.out(25777): android.view.View.draw(View.java:14846)
I/System.out(25777): android.view.ViewGroup.drawChild(ViewGroup.java:3405)
I/System.out(25777): android.view.ViewGroup.dispatchDraw(ViewGroup.java:3199)
I/System.out(25777): android.view.View.updateDisplayListIfDirty(View.java:14051)
I/System.out(25777): android.view.View.getDisplayList(View.java:14079)
I/System.out(25777): android.view.View.draw(View.java:14846)
I/System.out(25777): android.view.ViewGroup.drawChild(ViewGroup.java:3405)
I/System.out(25777): android.view.ViewGroup.dispatchDraw(ViewGroup.java:3199)
I/System.out(25777): android.view.View.draw(View.java:15125)
I/System.out(25777): android.widget.FrameLayout.draw(FrameLayout.java:592)
I/System.out(25777): com.android.internal.policy.impl.PhoneWindow$DecorView.draw(PhoneWindow.java:2646)
I/System.out(25777): android.view.View.updateDisplayListIfDirty(View.java:14056)
I/System.out(25777): android.view.View.getDisplayList(View.java:14079)
I/System.out(25777): android.view.ThreadedRenderer.updateViewTreeDisplayList(ThreadedRenderer.java:266)
I/System.out(25777): android.view.ThreadedRenderer.updateRootDisplayList(ThreadedRenderer.java:272)
I/System.out(25777): android.view.ThreadedRenderer.draw(ThreadedRenderer.java:311)
I/System.out(25777): android.view.ViewRootImpl.draw(ViewRootImpl.java:2523)
I/System.out(25777): android.view.ViewRootImpl.performDraw(ViewRootImpl.java:2361)
I/System.out(25777): android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1992)
I/System.out(25777): android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1078)
I/System.out(25777): android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:5810)
I/System.out(25777): android.view.Choreographer$CallbackRecord.run(Choreographer.java:818)
I/System.out(25777): android.view.Choreographer.doCallbacks(Choreographer.java:617)
I/System.out(25777): android.view.Choreographer.doFrame(Choreographer.java:583)
I/System.out(25777): android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:804)
I/System.out(25777): android.os.Handler.handleCallback(Handler.java:739)
I/System.out(25777): android.os.Handler.dispatchMessage(Handler.java:95)
I/System.out(25777): android.os.Looper.loop(Looper.java:135)
I/System.out(25777): android.app.ActivityThread.main(ActivityThread.java:5249)
I/System.out(25777): java.lang.reflect.Method.invoke(Native Method)
I/System.out(25777): java.lang.reflect.Method.invoke(Method.java:372)
I/System.out(25777): com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:907)
I/System.out(25777): com.android.internal.os.ZygoteInit.main(ZygoteInit.java:702)
multiViewGroup的draw方法被调用之前,先调用了MultiViewGrou.computeScroll方法,上面的computeScroll方法中调用mScroller.computeScrollOffset()方法更新了MultiViewGroup的mScrollX和mScrollY,然后在调用TestTextView的draw方法的时候会使用这个更新后的mScrollX和mScrollY来绘制新的MultiViewGroup。
上面就是一次“滑动中间位置”一帧计算和绘制的全过程,然后看是如何继续下一帧的。在computeScroll方法里面调用了invalidate方法(实际上并不是这个起了作用,下面分析问题2的时候会讲到)进行又一次的重绘,其实在这一次重绘事件请求发出前,上次帧的绘制还没有开始,因为上面的MultiViewGrou.computeScroll方法是在MultiViewGrou.draw方法前就调用了,看上去是有点乱,但是系统会帮助我们进行有条不紊的绘制。
在MultiViewGrou.computeScroll方法里面的invalidate方法会再一次触发整个绘制流程,从而又出现了上面的绘制步骤,绘制步骤中会调用MultiViewGrou.computeScroll方法,然后进行了scrollTo滑动,并且invalidate又触发了下一次的绘制流程,直到整个滑动动画结束的时候,MultiViewGrou.computeScroll方法里面的mScroller.computeScrollOffset()方法会返回false,从而不再出现invalidate调用,结束了整个的滑动动画。
问题2:根据最前面的scrollTo的注释,可以看到scrollTo会主动去调用invalidate()方法,但是为什么后面又要调用一次invalidate方法,网上的demo里面都说不调用会有误差,根据实际的测试,确实会出现误差,明明已经调用了invalidate,然后重复一次invalidate就没有误差了,真是奇怪,原因到底是什么?
上面问题1的流程网上基本上都说清楚了,但是问题2这个问题大家都不在乎,但是到底是为什么呢?答案就需要看源码了,先看View.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. 这个函数会导致onScrollChanged的调用,同时会invalidate这个View
* @param x the x position to scroll to
* @param y the y position to scroll to
*/
public void scrollTo(intx, inty) {
if (mScrollX != x || mScrollY != y) {
intoldX = mScrollX;
intoldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
很明显这里的postInvalidateOnAnimation方法比较惹人注意,但是这个方法的调用是有条件的,必须要awakenScrollBars方法返回false才会调用,看一下awakenScrollBars方法:
protected boolean awakenScrollBars() {
returnmScrollCache != null && //这两个条件同时为true的时候才会返回true
awakenScrollBars(mScrollCache.scrollBarDefaultDelayBeforeFade, true);
}
protected boolean awakenScrollBars(intstartDelay, booleaninvalidate) {
final ScrollabilityCache scrollCache = mScrollCache;
if (scrollCache == null || !scrollCache.fadeScrollBars) {
returnfalse;
}
if (scrollCache.scrollBar == null) {
scrollCache.scrollBar = new ScrollBarDrawable();
}
if (isHorizontalScrollBarEnabled() || isVerticalScrollBarEnabled()) { //默认情况下没有调用setVerticalScrollBarEnabled或者setHorizontalScrollBarEnabled方法的时候,这两个都是false
if (invalidate) { //上面给的参数是true
// Invalidate to show the scrollbars
postInvalidateOnAnimation();
}
if (scrollCache.state == ScrollabilityCache.OFF) {
// FIXME: this is copied from WindowManagerService.
// We should get this value from the system when it
// is possible to do so.
final int KEY_REPEAT_FIRST_DELAY = 750;
startDelay = Math.max(KEY_REPEAT_FIRST_DELAY, startDelay);
}
// Tell mScrollCache when we should start fading. This may
// extend the fade start time if one was already scheduled
longfadeStartTime = AnimationUtils.currentAnimationTimeMillis() + startDelay;
scrollCache.fadeStartTime = fadeStartTime;
scrollCache.state = ScrollabilityCache.ON;
// Schedule our fader to run, unscheduling any old ones first
if (mAttachInfo != null) {
mAttachInfo.mHandler.removeCallbacks(scrollCache);
mAttachInfo.mHandler.postAtTime(scrollCache, fadeStartTime);
}
return true;
}
return false;
}
根据上面的分析,这里awakenScrollBars()的调用会返回false,因此会调用postInvalidateOnAnimation方法,即使设置了setVerticalScrollBarEnabled或者setHorizontalScrollBarEnabled,awakenScrollBars返回了true,那么在调用awakenScrollBars方法的时候invalidate参数是true,还是会调用postInvalidateOnAnimation方法。既然scrollTo一定会调用postInvalidateOnAnimation方法,看一下这个方法:
/**
* Cause an invalidate to happen on the next animation time step, typically the
* next display frame.
* This method can be invoked from outside of the UI thread
* only when this View is attached to a window.
* 触发invalidate在下一个动画时间的时候发生,这个函数还可以在UI线程之外调用,如果这个View依附到了一个window上面
* @see #invalidate()
*/ // View.java
public void postInvalidateOnAnimation() {
// We try only with the AttachInfo because there's no point in invalidating
// if we are not attached to our window
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
attachInfo.mViewRootImpl.dispatchInvalidateOnAnimation(this);
}
}
// ViewRootImpl.java
public void dispatchInvalidateOnAnimation(View view) {
mInvalidateOnAnimationRunnable.addView(view);
}
// ViewRootImpl.java
final class InvalidateOnAnimationRunnable implementsRunnable {
private boolean mPosted;
private final ArrayList mViews = newArrayList();
private final ArrayList mViewRects = newArrayList();
private View[] mTempViews;
private AttachInfo.InvalidateInfo[] mTempViewRects;
public void addView(View view) {
synchronized (this) {
mViews.add(view);
postIfNeededLocked();
}
}
@Override
public void run() {
final int viewCount;
final int viewRectCount;
synchronized (this) {
mPosted = false;
viewCount = mViews.size(); //当前需要处理的View的数量
if (viewCount != 0) {
mTempViews = mViews.toArray(mTempViews != null ? mTempViews : new View[viewCount]); //将需要处理的View拷贝到数组mTempViews
mViews.clear();
}
viewRectCount = mViewRects.size();
if (viewRectCount != 0) {
mTempViewRects = mViewRects.toArray(mTempViewRects != null
? mTempViewRects : new AttachInfo.InvalidateInfo[viewRectCount]);
mViewRects.clear();
}
}
for (inti = 0; i < viewCount; i++) {
mTempViews[i].invalidate(); //依次调用每个View的invalidate方法
mTempViews[i] = null;
}
for (inti = 0; i < viewRectCount; i++) {
final View.AttachInfo.InvalidateInfo info = mTempViewRects[i];
info.target.invalidate(info.left, info.top, info.right, info.bottom);
info.recycle();
}
}
privatevoidpostIfNeededLocked() {
if (!mPosted) {
mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION, this, null);
mPosted = true;
}
}
}
可以看到上面的postInvalidateOnAnimation最终也调用了mChoreographer.postCallback方法,并且将这个this(是一个Runnable对象)作为参数传进了Choreographer的处理链表,这里面具体的原理要再去细看,postCallback这个方法就会去请求垂直同步信号,并且在垂直同步信号到来的时候回调这里的InvalidateOnAnimationRunnable.run方法, 这个方法会导致之前加到mViews中的每一个View的invalidate方法被调用,而且这里的时间顺序要仔细分析一下:
(1)上面的代码中忽略了View.scrollTo的实际作用,整个滑动动画的顺序是:
Scroller.startScroll(触发Scroller开始计算)–>View.invalidate(触发绘制流程)–>…–>View.draw(View的绘制流程)–>View.computeScroll(计算View的滑动位置)
(2)但是实际上去掉computeScroll中的View.invalidate方法后形成的顺序是:
Scroller.startScroll(触发Scroller开始计算)–>View.invalidate(触发绘制流程)–>…–>View.draw(View的绘制流程)–>View.computeScroll(计算View的滑动位置)
–>View.scrollTo(请求在下一次的垂直同步信号来的时候进行View.invalidate)–>View.onDraw(当前这一次绘制实际的View)
(3)上面(2)中整理后按照时间的详细顺序
根据上面的分析,需要将由invalidate触发的第1帧和后面的中间帧的流程分开:
第1帧:
*Scroller.startScroll(触发Scroller开始计算)–>View.invalidate(触发重绘请求)–>ViewRootImpl.scheduleTraversals(进入垂直同步信号请求流程)–>
–>Choreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL,加入绘制事件回调)…(等待同步信号)..
–>ViewRootImpl.performTraversals(响应垂直同步信号)–>ViewRootImpl.performDraw(分发绘制事件)–>View.draw(View的绘制流程)
–>View.computeScroll(计算View的滑动位置)–>View.scrollTo(设置本次的滑动位置,View.postInvalidateOnAnimation请求下一次的垂直同步信号)–>View.onDraw(当前这一次绘制实际的View)
–>…(等待scrollTo请求的下一个同步信号)..–>*
后续帧(比较复杂):
*Choreographer. FrameDisplayEventReceiver.onVsync(获取同步信号,发送异步消息)–>Choreographer.FrameDisplayEventReceiver.run–>Choreographer.doFrame(处理同步信号回调事件)
–>Choreographer.doCallbacks(Choreographer.CALLBACK_ANIMATION,先处理动画回调)–>ViewRootImpl.InvalidateOnAnimationRunnable.run
–>View.invalidate(触发重绘请求)–>ViewRootImpl.scheduleTraversals(进入垂直同步信号请求流程)–>Choreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL,加入绘制事件回调)
–>Choreographer.doCallbacks(Choreographer.CALLBACK_TRAVERSAL,再处理刚刚加入的绘制回调)
–>ViewRootImpl.performTraversals(不是响应下一个垂直同步信号,而是在当前的同步信号回调中立马被处理)–>ViewRootImpl.performDraw(分发绘制事件)–>View.draw(View的绘制流程)
–>View.computeScroll(计算View的滑动位置)–>View.scrollTo(设置本次的滑动位置,请求下一次的垂直同步信号)–>View.onDraw(当前这一次绘制实际的View)
–>…(等待scrollTo请求的下一个同步信号,上面invalidate也请求了,但是事件已经被当前的同步回调处理完了)..–>*
下面结合调用堆栈和代码进行分析,再看一下Scroller滑动动画的使用步骤:
创建一个Scroller对象;
调用Scroller.startScroll(起始x,起始y,滑动dx,滑动dy,滑动持续时间),invalidate请求
反复调用computeScrollOffset,然后调用View.scrollTo方法
public class MultiViewGroup extends ViewGroup {
public int mDurationTimeMs = 1000;
Scroller mScroller = new Scroller(); //这里使用默认的插值器
publicvoid startMove(){
mScroller.startScroll(0, 0, 720, 0,mDurationTimeMs);
invalidate();
}
@Override
public void computeScroll() {
boolean needInvalidate = false;
if (mScroller.computeScrollOffset()) { // 如果返回true,表示动画还没有结束,会进行一次新的scrollTo滑动,如果返回true说明动画没有结束,并且下一帧滑动的位置已经在computeScrollOffset计算好
// 产生了动画效果每次滚动一点
scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); // 这里getCurrX或Y取出Scroller.computeScrollOffset计算出来的滑动位置信息,然后进行scrollTo滑动
invalidate(); //这个是没有必要,也是没有道理的
}
}
}
这里不分析startMove中的invalidate调用了,这个是触发第1帧的源头,分析下面的computeScroll和里面的scrollTo流程,根据上面的流程可以看出来scrollTo里面主要做了两件事:
(1)根据Scroller.computeScrollOffset计算出来的滑动位置设置当前要绘制的帧的mScrollX和mScrollY的值;
(2)调用postInvalidateOnAnimation方法请求下一个同步信号,同步信号的回调类型为Choreographer.CALLBACK_ANIMATION,相应的回调方法是ViewRootImpl.InvalidateOnAnimationRunnable.run
刚才这一帧还没有绘制,computeScroll计算结束后,会调用View.draw,然后调用View.onDraw对当前帧进行绘制,绘制结束以后开始等待下一个垂直同步信号的到来,下一个同步信号是由postInvalidateOnAnimation请求,对应的回调流程看下面的堆栈:
I/System.out(10309): -----------------invalidate stack---------------------
I/System.out(10309): dalvik.system.VMStack.getThreadStackTrace(Native Method)
I/System.out(10309): java.lang.Thread.getStackTrace(Thread.java:580)
I/System.out(10309): com.qin.scrollerview.LogPrinter.printStack(LogPrinter.java:30)
I/System.out(10309): com.qin.scrollerview.TestTextView.invalidate(TestTextView.java:135)
I/System.out(10309): android.view.ViewRootImpl$InvalidateOnAnimationRunnable.run(ViewRootImpl.java:5930)
I/System.out(10309): android.view.Choreographer$CallbackRecord.run(Choreographer.java:818)
I/System.out(10309): android.view.Choreographer.doCallbacks(Choreographer.java:617)
I/System.out(10309): android.view.Choreographer.doFrame(Choreographer.java:582)
I/System.out(10309): android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:804)
I/System.out(10309): android.os.Handler.handleCallback(Handler.java:739)
I/System.out(10309): android.os.Handler.dispatchMessage(Handler.java:95)
I/System.out(10309): android.os.Looper.loop(Looper.java:135)
I/System.out(10309): android.app.ActivityThread.main(ActivityThread.java:5249)
I/System.out(10309): java.lang.reflect.Method.invoke(Native Method)
I/System.out(10309): java.lang.reflect.Method.invoke(Method.java:372)
I/System.out(10309): com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:907)
I/System.out(10309): com.android.internal.os.ZygoteInit.main(ZygoteInit.java:702)
这个堆栈流程描述的很清楚,首先这个垂直同步的消息是发送到Choreographer.mDisplayEventReceiver.onVsync方法里面的,FrameDisplayEventReceiver是Choreographer的一个内部类,看一下这里面的onVsync方法:
@Override
public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
//......省略一些代码
mTimestampNanos = timestampNanos;
mFrame = frame;
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
向Choreographer.mHandler中发送一条异步消息,注意这里的msg.setAsynchronous(true)是很有讲究的,在绘制Choreographer.CALLBACK_TRAVERSAL被加入的时候就会阻塞非异步消息的处理,这里只有异步消息才能被处理,看这里发送的是一个callback,这个callback会去回调Choreographer.mDisplayEventReceiver.run方法:
@Override
publicvoid run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame);
}
调用到了Choreographer.doFrame:
```
void doFrame(long frameTimeNanos, int frame) {
finallong startNanos;
synchronized (mLock) {
//判断是否存在跳帧
startNanos = System.nanoTime();
finallong jitterNanos = startNanos - frameTimeNanos;
if (jitterNanos >= mFrameIntervalNanos) {
finallong skippedFrames = jitterNanos / mFrameIntervalNanos;
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
}
finallong lastFrameOffset = jitterNanos % mFrameIntervalNanos;
frameTimeNanos = startNanos - lastFrameOffset;
}
mFrameScheduled = false;
mLastFrameTimeNanos = frameTimeNanos;
}
//进行Choreographer的垂直同步信号的回调,按照Choreographer.CALLBACK_INPUT、Choreographer.CALLBACK_ANIMATION到Choreographer.CALLBACK_TRAVERSAL的顺序分别处理
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
}
调用到了Choreographer.doCallbacks方法:
void doCallbacks(int callbackType, long frameTimeNanos) {
CallbackRecord callbacks;
synchronized (mLock) {
//取出mCallbackQueues[callbackType]链表中所有时间小于等于now的所有的请求,形成一个callbacks链表
finallong now = SystemClock.uptimeMillis();
callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(now);
if (callbacks == null) {
return;
}
mCallbacksRunning = true;
}
try {
//一次性进行处理上面取出的callbacks链表的所有的回调
for (CallbackRecord c = callbacks; c != null; c = c.next) {
c.run(frameTimeNanos); //调用请求的时候注册的回调进行处理
}
} finally {
synchronized (mLock) {
mCallbacksRunning = false;
do {
//回收上面的callbacks链表中的元素
final CallbackRecord next = callbacks.next;
recycleCallbackLocked(callbacks);
callbacks = next;
} while (callbacks != null);
}
}
}
上面列出了堆栈中涉及到的应用中的所有的调用函数,最重要的一点是:
//进行Choreographer的垂直同步信号的回调,按照Choreographer.CALLBACK_INPUT、Choreographer.CALLBACK_ANIMATION到Choreographer.CALLBACK_TRAVERSAL的顺序分别处理
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
这里会分别对几种类型的注册回调进行分别处理,并且是存在先后顺序的,先到达InvalidateOnAnimationRunnable.run方法,根据上面给出的InvalidateOnAnimationRunnable.run代码会发现里面调用了View.invalidate方法,这个方法最终会到达ViewRootImpl.invalidateChildInParent方法,然后转向ViewRootImpl.scheduleTraversals方法:
void scheduleTraversals() {
if (!mTraversalScheduled) { //在本轮的同步信号的响应过程中,Choreographer.CALLBACK_TRAVERSAL是否被加入的标记,可以看出来在一个同步信号周期内只能加入一个这样的消息
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().postSyncBarrier(); //同步信号的阻塞消息
mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); //请求下一个同步信号,并将mTraversalRunnable作为回调
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
}
}
上面的View.invalidate处理结束以后,达到如下效果:
(1)请求下一个同步信号;
(2)在mChoreographer.CALLBACK_TRAVERSAL处理链表中加入一条处理回调
InvalidateOnAnimationRunnable.run处理结束一个就会继续处理:
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
注意上面在InvalidateOnAnimationRunnable.run处理过程中调用View.invalidate向Choreographer.CALLBACK_TRAVERSAL对应的链表中加入一条处理记录,所以这里立马就会进行处理,处理的内容就是ViewRootImpl.mTraversalRunnable里面的run方法:
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
看一下里面的 ViewRootImpl.doTraversal方法:
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false; //设置标记为false,Choreographer.CALLBACK_TRAVERSAL又可以被加入了
mHandler.getLooper().removeSyncBarrier(mTraversalBarrier); //移除刚才在scheduleTraversals中postSyncBarrier的同步阻塞信号
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "performTraversals");
try {
performTraversals(); //进入绘制的流程
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
绘制的时候又会调用到View.computeScroll方法中的scrollTo请求下一次的垂直同步信号的回调(其实上面的View.invalidate已经发出了垂直同步信号的请求,但是它的回调已经被处理掉了),然后形成一个循环直到View.computeScroll方法中不再发出垂直同步信号的请求为止就结束了。
下面是一个整个过程中前几帧的调用过程,并且以上一次同步信号的时间为时间0点,理想情况下如果一直请求同步信号,那么每16ms会收到一次同步请求,第一次请求同步信号的时间是随机的,会在下一个垂直同步信号到的时间点得到响应。
误差分析:大家都说会存在误差,然后手动调用invalidate,但是原因是什么呢?
原因就在View.scrollTo方法里面,看一下代码:
public void scrollTo(intx, inty) {
if (mScrollX != x || mScrollY != y) {
intoldX = mScrollX;
intoldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
这里面传入的x y就是当前帧需要滑动到的位置,原因就在于这里mScrollX == x && mScrollY == y导致的,一旦相等就不会调用postInvalidateOnAnimation请求下一个同步信号,这样整个滑动驱动就没有了,也就不会继续了。为什么会出现相等的情况呢,看一下Scroller里面默认的插值器:
public boolean computeScrollOffset() {
if (mFinished) {
returnfalse;
}
inttimePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
caseSCROLL_MODE:
finalfloatx = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
mCurrX = mStartX + Math.round(x * mDeltaX); //原因就在这里,这里经过计算以后进行了取整,这就导致出现两次计算出的结果是一样的
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
}
}
return true;
}
那么为什么反复调用invalidate就可以到达效果呢?
这就要看整个滑动的驱动过程了:
startScroll–invalidate–computeScroll–scrollTo–invalidate–onDraw–computeScroll–scrollTo–invalidate–onDraw–computeScroll–scrollTo–invalidate–onDraw
假设出现mScrollX == x && mScrollY == y相等的情况,导致scrollTo没有办法执行:
startScroll–invalidate–computeScroll–scrollTo–invalidate–onDraw–computeScroll–invalidate–onDraw–computeScroll–invalidate–onDraw–computeScroll–scrollTo–invalidate–onDraw
看中间红字部分那一段,相等的时候scrollTo没有办法执行,就会通过invalidate反复驱动computeScroll进行计算,直到满足mScrollX != x || mScrollY != y就可以继续滑动了。
然后我们分析一下这个突兀的invalidate的实际作用:
(1)scrollTo能够执行时,View.invalidate在ViewRootImpl.InvalidateOnAnimationRunnable.run会在下一个垂直同步信号来的时候调用View.invalidate,再结合对上面的Choreographer的分析,可以看出来在上一个Choreographer.CALLBACK_TRAVERSAL记录被处理前,是没有办法加入新的Choreographer.CALLBACK_TRAVERSAL记录请求下一个垂直同步信号,下一个垂直信号来的时候会先处理Choreographer.CALLBACK_ANIMATION记录,系统尝试调用View.invalidate插入的Choreographer.CALLBACK_TRAVERSAL记录,但是之前的Choreographer.CALLBACK_TRAVERSAL记录尚未处理,因此系统加入的这个垂直同步信号会失败;
(2)scrollTo不能够执行时,就像上面分析的那样scrollTo中的postInvalidateOnAnimation不会被执行,滑动过程没有了驱动从而停止,这时候invalidate会一直推动整个过程反复computeScroll直到scrollTo能够执行为止
可以通过自己定义比较好的插值器来减少这样的概率,分析出问题就可以改进了,实际上根据设计来看invalidate是没有任何必要的,但是去掉以后,如何做到没有误差呢?
分析出原因就可以解决了,我们可以在scrollTo没有办法执行的时候才去invalidate推动整个滑动过程:
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
if (mScroller.getCurrX() == getScrollX() && mScroller.getCurrY() == getScrollY()) {
invalidate();
return;
}else{
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
}
}
}
//暂时写到这里吧,还有有关自定义View的时候onMeasure和onLayout来配合的过程,网上的demo都很多,还有结合VelocityTracker计算滑动速度来滑动的有时间再另外补充。