补间动画,设置动画初始与结束状态,中间状态由系统计算并控制。Animation是抽象类,它的子类实现动画的具体行为和效果,动画帧的显示与视图关联。四种补间动画类型,平移,旋转,透明度,缩放。
示例介绍
Animation mAnimation = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.tween_anim_sample);
mAnimation.setFillAfter(true);//动画结束后保留结束状态
mAnimation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
mTextView.startAnimation(mAnimation);
//xml定义,以缩放为例
<scale
android:duration="6000"
android:fromXScale="1.0"
android:fromYScale="1.0"
android:pivotX="50%"
android:pivotY="50%"//缩放的中心点,视图中点
android:toXScale="0.2"
android:toYScale="0.2" />
实现一个补间动画,过程很简单,两步就可以。首先创建一个动画对象,可以在xml中定义。然后,对将要进行动画的视图执行startAnimation方法,另外,可以设置动画监听。下面分析一下原理,动画从启动到结束,系统是如何控制的。
动画原理
每个视图都可以实现补间动画,在基类View中定义动画的启动方法。
public void startAnimation(Animation animation) {
animation.setStartTime(Animation.START_ON_FIRST_FRAME);//值-1
setAnimation(animation);//为View设置动画
invalidateParentCaches();
invalidate(true);//重绘
}
设置Animation内部mStartTime值,(值-1),初始化mStarted和mEnd标志。视图内部mCurrentAnimation赋值,并触发动画#reset方法,动画初始化状态重置。
protected void invalidateParentCaches() {
if (mParent instanceof View) {
((View) mParent).mPrivateFlags |= PFLAG_INVALIDATED;
}
}
在视图中,找到它的父视图mParent,为父视图增加PFLAG_INVALIDATED标志位。在后面的invalidate方法时,父视图的Canvas将会重建。
invalidate方法,视图重绘,
//invalidateInternal方法代码。
if (invalidateCache) {
mPrivateFlags |= PFLAG_INVALIDATED;
mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
}
动画视图增加PFLAG_INVALIDATED标志。
当一个视图执行invalidate方法,硬件渲染时,并非整个树结构视图的Canvas全部重建。从根视图的updateDisplayListIfDirty方法开始,在树结构视图遍历,每个节点都会执行该方法。
//View的updateDisplayListIfDirty方法代码。
if (renderNode.isValid()&& !mRecreateDisplayList) {
mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
dispatchGetDisplayList();//派发子视图
return renderNode; // no work needed
}
若视图Canvas不需要重建,触发dispatchGetDisplayList方法。
private void recreateChildDisplayList(View child) {
child.mRecreateDisplayList = (child.mPrivateFlags & PFLAG_INVALIDATED) != 0;
child.mPrivateFlags &= ~PFLAG_INVALIDATED;
child.updateDisplayListIfDirty();
child.mRecreateDisplayList = false;
}
当动画视图的父视图执行到recreateChildDisplayList方法时,它曾设置过PFLAG_INVALIDATED标志,因此,动画启动后,动画视图与父视图重建Canvas,它的上层视图与动画视图的兄弟视图均不需要重建。第一帧动画重建Canvas,然后除去该标志,后续动画不需要重建。
在父视图的View#updateDisplayListIfDirty方法,Canvas重建,然后,父视图跳过绘制,它自己的onDraw方法不会触发,dispatchDraw方法分发绘制子视图,也就是动画视图。
...
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
dispatchDraw(canvas);
...
} else {
draw(canvas);
}
ViewGroup#dispatchDraw方法绘制子视图,包括动画视图与其兄弟视图,触发重载的带三个参数的draw方法,该方法将处理动画事务。该方法是动画视图执行。
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();
}
...
if (hardwareAcceleratedCanvas) {
mRecreateDisplayList = (mPrivateFlags & PFLAG_INVALIDATED) != 0;
mPrivateFlags &= ~PFLAG_INVALIDATED;//除去PFLAG_INVALIDATED标志
}
...
if (drawingWithRenderNode) {
renderNode = updateDisplayListIfDirty();
}
}
首先,判断该视图是否有动画对象,在动画状态下,处理视图上活动的动画,
触发View#applyLegacyAnimation方法。第一帧动画视图重建Canvas,执行onDraw方法,然后,将除去PFLAG_INVALIDATED标志,动画视图后续将不再Canvas重建。
private boolean applyLegacyAnimation(ViewGroup parent, long drawingTime,
Animation a, boolean scalingRequired) {
Transformation invalidationTransform;
final int flags = parent.mGroupFlags;
final boolean initialized = a.isInitialized();
if (!initialized) {
//Animation初始化
a.initialize(mRight - mLeft, mBottom - mTop, parent.getWidth(), parent.getHeight());
a.initializeInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop);
...
//mPrivateFlags加上PFLAG_ANIMATION_STARTED标志
onAnimationStart();
}
//父视图内部Transformation
final Transformation t = parent.getChildTransformation();
boolean more = a.getTransformation(drawingTime, t, 1f);
...
if (more) {//成功
if (!a.willChangeBounds()) {//动画不会改变View的边界,如Alpha动画
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) {
parent.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
//不改变边界,绘制动画View的区域
parent.invalidate(mLeft, mTop, mRight, mBottom);
}
} else {//动画会改变View的边界,如Scale动画
if (parent.mInvalidateRegion == null) {
parent.mInvalidateRegion = new RectF();
}
//mInvalidateRegion改变区域
final RectF region = parent.mInvalidateRegion;
//该方法的目的就是改变mInvalidateRegion。
a.getInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop, region,
invalidationTransform);
parent.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
//region是动画过程中的改变区域
final int left = mLeft + (int) region.left;
final int top = mTop + (int) region.top;
parent.invalidate(left, top, left + (int) (region.width() + .5f),
top + (int) (region.height() + .5f));
}
}
return more;
}
Animation初始化,入参是动画视图宽高和父视图的宽高(貌似没用到)。
Animation#initializeInvalidateRegion方法将动画视图区域存储在内部的mPreviousRegion对象,动画视图增加PFLAG_ANIMATION_STARTED标志。
获取父视图内部mChildTransformation,将视图改变存储在Transformation的Matrix。
当动画未结束,会继续触发父视图#invalidate(区域)方法重绘。绘制区域包括两种情况,一种是动画未改变视图边界,如透明度动画,另一种是改变视图边界,如Scale动画,这两种情况绘制区域不同。但每一帧动画父视图都会Canvas重建。
willChangeBounds方法,判断边界是否改变,默认改变边界。例如,缩放动画ScaleAnimation会改变边界,透明度动画AlphaAnimation不会改变边界,需重写willChangeBounds方法。
若不改变视图边界,绘制区域是动画视图相对于父视图坐标系的边界坐标(mLeft, mTop, mRight, mBottom)。
若改变视图边界,子视图相对父视图的边界距离不会改变(mLeft/mTop)。子视图宽高不会改变(getHeight与getWidth获取)。
getInvalidateRegion方法,根据视图区域和Transformation变换获取当前需要改变的区域,在父视图中存储。改变的是mInvalidateRegion区域。
动画视图在父视图中绘制图。
动画改变分析
public boolean getTransformation(long currentTime, Transformation outTransformation) {
//若开始时间为-1,说明动画刚开始,设置开始事件为View绘制的当前时间
if (mStartTime == -1) {
mStartTime = currentTime;
}
final long startOffset = getStartOffset();
final long duration = mDuration;
float normalizedTime;
if (duration != 0) { //经历的时间占总耗时的比例
normalizedTime = ((float) (currentTime - (mStartTime + startOffset))) /(float) duration;
} else {
normalizedTime = currentTime < mStartTime ? 0.0f : 1.0f;
}
//比例大于等于1说明动画结束
final boolean expired = normalizedTime >= 1.0f;
mMore = !expired;
...
if ((normalizedTime >= 0.0f || mFillBefore) &&
(normalizedTime <= 1.0f || mFillAfter)) {
if (!mStarted) {
fireAnimationStart();
mStarted = true;
}
...
final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);
applyTransformation(interpolatedTime, outTransformation);
}
if (expired) {//动画结束,判断重复
if (mRepeatCount == mRepeated) {
} else {//重复动画
if (mRepeatCount > 0) {
mRepeated++;
}
...
}
}
...
return mMore;
}
首先,根据当前时间、开始时间和动画持续时间,计算动画已完成比例,判断动画完成,若比例>=1,说明动画执行已到达持续时间,expired失效,返回结束标志。
normalizedTime时间占比是动画从mStartTime(设置的开始时间)开始计算,已运行时间占总时间的比例。
其次,在动画运行过程中,触发applyTransformation方法,将视图变化写入Transformation内部Matrix,它是一个抽象方法,Animation的子类去实现不同类型动画。插值器Interpolator通过控制时间占比来计算动画运动的变化率。
最后,在动画开始和结束时,根据mStarted标志和重复标志,fireAnimationStart方法和fireAnimationEnd方法,调用动画AnimationListener监听器。若动画重复,自增mRepeated,mStartTime设值-1,重新计时。
下面以子类ScaleAnimation为例,分析实现动画改变的applyTransformation方法。
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
float sx = 1.0f;
float sy = 1.0f;
//获取Scale因子,在Anmiation类中。
float scale = getScaleFactor();
if (mFromX != 1.0f || mToX != 1.0f) {
sx = mFromX + ((mToX - mFromX) * interpolatedTime);
}
if (mFromY != 1.0f || mToY != 1.0f) {
sy = mFromY + ((mToY - mFromY) * interpolatedTime);
}
if (mPivotX == 0 && mPivotY == 0) {
t.getMatrix().setScale(sx, sy);
} else {
t.getMatrix().setScale(sx, sy, scale * mPivotX, scale * mPivotY);
}
}
视图在x轴Scale的初始比例mFromX,目标比例mToX,当值是1.0f是正常视图。若mFromX和mToX有一个不是1.0f,说明动画正在进行,发生变化。
sx是当前x轴Scale比例值,(mToX-mFromX)是Scale变化差值,interpolatedTime是根据Interpolation计算动画运行进度占比,实现动画速度控制。根据interpolatedTime、开始值(mFromX)和结束值(mToX),计算当前sx和sy。
Matrix#setScale方法,将sx和sy保存在底层Matrix。mPivotX与mPivotY是Scale的中心点,默认值是视图左上角坐标(0,0),中心点不在左上角时,将mPivotX和mPivotY一起保存。
保存Transformation后,getTransformation方法将返回是否动画的标志,接下来继续回到applyLegacyAnimation方法,根据运行标志,计算刷新边界,刷新父视图。下面看一下getInvalidateRegion方法,计算改变的区域。
public void getInvalidateRegion(int left, int top, int right, int bottom,
RectF invalidate, Transformation transformation) {
final RectF tempRegion = mRegion;
final RectF previousRegion = mPreviousRegion;
//先设置为视图View区域(以View坐标)
invalidate.set(left, top, right, bottom);
//底层变换
transformation.getMatrix().mapRect(invalidate);
invalidate.inset(-1.0f, -1.0f);
//invalidate区域值设置成内部mRegion
tempRegion.set(invalidate);
//与上次变换的得到的区域合并
invalidate.union(previousRegion);
//存储变换后的区域
previousRegion.set(tempRegion);
final Transformation tempTransformation = mTransformation;
final Transformation previousTransformation = mPreviousTransformation;
tempTransformation.set(transformation);
transformation.set(previousTransformation);
//存储此次变换
previousTransformation.set(tempTransformation);
}
入参是动画视图区域,以视图自己坐标系为标准坐标值(0,0,width,height), RectF区域invalidate在父视图保存,它是上一次动画视图帧的改变区域(变换+合并),将它重新传入,计算这次动画帧的改变区域。
首先,将invalidate设置成动画视图区域(动画视图坐标系),调用Matrix的JNI#native_mapRect(native_instance, dst, src)方法,底层mapRect方法的Matrix变换,将一个src区域(0,0,width,height)转换为一个新的dst目标区域。源区域src和目标区域都是invalidate。因此,转换后的区域保存在invalidate。根据动画运行时间,Matrix转换程度不同。
然后,变换后的目标区域invalidate与之前保存mPreviousRegion区域合并,mPreviousRegion默认初始化动画视图区域,此后,在动画过程中,专门存储每次变换后的区域。tempRegion缓存新dst目标区域,赋值给previousRegion,下一次变换后合并时使用。
最终,invalidate设置的值是以(0,0,width,height)区域进行变换,再+合并的改变区域。
改变区域的本质是动画视图在以自己为坐标系的坐标值(0,0,width,height)区域中,根据当前动画进度计算的Matrix,转换成一个新的坐标区域。
以Scale动画放大视图为例,若中心点是动画视图的中心(不是以左上角为中心),则得到的目标区域left/top是负值。新dst目标区域(-5,-5,width+5,height+5),这个是以动画视图自己坐标系的坐标值。
父视图绘制的区域,invalidate(区域)需要相对父视图的坐标系的坐标值,mLeft/mTop+新dst目标区域,即是动画当前的绘制区域(示意图绿色区域)。
总结
补间动画的核心本质是在一定的持续时间内,不断改变视图Matrix变换,并且不断刷新的过程。
在动画过程中,第一帧刷新父视图和动画视图,后面仅刷新父视图,刷新区域通过计算获取,invalidate(区域),父视图每一帧都会Canvas重建。
在动画启动时,第一帧动画视图Canvas重建,后续动画帧,动画视图不会再Canvas重建。
动画视图的兄弟视图不会Canvas重建,不会触发onDraw方法。
任重而道远