欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android补间动画(Animation)是如何运行的

程序员文章站 2022-03-25 14:55:07
...

        Animation内部计算坐标用的就是Matrix,如果对Matrix还不太清楚的可以看看Android矩阵变换讲解Matrix

        对于Animation的使用,一般开始执行动画调用的是view.startAnimation(),之后,设置的view开始就会执行相应的动画,我们就以这个为入口来看看整个的流程的怎样的:

 

public void startAnimation(Animation animation) {
    //这个是设置动画开始的时间,开启动画后动画并不是马上执行,而是要等到屏幕刷
    //新的时候才会开始执行
    animation.setStartTime(Animation.START_ON_FIRST_FRAME);
    //这里就是给view中的动画属性进行赋值
    setAnimation(animation);
    //这里就是设置一个标识:PFLAG_INVALIDATED
    invalidateParentCaches();
    //这里就是请求下次屏幕刷新时重新进行绘制,所以这里就是下一个入口
    invalidate(true);
}
public void setAnimation(Animation animation) {
    mCurrentAnimation = animation;

    if (animation != null) {
        // If the screen is off assume the animation start time is now instead of
        // the next frame we draw. Keeping the START_ON_FIRST_FRAME start time
        // would cause the animation to start when the screen turns back on
        //在之前分析view.post()方法时,有讲到mAttachInfo这个属性是在dispatchAttachedToWindow进行赋值的
        if (mAttachInfo != null && mAttachInfo.mDisplayState == Display.STATE_OFF
                && animation.getStartTime() == Animation.START_ON_FIRST_FRAME) {
            //前面刚设置了这个方法,这个是为了减少动画执行时间的误差,就是保证我们设置的动画时间可以得到准确执行
            animation.setStartTime(AnimationUtils.currentAnimationTimeMillis());
        }
        animation.reset();
    }
}
protected void invalidateParentCaches() {
    if (mParent instanceof View) {
        ((View) mParent).mPrivateFlags |= PFLAG_INVALIDATED;
    }
}
public void invalidate(boolean invalidateCache) {
    invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}

void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
                        boolean fullInvalidate) {
    ......

    if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
            || (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
            || (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED
            || (fullInvalidate && isOpaque() != mLastIsOpaque)) {

        final AttachInfo ai = mAttachInfo;
        final ViewParent p = mParent;
        if (p != null && ai != null && l < r && t < b) {
            final Rect damage = ai.mTmpInvalRect;
            damage.set(l, t, r, b);
            //这里调用到时父类的方法
            p.invalidateChild(this, damage);
        }
        ......
    }
}

如果你对mAttachInfo的赋值感兴趣,可以看看view.post()的工作原理。mAttachInfo如果硬要以Activity的生命周期进行划分,具体是在哪个阶段进行赋值的,你可以理解是在onResume()中,实际是在onResume()执行之后。看到这里你也许会有一个疑惑,那就是我如果是在onCreate()开始动画,那岂不这里就不会往下执行,那是不是动画就不执行了呢?继续往后看。

        p.invalidateChild()这个方法是在ViewGroup中实现的:

 

public final void invalidateChild(View child, final Rect dirty) {
    final AttachInfo attachInfo = mAttachInfo;
    //这里就是判断你得设备是否支持硬件加速,如果支持就直接执行这里,执行这里和执行后面其实最后
    // 回到的都是同一个地方,那就是ViewRootImpl中的invalidate()方法,这里我就不从这个入口进入了
    if (attachInfo != null && attachInfo.mHardwareAccelerated) {
        // HW accelerated fast path
        onDescendantInvalidated(child, child);
        return;
    }

    ViewParent parent = this;
    if (attachInfo != null) {
        ......

        final int[] location = attachInfo.mInvalidateChildLocation;
        location[CHILD_LEFT_INDEX] = child.mLeft;
        location[CHILD_TOP_INDEX] = child.mTop;
        ......
        do {
            ......
            //这个一开始是自己,所以继续看它的invalidateChildInParent()方法,
            //这里还有一点,这里是一个while循环,意味着只有返回的parent为null时循环才会终止
            parent = parent.invalidateChildInParent(location, dirty);

            ......
        } while (parent != null);
    }
}
public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
    if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID)) != 0) {
        ......
        //这里返回的是它的父View,不过最终执行的还是父view的这个方法,所以这里我们就当是执行到了
        // 最顶层的ViewGroup了,那这里就涉及到一个问题,顶层的mParent是谁,那就是ViewRootImpl,也
        // 许你有个疑问,ViewRootImpl并不是一个view,怎么成父view了,这里你要看清一点,这个mParent的
        // 类型是ViewParent,ViewRootImpl实现了这个接口,那我们就去ViewRootImpl看看invalidateChildInParent()
        // 这个方法是怎么实现的
        return mParent;
    }

    return null;
}

上面其实是一个递归的方法,最终会调用到ViewRootImpl这个类中来,来看看它是怎么实现invalidateChildInParent()的:

 

@Override
public void invalidateChild(View child, Rect dirty) {
    invalidateChildInParent(null, dirty);
}
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
    //检查当前线程是否是在主线程中
    checkThread();
    if (DEBUG_DRAW) Log.v(mTag, "Invalidate child: " + dirty);
    //从前面传进来的参数我们知道这里dirty不为null
    if (dirty == null) {
        invalidate();
        return null;
    } else if (dirty.isEmpty() && !mIsAnimating) {
        return null;
    }

    ......
    //所以最后肯定是执行到这个方法了
    invalidateRectOnScreen(dirty);
    //嗯,终于看到这里返回null,前面看到的循环在这返回null就退出循环了
    return null;
}

接下来就是去看看invalidateRectOnScreen():

 

private void invalidateRectOnScreen(Rect dirty) {
    .....
    
    if (!mWillDrawSoon && (intersected || mIsAnimating)) {
        //对于view的测量、布局还有绘制这个肯定都很熟悉,而scheduleTraversals()就是这些方法的
        // 入口,这里其实你就可以认为是再次去重新执行这些流程了,只不过针对的是改变的区域,
        // 实际上这些操作并不是马上就去执行,而是会等到屏幕刷新的信号来到时才会去执行,
        // 所以可能会有一个等待的时间,这也就是为什么一开始动画的时候要设置一个开始的时间,
        // 就是为了减少动画执行是时间的误差如果你对屏幕刷新机制一点都不懂,可以自己去学习学习
        scheduleTraversals();
    }
}

这里请思考一个问题,如果在view中多次调用invalidate(),是不是每重新调用一次就会请求一次绘制?

        带着这个问题,进入scheduleTraversals()这个方法中去看看:

 

void scheduleTraversals() {
    //这里将mTraversalScheduled赋值为了true,意味着在mTraversalScheduled赋值为false时,
    // 这里都将不执行任何操作,也就意味着后面的请求重绘都不做处理(实际并不是不做处理,
    // 只是这里第一次已经处理了,下次重绘时都将会处理,所以没必要做重复的处理)
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        ......
    }
}
void doTraversal() {
    //mTraversalScheduled就是在这里重新复制为false
    //这里是执行测量、布局和绘制的开始的入口,进入到这里后,上面的请求重绘将再次被接受处理
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        ......
    }
}

        分析到这里,请求重绘的流程就差不多了,接下来就是等屏幕刷新信号的到来,之后再重新执行测量、布局和绘制,这里主要讲的是动画,那主要逻辑自然是在View的draw()当中了:

 

public void draw(Canvas canvas) {
    ......

    //下面这段话是Android中的,主要的意思就是view的绘制分为六个步骤,第二步和第五步应该是
    // 绘制图层,和我们关系不大,第一步和第六步分别绘制背景色和前景色,这里多提一点,设置
    // 背景色的时候应该尽量避免过度绘制这个问题,接下来就是比较重要的第三和第四步了,
    // 第三步是绘制本身的内容,第四步是绘制子view,这里看下第三步的执行方法:
    // Step 3, draw the content
    // if (!dirtyOpaque) onDraw(canvas);
    // 这里是自定义view时绘制时重写的方法,这个方法里不涉及view的动画,
    // 那这里就剩第四步没讲到了,这里看下第四部执行的方法:
    // Step 4, draw the children
    // dispatchDraw(canvas);
    //这个方法也是一个空方法,但是在他的子类ViewGroup有实现
    /*
     * 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)
     */

    ......
}

下面我们就去看看ViewGroup中的dispatchDraw():

 

@Override
protected void dispatchDraw(Canvas canvas) {
    ......
    
    more |= drawChild(canvas, transientChild, drawingTime);
    
    ......

}
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    return child.draw(canvas, this, drawingTime);
}

 

这里有一个drawChild()方法,这个方法调用的是View中draw()三个参数的重载方法,那又回到了View这个类中:

 

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
    ......
    final Animation a = getAnimation();
    //还记得前面开始动画的时候设置的动画书画属性,这里就是拿到那个设置的动画
    if (a != null) {
        //这个方法就是动画执行逻辑的的处理,跟进去看看
        more = applyLegacyAnimation(parent, drawingTime, a, scalingRequired);
        concatMatrix = a.willChangeTransformationMatrix();
        if (concatMatrix) {
            mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
        }
        transformToApply = parent.getChildTransformation();
    }
    ......
}

设置的动画逻辑处理在applyLegacyAnimation()方法中:

 

private boolean applyLegacyAnimation(ViewGroup parent, long drawingTime,
                                     Animation a, boolean scalingRequired) {
    Transformation invalidationTransform;
    final int flags = parent.mGroupFlags;
    final boolean initialized = a.isInitialized();
    //动画是否初始化过,首次进来的时候是false
    if (!initialized) {
        //进入到这里后,对Animation中的一些参数进行初始化
        a.initialize(mRight - mLeft, mBottom - mTop, parent.getWidth(), parent.getHeight());
        a.initializeInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop);
        if (mAttachInfo != null) a.setListenerHandler(mAttachInfo.mHandler);
        //设置一个标示说明动画开始了
        onAnimationStart();
    }

    final Transformation t = parent.getChildTransformation();
    boolean more = a.getTransformation(drawingTime, t, 1f);
    if (scalingRequired && mAttachInfo.mApplicationScale != 1f) {
        if (parent.mInvalidationTransformation == null) {
            parent.mInvalidationTransformation = new Transformation();
        }
        invalidationTransform = parent.mInvalidationTransformation;
        //从这里进入到动画类进行相关逻辑的处理,下次动画处理invalidationTransform保存这个对象中
        //下面边界的计算就是基于这个对象的
        a.getTransformation(drawingTime, invalidationTransform, 1f);
    } else {
        invalidationTransform = t;
    }

    //这里判断动画是否执行完成,
    if (more) {
        if (!a.willChangeBounds()) {
            if ((flags & (ViewGroup.FLAG_OPTIMIZE_INVALIDATE | ViewGroup.FLAG_ANIMATION_DONE)) ==
                    ViewGroup.FLAG_OPTIMIZE_INVALIDATE) {
                parent.mGroupFlags |= ViewGroup.FLAG_INVALIDATE_REQUIRED;
            } else if ((flags & ViewGroup.FLAG_INVALIDATE_REQUIRED) == 0) {
                // The child need to draw an animation, potentially offscreen, so
                // make sure we do not cancel invalidate requests
                parent.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
                parent.invalidate(mLeft, mTop, mRight, mBottom);
            }
        } else {
            //动画还没完成,进入到这里
            if (parent.mInvalidateRegion == null) {
                parent.mInvalidateRegion = new RectF();
            }
            final RectF region = parent.mInvalidateRegion;
            //这里计算下次绘制的边界,动画下次执行的边界是根据invalidationTransform这个计算的
            a.getInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop, region,
                    invalidationTransform);

            // The child need to draw an animation, potentially offscreen, so
            // make sure we do not cancel invalidate requests
            parent.mPrivateFlags |= PFLAG_DRAW_ANIMATION;

            final int left = mLeft + (int) region.left;
            final int top = mTop + (int) region.top;
            //invalidate()这个方法是不是在开启动画时执行的,所以就是依照前面的逻辑再次执行
            //直到动画完成时才不会执行到这里
            parent.invalidate(left, top, left + (int) (region.width() + .5f),
                    top + (int) (region.height() + .5f));
        }
    }
    return more;
}

到这里,view的部分就差不多了,对于动画其实就是由一帧帧的画面构成,接下来思考的是每帧动画是怎么计算来的,这里先进入到Animation的getTansformation()这个方法中去看看:

 

public boolean getTransformation(long currentTime, Transformation outTransformation,
                                 float scale) {
    mScaleFactor = scale;
    return getTransformation(currentTime, outTransformation);
}
public boolean getTransformation(long currentTime, Transformation outTransformation) {
    ......
    final long duration = mDuration;
    float normalizedTime;
    ......
    //这里根据传进来的时间和一开始动画执行的时长做一些处理,让后用插值器去获取一个时间插值,
    final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);
    //下面这个方法就是动画执行过程的换算,不过这个方法是一个空,具体的实现是由子类去实现,
    //像平移、缩放,旋转都应的实现类是TanslateAnimation,ScaleAnimation,RotateAnimation
    applyTransformation(interpolatedTime, outTransformation);
    ......
    return mMore;
}

看来还得到子类中去看看applyTransformation()是如何实现的,这里选择进入到ScaleAnimation类中看看:

 

@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
    float sx = 1.0f;
    float sy = 1.0f;
    float scale = getScaleFactor();

    //这里就是缩放比列计算,假设从1放大到2,那这个放大比列(sx)是逐渐从1到2渐变的
    if (mFromX != 1.0f || mToX != 1.0f) {
        sx = mFromX + ((mToX - mFromX) * interpolatedTime);
    }
    if (mFromY != 1.0f || mToY != 1.0f) {
        sy = mFromY + ((mToY - mFromY) * interpolatedTime);
    }
    //t是从view中传进来的,这里就是设置一个缩放矩阵,view拿着这个矩阵进行计算缩放,
    if (mPivotX == 0 && mPivotY == 0) {
        t.getMatrix().setScale(sx, sy);
    } else {
        t.getMatrix().setScale(sx, sy, scale * mPivotX, scale * mPivotY);
    }
}

这个方法还是比较简单的,主要是根据传进来的插值计算缩放比列,然后将其设置到矩阵中去,如果你对矩阵还不太了解,可以去看看Android矩阵变换讲解。到这里可以先猜想一下,后面对view尺寸的计算肯定用到这了这个矩阵。但是哪里用到了呢?可以先回到上面的applyLegacyAnimation()方法中。在这个方法中有调用到Animation的getInvalidateRegion(),并把上面设置矩阵对象的Transformation一并传进去了,所以肯定是在这里对view进行的重新计算:

 

public void getInvalidateRegion(int left, int top, int right, int bottom,
                                RectF invalidate, Transformation transformation) {

    final RectF tempRegion = mRegion;
    final RectF previousRegion = mPreviousRegion;
    //将view的位置设置到方形中
    invalidate.set(left, top, right, bottom);
    //使用之前计算得到的矩阵变换得到当前view的位置
    transformation.getMatrix().mapRect(invalidate);
    // Enlarge the invalidate region to account for rounding errors
    
    ......
}

到这view动画中的一帧动画就分析完了,后面就是重复这个流程了。

        前面留了一个问题,就是在onCreate()方法中开启的动画并不会请求绘制界面,那这个时候开启的动画是怎么实现的呢?跟着上面的整个流程走下来,知道这个流程实际是为了绘制动画,但是在onResume()中,同样会执行绘制界面,所以设置了动画,在这个时候是会执行的。

总结:

        1、Animation及其子类其实就是一个工具类,在设置一些参数后,会根据这些参数确定一个初始位置和最终位置,从初始位置到最终位置有一个执行时间,根据这个时间来划分动画分多少次完成。

        2、多次调用invalidate()请求绘制界面,如果是在下次屏幕刷新信号到来之前,最终只会执行第一次的绘制界面的请求,刷新信号到来之后,界面执行测量、布局和绘制。

        3、Animation内部计算view的尺寸时,用到的是Matrix。