概述
视图动画支持4种动画效果,分别是translate,scale,rotate,alpha。
此外帧动画也属于视图动画。
基本使用方式
视图动画的实例化方式:
1.从xml加载:
Animation animation = AnimationUtils.loadAnimation(getContext(), R.anim.scale_animation);
scale_animation.xml:
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<scale xmlns:android="http://schemas.android.com/apk/res/android"
android:fromXScale="0.0"
android:toXScale="1.0"
android:fromYScale="0.0"
android:toYScale="1.0"
android:pivotX="50"
android:pivotY="50"
android:duration="700"
/>
</set>
2.直接java代码初始化
Animation animation = new ScaleAnimation(0,1,0,1,Animation.ABSOLUTE,50,Animation.ABSOLUTE,50);
视图动画使用:
视图动画怎么使用我这里就不详细说了,这里推荐一篇写的很好的博客,有需要的可以点进去看看:参考文章
优缺点
这里概括了几个视图动画的优缺点:
优点:
- 可以实现基本的缩放,旋转,平移,透明度变换,以及这四种变换的组合动画
缺点:
- 不能实现复杂动画效果。
- 可以实现组合动画(AnimationSet),但是组合动画只能同时播,不能控制动画播放的先后顺序
- 不能监听动画的执行过程,只能监听到动画开始,结束和重复事件。
原理解析
本文着重介绍视图动画的实现原理,结合分析源码讲解。
在分析源码前,先用一句话概括视图动画的原理:
当视图开启动画时,会触发视图重绘,在draw方法拿到该视图关联的Animation对象,把当前时间和一个存有Matrix的Transformation对象传给Animation,Animation会改变Transformation对象的Matrix,然后回传给视图,并且告诉视图是否动画已经结束,视图根据Matrix绘制图像,并且判断是否动画已经执行结束,如果没有就会继续调用invalidate方法重绘视图,然后又继续刚才的步骤,直到动画结束。
源码分析
下面是源码分析,直接看注释讲解:
1.开启动画。
/**
* Start the specified animation now.
*
* @param animation the animation to start now
*/
public void startAnimation(Animation animation) {
animation.setStartTime(Animation.START_ON_FIRST_FRAME);
//设置关联动画
setAnimation(animation);
invalidateParentCaches();
//触发视图重绘
invalidate(true);
}
2.执行到View的draw方法,注意该方法的参数带有当前的时间,判断动画是否已经结束会用到。
/**
* This method is called by ViewGroup.drawChild() to have each child view draw itself.
*
* This is where the View specializes rendering behavior based on layer type,
* and hardware acceleration.
*/
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
final boolean hardwareAcceleratedCanvas = canvas.isHardwareAccelerated();
//判断是否启用硬件加速绘制视图
boolean drawingWithRenderNode = mAttachInfo != null
&& mAttachInfo.mHardwareAccelerated
&& hardwareAcceleratedCanvas;
boolean more = false;
.........省去部分代码.........
//用于改变图像的Transformation
Transformation transformToApply = null;
boolean concatMatrix = false;
final boolean scalingRequired = mAttachInfo != null && mAttachInfo.mScalingRequired;
final Animation a = getAnimation();
if (a != null) {
//如果视图有关联动画,则交给处理动画
more = applyLegacyAnimation(parent, drawingTime, a, scalingRequired);
concatMatrix = a.willChangeTransformationMatrix();
if (concatMatrix) {
mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
}
//这时候拿到的Transformation 已经是经过动画变换后的,注意Transformation其实是存储在父控件的
transformToApply = parent.getChildTransformation();
}
........省去部分代码........
if (transformToApply != null) {
if (concatMatrix) {
if (drawingWithRenderNode) {
//如果启用了硬件加速则会走到这里
renderNode.setAnimationMatrix(transformToApply.getMatrix());
} else {
// Undo the scroll translation, apply the transformation matrix,
// then redo the scroll translate to get the correct result.
canvas.translate(-transX, -transY);
//拿到Transformation的矩阵绘制图像
canvas.concat(transformToApply.getMatrix());
canvas.translate(transX, transY);
}
parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
}
float transformAlpha = transformToApply.getAlpha();
if (transformAlpha < 1) {
alpha *= transformAlpha;
parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
}
}
........省去部分代码........
return more;
}
3.执行到View的applyLegacyAnimation方法。
/**
* Utility function, called by draw(canvas, parent, drawingTime) to handle the less common
* case of an active Animation being run on the view.
*/
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) {
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();
}
//拿到父控件的Transformation,通过改变它的值来实现视图变换
final Transformation t = parent.getChildTransformation();
//将Transformation交给Animation处理,返回值表示是否动画已经执行完毕
boolean more = a.getTransformation(drawingTime, t, 1f);
if (scalingRequired && mAttachInfo.mApplicationScale != 1f) {
if (parent.mInvalidationTransformation == null) {
parent.mInvalidationTransformation = new Transformation();
}
invalidationTransform = parent.mInvalidationTransformation;
a.getTransformation(drawingTime, invalidationTransform, 1f);
} else {
invalidationTransform = t;
}
if (more) {
//more为true表示动画还没结束
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;
//动画没结束需要继续调用invalidate绘制视图
parent.invalidate(mLeft, mTop, mRight, mBottom);
}
} else {
if (parent.mInvalidateRegion == null) {
parent.mInvalidateRegion = new RectF();
}
final RectF region = parent.mInvalidateRegion;
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;
}
4.执行到Animation的getTransformation方法,在这里Animation会做一些通用逻辑,比如计算动画已经执行的时间占比,算出经差值器转换后的比例值,然后将该值和outTransformation一起作为参数传给applyTransformation方法,在这里对outTransformation的Matrix做变换。applyTransformation方法需要各个子类实现。
/**
* Gets the transformation to apply at a specified point in time. Implementations of this
* method should always replace the specified Transformation or document they are doing
* otherwise.
*
* @param currentTime Where we are in the animation. This is wall clock time.
* @param outTransformation A transformation object that is provided by the
* caller and will be filled in by the animation.
* @return True if the animation is still running
*/
public boolean getTransformation(long currentTime, Transformation outTransformation) {
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 {
// time is a step-change with a zero duration
normalizedTime = currentTime < mStartTime ? 0.0f : 1.0f;
}
//当normalizedTime >= 1.0f或者取消了动画则表示动画过期,也就是动画结束
final boolean expired = normalizedTime >= 1.0f || isCanceled();
//如果动画没过期,则mMore赋值为true,告诉视图动画还没结束,需继续执行invalidate重绘
mMore = !expired;
if (!mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);
if ((normalizedTime >= 0.0f || mFillBefore) && (normalizedTime <= 1.0f || mFillAfter)) {
if (!mStarted) {
fireAnimationStart();
mStarted = true;
if (NoImagePreloadHolder.USE_CLOSEGUARD) {
guard.open("cancel or detach or getTransformation");
}
}
if (mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);
if (mCycleFlip) {
normalizedTime = 1.0f - normalizedTime;
}
//通过差值器将时间比例做了转换
//时间比例是线性的,但是实际动画执行时并不一定按照线性变化,通过差值器可以实现动画非线性变化。
final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);
//真正改变outTransformation矩阵的方法
applyTransformation(interpolatedTime, outTransformation);
}
........省去部分代码........
return mMore;
}
5.以ScaleAnimation动画为例,分析它的applyTransformation方法。
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
float sx = 1.0f;
float sy = 1.0f;
float scale = getScaleFactor();
if (mFromX != 1.0f || mToX != 1.0f) {
//根据interpolatedTime算出此时的缩放值,因为interpolatedTime不是直接等于时间变化的比例,
//而是由差值器根据时间变化的比例经过特定算法得出的,所以图像的缩放就不一定是随着时间线性变化。
sx = mFromX + ((mToX - mFromX) * interpolatedTime);
}
if (mFromY != 1.0f || mToY != 1.0f) {
sy = mFromY + ((mToY - mFromY) * interpolatedTime);
}
//通过改变Transformation的矩阵来改变图像
if (mPivotX == 0 && mPivotY == 0) {
//如果锚点为左上角,则直接缩放即可
t.getMatrix().setScale(sx, sy);
} else {
//如果锚点为特定的点,则除了根据锚点缩放
t.getMatrix().setScale(sx, sy, scale * mPivotX, scale * mPivotY);
}
}
在这里Transformation的矩阵已经被ScaleAnimation做了改变,那么当它继续执行到View的draw方法后面的部分,视图会拿到该矩阵,根据矩阵绘制图像,这样ScaleAnimation就间接改变了图像的绘制,因为这个过程是在一段时间内完成的,所以就形成了动画效果。
提个问题
视图动画会改变视图的尺寸吗?
不会,因为动画只是改变了draw的过程,并不会改变视图的尺寸大小,你看到的视图的大小改变了其实只是绘制区域改变了,但尺寸并没有变。
特别注意:
1.视图动画执行之后并未改变View的真实布局属性值。切记这一点,譬如,在界面上有一个Button在屏幕上方,我们设置了平移动画移动到屏幕下方然后保持动画最后执行状态,这时点击屏幕下方动画执行之后的Button是没有任何反应的,而点击原来屏幕上方没有Button的地方却响应了点击Button的事件。
2.Animation的动画效果其实是通过父View实现的。这也是为什么执行动画的View大小和位置不变,但是图像却可以绘制在View之外的原因,严格上来说是父View在执行动画,所以动画效果的作用范围是父View的显示区域。
自定义视图动画
经过上面的分析,我们知道视图动画最终起决定性作用的是applyTransformation方法,因为该方法可以改变视图绘制的图像。所以自定义视图动画的关键就是继承Animation并且实现applyTransformation方法,在这里实现你想要的动画效果,因为用到的不多,所以就不多说了。