1 前言
日常处理UI相关的开发过程中,我们不免要与动画打交道。通过动画,我们实现了酷炫的效果、高级的交互体验,完成了web网页不可替代的顺滑体验。Android基础库为我们提供了多种动画的实现方式,支持的功能也愈来愈强大。
- 作用范围上,动画不只作用在单个View上,也支持作用在ViewGroup、RecyclerView、ViewPager、Fragment、甚至Activity之间完成动画。
- 动画效果上,除了帧动画,以及基础的补间动画;属性动画的出现支持我们更加灵活的配置、处理;FlingAnimation、SpringAnimation的出现支持动画符合物理特性,更加自然生动。
2 总览

3 视图动画
视图动画又分为帧动画(Frame Animation)和补间动画(Tween Animation),帧动画可以去加载一系列可绘制的Drawable逐帧实现动画,补间动画可以执行一系列简单的变化,这些变化包括旋转、位移、缩放等等,他们都做用在单个view上。
3.1 帧动画
3.1.1 原理
帧动画的原理就是一组图片按照指定顺序、指定时间顺序展现的播放的图片。

3.1.2 实现方式
关键类:AnimationDrawable
实现方式
- 【Step 1 】 在 res/anim的文件夹里创建动画效果xml文件
<?xml version="1.0" encoding="utf-8"?>
<animation-list
xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="true" // 设置是否只播放一次,默认为false
>
<item android:drawable="@drawable/a0" android:duration="100"/>
<item android:drawable="@drawable/a1" android:duration="100"/>
</animation-list>
- 【Step 2 】在指定位置中设置动画执行
void startFrameAnimation() {
// 执行动画view
View targetView = findViewById(R.id.tag_desc);
// 获取动画对象
AnimationDrawable animationDrawable = (AnimationDrawable) (targetView).getDrawable();
// 启动动画
animationDrawable.start();
}
3.1.3 关键代码
private void setFrame(int frame, boolean unschedule, boolean animate) {
// 判断当前帧是否在
if (frame >= mAnimationState.getChildCount()) {
return;
}
// 选取该帧图片
selectDrawable(frame);
if (unschedule || animate) {
unscheduleSelf(this);
}
if (animate) {
// 定时绘制该帧图片
scheduleSelf(this, SystemClock.uptimeMillis() + mAnimationState.mDurations[frame]);
}
}
3.1.4 优势&弊端
优点:简单方便,通过帧动画可以实现一些特殊的动画效果
弊端:图片资源过多,容易引起OOM
3.2 补间动画
补间动画的四大子类包括:
- translate (平移动画) — TranslateAnimation
- scale (缩放动画) — ScaleAnimation
- rotate (旋转动画) — RotateAnimation
- alpha ( 透明度动画) — AlphaAnimation
3.2.1 原理
通过确定开始的视图样式 & 结束的视图样式、中间动画变化过程由系统补全来确定一个动画。
3.2.2 使用
初始化
支持xml设置和java动态设置,实现方式略。
组合动画
- 支持组合动画,AnimationSet继承于Animation,可以看做基本补间动画的容器集合。
- 支持设置多种基本补间动画,补间动画间的插值器可相互独立也可复用。
- AnimationSet的初始化、执行动画逻辑同普通Animation,不支持串行执行。
插值器
通过设置插值器的方式,可以是动画按照预期速率执行,除了系统插值器,还可以自定义插值器。

自定义插值器需要重写getInterpolation方法,以AccelerateInterpolator 插值器为例:
public class AccelerateInterpolator extends BaseInterpolator implements NativeInterpolatorFactory {
private final float mFactor;
private final double mDoubleFactor;
public AccelerateInterpolator() {
mFactor = 1.0f;
mDoubleFactor = 2.0;
}
public AccelerateInterpolator(float factor) {
mFactor = factor;
mDoubleFactor = 2 * mFactor;
}
public float getInterpolation(float input) {
if (mFactor == 1.0f) {
return input * input;
} else {
return (float)Math.pow(input, mDoubleFactor);
}
}
}
getInterpolation()方法中,input传入是均匀等分的float字段,通过实现x^2的函数,达到了逐渐加速的效果。
Input=0.1 返回0.01,input=0.9 返回0.81,input=1 返回1
参考文档
3.2.3 关键代码
整体流程

- start逻辑
/**
* 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);
}
- view绘制逻辑
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
final Animation a = getAnimation();
if (a != null) {
// 执行动画的核心逻辑
more = applyLegacyAnimation(parent, drawingTime, a, scalingRequired);
// 是否需要改变边界martix,除alpha动画外均需要
concatMatrix = a.willChangeTransformationMatrix();
if (concatMatrix) {
mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
}
// 返回值非空
transformToApply = parent.getChildTransformation();
} else {
// ...省略
}
// more为false 通知父view动画结束,修改flag值
if (a != null && !more) {
if (!hardwareAcceleratedCanvas && !a.getFillAfter()) {
onSetAlpha(255);
}
parent.finishAnimatingView(this, a);
}
// more为true 动画状态中
if (more && hardwareAcceleratedCanvas) {
// 含有alpha动画需要通知子view更新动画
if (a.hasAlpha() && (mPrivateFlags & PFLAG_ALPHA_SET) == PFLAG_ALPHA_SET) {
invalidate(true);
}
// 否则子view无需重建canvas
}
if (transformToApply){
// 进行一些绘制操作
}
}
}
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初始化
}
// 动画内核心执行逻辑
boolean more = a.getTransformation(drawingTime, t, 1f);
...
if (more) {//成功
// 不管是否需要改变边界值,都通知父view更新
if (!a.willChangeBounds()) {//动画不会改变View的边界,如Alpha动画
parent.invalidate(mLeft, mTop, mRight, mBottom);
} else {//动画会改变View的边界,如Scale动画
parent.invalidate(left, top, left + (int) (region.width() + .5f),
top + (int) (region.height() + .5f));
}
}
return more;
}
- Animation相关逻辑
public boolean getTransformation(long currentTime, Transformation outTransformation) {
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;
// More标识动画是否结束
mMore = !expired;
...
// todo mFillBefore如何保证动画结束不改变view布局 没找到具体逻辑,只找到了废弃的逻辑
if ((normalizedTime >= 0.0f || mFillBefore) && (normalizedTime <= 1.0f || mFillAfter)) {
// 通过插值器获取当前时间点的相对比值
final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);
// 执行动画核心逻辑,不同animation需重写此方法。
applyTransformation(interpolatedTime, outTransformation);
}
//动画结束
if (expired) {
if (mRepeatCount == mRepeated) {
// 通知动画结束
} else {
//判断repeat,reverse等逻辑
}
}
...
return mMore;
}
总结
补间动画的核心本质是在一定的持续时间内,不断改变视图Matrix变换,并且不断刷新的过程。
在动画过程中,第一帧刷新父视图和动画视图。
父视图每一帧都会Canvas重建。
在动画启动时,第一帧动画视图Canvas重建,后续若包含alpha动画,动画视图也会重复invalidate,否则不会再Canvas重建。
动画视图的兄弟视图不会Canvas重建,不会触发onDraw方法。
问题解疑
- 调用startAnimation会立即执行动画嘛?
- 不会,会先更新设置值,通过调用父view的invalidate,通知到跟布局遍历view tree进行绘制,触发动画逻辑。
- Animation重复调用startAnimation会发生什么?
- 可以看到每次startAnimation都会重新设置flag值及动画的标记信息,重复调用的表现是:
- 若重复调用的时间 < 动画执行时长,表现可能为旧动画被新动画覆盖,或是不会重复执行动画,仅会在最后一次调用startAnimation后执行。
- 若重复调用的时间 > 动画执行时长,表现是连续多次执行动画。
- 可以看到每次startAnimation都会重新设置flag值及动画的标记信息,重复调用的表现是:
- Animation创建销毁是否会有内存泄露?
- 不会,view中持有Animation,在view的onDetachedFromWindow方法会取消掉动画。
优势 & 弊端
优势:操作简单,可实现动画效果丰富,支持叠加动效,动画与view绑定,不会产生泄露隐患。
弊端:仅与View绑定,无法针对其他对象做动画。无法做更高级的动画,如变色、按照贝塞尔曲线移动等。
3.3 布局动画
布局动画是在viewgroup层级上的动画,通过设置布局动画可以给容器中的控件统一设置进入、进出动画等。
- 布局动画主要分为:
- layoutAnimation
- gridLayoutAnimation
- animateLayoutChanges
- LayoutTransition
3.3.1 原理
以layoutAnimation为例,其原理类似于补间动画,其设置、调用方法在viewgroup中,通过设置标记值在onDraw中遍历viewTree进行动画变换。
- 区别
- layoutAnimation:作用于viewgroup,动画只能使用补间动画
- gridLayoutAnimation:给网格布局使用的,动画只能使用补间动画
- animateLayoutChanges:这是在 ViewGroup 创建显示之后,内容改变时对于新的内容做动画,和 layoutAnimation 是配合使用的。
- LayoutTransition:作用范围 = layoutAnimation + animateLayoutChanges。
- 系统中支持以下5种状态变化:
- 1、APPEARING:容器中出现一个视图。
- 2、DISAPPEARING:容器中消失一个视图。
- 3、CHANGING:布局改变导致某个视图随之改变,例如调整大小,但不包括添加或者移除视图。
- 4、CHANGE_APPEARING:其他视图的出现导致某个视图改变。
- 5、CHANGE_DISAPPEARING:其他视图的消失导致某个视图改变。 动画只能使用属性动画
3.3.2 使用
以LayoutAnimation为例:初始化,支持xml设置 & java动态设置
4 属性动画
由于补间动画一些限制问题(动画仅可作用于View,无法自定义动画,无法真实的改变view的位置…),Android在3.0版本当中引入属性动画这个功能,它不再是针对于View来设计的了,也不限定于只能实现移动、缩放、旋转和淡入淡出这几种动画操作,同时也不再只是一种视觉上的动画效果了。
由Animator可以看出,ValueAnimator是属性动画的关键类,下面将以ValueAnimator为例进行介绍。
4.1 原理
在一定时间间隔内,通过不断对值进行改变,并不断将该值赋给对象的属性,从而实现任意对象在该属性上的动画效果。
4.2 使用
支持xml和JAVA动态设置的方式。实际开发中,建议使用Java代码实现属性动画:因为很多时候属性的起始值是无法提前确定的(无法使用XML设置),需要在Java代码里动态获取。
4.2.1 初始化
ValueAnimtor重要初始化方法
- ValueAnimator.ofInt(int… values)
- ValueAnimator.ofFloat(float… values)
- ValueAnimator.ofArgb(int… values)
- ValueAnimator.ofPropertyValuesHolder(PropertyValuesHolder… values)
- ValueAnimator.ofObject(TypeEvaluator evaluator, Object… values)
区别在于初始值~结束值可以以int型/float型/… 过渡到结束值,分别对应了不用的估值器。这里见到了一些新的名词,下一小节进行介绍。
ValueAnimtor也可通过xml进行设置,通过AnimatorInflater.loadAnimator()方法进行载入。
4.2.2 估值器
什么是估值器TypeEvaluator?
估值器类似于差值器,是辅佐动画执行的重要类。他的作用是设置初始值→ 结束值之间过渡的具体逻辑。
估值器进可作用于属性动画。
- 估值器 VS 插值器
- 插值器(Interpolator)决定 值 的变化模式(匀速、加速blabla)
- 估值器(TypeEvaluator)决定 值 的具体变化数值
用法
同样的,我们可以通过重写evaluate方法自定义估值器实现特殊功能。Android也为我们实现了几种常用的估值方法,下面以ArgbEvaluator和FloatEvaluator为例。
- ArgbEvaluator
public Object evaluate(float fraction, Object startValue, Object endValue) {
int startInt = (Integer) startValue;
float startA = ((startInt >> 24) & 0xff) / 255.0f;
float startR = ((startInt >> 16) & 0xff) / 255.0f;
float startG = ((startInt >> 8) & 0xff) / 255.0f;
float startB = ( startInt & 0xff) / 255.0f;
int endInt = (Integer) endValue;
float endA = ((endInt >> 24) & 0xff) / 255.0f;
float endR = ((endInt >> 16) & 0xff) / 255.0f;
float endG = ((endInt >> 8) & 0xff) / 255.0f;
float endB = ( endInt & 0xff) / 255.0f;
// 默认线性变换,计算中间色值
float a = startA + fraction * (endA - startA);
float r = startR + fraction * (endR - startR);
float g = startG + fraction * (endG - startG);
float b = startB + fraction * (endB - startB);
return Math.round(a) << 24 | Math.round(r) << 16 | Math.round(g) << 8 | Math.round(b);
}
- FloatEvaluator
public Float evaluate(float fraction, Number startValue, Number endValue) {
float startFloat = startValue.floatValue();
// 返回对应的float中间值
return startFloat + fraction * (endValue.floatValue() - startFloat);
}
4.2.3 Keyframe
- 什么是Keyframe?
- 顾名思义,他代表了关键帧。 前面讲到我们可以通过设置插值器或插值器修改动画速率,但这些都需要有一定的数学基础来设计速率曲线。为拯救广大学渣,keyframe诞生了!
- 使用
- 从构造函数可以看出,接受两个传参(float fraction,T value),即置顶关键帧的value值,系统通过多组keyframe来计算速率。
4.2.4 PropertyValuesHolder
- 什么是PropertyValuesHolder?
它其中保存了动画过程中所需要操作的属性和对应的值。
开发者文档:https://developer.android.com/reference/android/animation/PropertyValuesHolder.html
- 初始化
以ofFloat为例,他支持传入动画属性对象or属性名称。


4.3 关键代码
我们还是从调用启动动画animator.start()开始看起
private void start(boolean playBackwards) {
if (Looper.myLooper() == null) {
throw new AndroidRuntimeException("Animators may only be run on Looper threads");
}
mReversing = playBackwards;
mSelfPulse = !mSuppressSelfPulseRequested;
// 如果需要反向执行动画,反向修改当前进度值
if (playBackwards && mSeekFraction != -1 && mSeekFraction != 0) {
// ..
}
// 修改标记值
mStarted = true;
mPaused = false;
mRunning = false;
mAnimationEndRequested = false;
// Resets mLastFrameTime when start() is called, so that if the animation was running,
// calling start() would put the animation in the
// started-but-not-yet-reached-the-first-frame phase.
mLastFrameTime = -1;
mFirstFrameTime = -1;
mStartTime = -1;
// 关键方法
addAnimationCallback(0);
if (mStartDelay == 0 || mSeekFraction >= 0 || mReversing) {
// 若无需延迟,直接执行动画,这里做了些初始化和通知回调的逻辑
startAnimation();
// 更新fraction
setCurrentFraction(mSeekFraction);
}
}
start方法中主要是进行一些变量的初始化,及回调的通知(startAnimation()),跟进addAnimationCallback方法,可以近一步看到执行动画逻辑。
private void addAnimationCallback(long delay) {
if (!mSelfPulse) {
return;
}
// AnimationHandler是一个单例
getAnimationHandler().addAnimationFrameCallback(this, delay);
}
private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
// 执行本帧动画
doAnimationFrame(getProvider().getFrameTime());
// 持续执行下一帧动画
if (mAnimationCallbacks.size() > 0) {
getProvider().postFrameCallback(this);
}
}
};
/**
* Register to get a callback on the next frame after the delay.
*/
public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
// 若第一次执行动画,或之前动画已执行完
if (mAnimationCallbacks.size() == 0) {
getProvider().postFrameCallback(mFrameCallback);
}
if (!mAnimationCallbacks.contains(callback)) {
mAnimationCallbacks.add(callback);
}
if (delay > 0) {
mDelayedCallbackStartTime.put(callback, (SystemClock.uptimeMillis() + delay));
}
}
动画执行的逻辑比较明朗了,那么我们就跟进doAnimationFrame()方法看下属性动画每一帧是怎么执行的。
private void doAnimationFrame(long frameTime) {
long currentTime = SystemClock.uptimeMillis();
final int size = mAnimationCallbacks.size();
// 遍历动画队列(mAnimationCallbacks)
for (int i = 0; i < size; i++) {
final AnimationFrameCallback callback = mAnimationCallbacks.get(i);
if (callback == null) {
continue;
}
// 判断当前时间是否到达(需要延迟返回false)
if (isCallbackDue(callback, currentTime)) {
// 这里调用了animator的doAnimationFrame方法
callback.doAnimationFrame(frameTime);
if (mCommitCallbacks.contains(callback)) {
getProvider().postCommitCallback(new Runnable() {
@Override
public void run() {
commitAnimationFrame(callback, getProvider().getFrameTime());
}
});
}
}
}
// 清理动画队列mAnimationCallbacks
cleanUpList();
}
/**
* Processes a frame of the animation, adjusting the start time if needed.
*
* @param frameTime The frame time.
* @return true if the animation has ended.
* @hide
*/
public final boolean doAnimationFrame(long frameTime) {
// 处理 pause/resume
if (mPaused) {
// ...
return false
} else if (mResumed) {
// ...
}
// 允许动画已被触发过
if (mLastFrameTime < 0) {
if (mSeekFraction >= 0) {
long seekTime = (long) (getScaledDuration() * mSeekFraction);
mStartTime = frameTime - seekTime;
mSeekFraction = -1;
}
mStartTimeCommitted = false; // allow start time to be compensated for jank
}
// 重要代码
boolean finished = animateBasedOnTime(currentTime);
// 若动画结束,进行一些清理工作
if (finished) {
endAnimation();
}
return finished;
}
boolean animateBasedOnTime(long currentTime) {
// 是否完成动画
boolean done = false;
if (mRunning) {
// 是否全部循环执行完
final boolean lastIterationFinished = (fraction >= mRepeatCount + 1) &&
(mRepeatCount != INFINITE);
// 判断是否已结束
if (scaledDuration == 0) {
// 0 duration animator, ignore the repeat count and skip to the end
done = true;
} else if (newIteration && !lastIterationFinished) {
// Time to repeat
// ...
} else if (lastIterationFinished) {
done = true;
}
// 获取当前进度值
float currentIterationFraction = getCurrentIterationFraction(
mOverallFraction, mReversing);
// 关键方法
animateValue(currentIterationFraction);
}
return done;
}
void animateValue(float fraction) {
// 根据插值器重新获取进度值
fraction = mInterpolator.getInterpolation(fraction);
mCurrentFraction = fraction;
// 通过PropertyValuesHolder计算当前进度下的值
int numValues = mValues.length;
for (int i = 0; i < numValues; ++i) {
mValues[i].calculateValue(fraction);
}
// 通知update回调,返回最新进度
if (mUpdateListeners != null) {
int numListeners = mUpdateListeners.size();
for (int i = 0; i < numListeners; ++i) {
mUpdateListeners.get(i).onAnimationUpdate(this);
}
}
}
总结
属性动画不是与View强绑定的,它作用于任意对象。
通过AnimationHandler维持当前项目内全部待执行的属性动画,依赖Choreographer.FrameCallback回调在每次刷新屏幕时处理动画逻辑。
动画处理逻辑是:
遍历handler中全部animator,若当前帧需要执行动画,更新对应的PropertyValuesHolder。
4.4 其他animator
4.4.1 ObjectAnimator
ObjectAnimator可以直接的将动画作用于对象。
相比较于ValueAnimator往往需要外部设置updateListener去手动的设置对象值,ObjectAnimator支持设置目标对象,并自动的更新对象相关值。可以看做ValueAnimator的升级版。
初始化
public static ObjectAnimator ofInt(Object target, String propertyName, int... values) {
ObjectAnimator anim = new ObjectAnimator(target, propertyName);
anim.setIntValues(values);
return anim;
}
private ObjectAnimator(Object target, String propertyName) {
setTarget(target);
setPropertyName(propertyName);
}
执行动画关键代码
void animateValue(float fraction) {
final Object target = getTarget();
// 获取目标对象,对不符合预期的情况进行取消
if (mTarget != null && target == null) {
// We lost the target reference, cancel and clean up. Note: we allow null target if the
/// target has never been set.
cancel();
return;
}
super.animateValue(fraction);
int numValues = mValues.length;
// 自动修改对象值
for (int i = 0; i < numValues; ++i) {
mValues[i].setAnimatedValue(target);
}
}
4.4.2 TimeAnimator
这个Animator主要就提供了一个setTimeListner的方法。它没有时间,插值或是对象值设定。回调监听器为每一帧动画接受信息,包括总运行时间和从前一帧到现在的运行时间.
4.4.3 LottieAnimator
服务于lottie的属性动画,目前作用在lottie渲染动画类LottieDrawable中,进行lottie动画的一些设置。
4.4.4 AnimatorSet
属性动画的动画集合,相比较AnimationSet,对动画的设置更加灵活。
-
支持序列化执行动画
-
支持同时执行动画 animatorSet.playTogether(objectAnimator,objectAnimator1);
-
支持各动画独立设置属性,但对于animatorSet设置的属性(duration、target、插值器等)优先级更高,会覆盖带个动画相关属性的设置。
4.5 优势&弊端
优势:功能强大,支持更多种、丰富的动效,支持对非view对象进行操作。
弊端:操作时需要更加关注初始化&销毁逻辑,易引发内存泄露。
5 几点建议
5.1 性能优化
整体思路:
对于复杂动画需要叠加多个Animator,减小Animator量级,进而减少AnimationHandler中持有的动画队列的量级,减少动画更新过程中整体的迭代次数。
针对同一个对象,进行复杂动画时,善用PropertyValuesHolder 和 Keyframe or 插值器或ViewPropertyAnimator。 示例1:同时为一个对象设置多种属性动画,通过PropertyValuesHolder设置方式,可以使叠加动画统一成一个Animator。
//传统写法
ObjectAnimator tX = ObjectAnimator.ofFloat(textView, "translationX", currentX, -300, currentX);
ObjectAnimator tY = ObjectAnimator.ofFloat(textView, "translationY", currentY, 200, currentY);
AnimatorSet set = new AnimatorSet();
set.setDuration(1000).playTogether(tX, tY);
set.start();
//一个view同时发生多种效果时,建议使用PropertyValuesHolder,这样ObjectAnimator只有一个对象
PropertyValuesHolder pvhX = PropertyValuesHolder.ofFloat("translationX", currentX, 500, currentX);
PropertyValuesHolder pvhY = PropertyValuesHolder.ofFloat("translationY", currentY, 400, currentY);
ObjectAnimator.ofPropertyValuesHolder(textView, pvhX, pvhY).setDuration(1000).start();
示例2:为一个对象设置同一属性动画的变速操作,建议使用Keyframe or 插值器整合。
//传统写法
ObjectAnimator transYFirstAnim = ObjectAnimator.ofFloat(textView, "translationY", 0, 100);
ObjectAnimator transYSecondAnim = ObjectAnimator.ofFloat(textView, "translationY", 100, 0);
AnimatorSet animSet = new AnimatorSet();
animSet.playSequentially(transYFirstAnim, transYSecondAnim);
//一个view的单个属性先后发生一系列变化时,建议使用Keyframe达到效果。从词义上来理解Keyframe是关键帧
Keyframe k0 = Keyframe.ofFloat(0f, 0); //第一个参数为“何时”,第二个参数为“何地”
Keyframe k1 = Keyframe.ofFloat(0.5f, 100);
Keyframe k2 = Keyframe.ofFloat(1f, 0);
PropertyValuesHolder p = PropertyValuesHolder.ofKeyframe("translationY", k0, k1, k2);
ObjectAnimator objectAnimator = ObjectAnimator.ofPropertyValuesHolder(textView, p);
objectAnimator.start();
针对不同对象进行复杂动画,建议使用ValueAnition进行动画速率控制,外部设置updateListener对不同对象进行统一操作。
进度回调建议使用AnimatorListenerAdapter代替AnimatorListener 由于AnimatorListener是接口,所以实现它得实现它所有的方法,而我们有时只用到它的个别回调(如:onAnimationStart),使用它会导致代码看起来非常冗杂。
而AnimatorListenerAdapter是默认实现了AnimatorListener的一个抽象类,你可以按需要重写其中的方法,代码会优雅一点。
5.2 StackOverflow
我们已知属性动画时由一个全局的单例中持有的,只有动画结束或者被cancel掉,才会从队列中移除。
如果我们将动画设置成了极易循环播放,引发内存泄露。
建议: Activity/Fragment生命周期结束时,如onDestroy方法中,及时的cancel掉并置空属性动画。 View内部动画,在onDetachedFromWindow,及时的cancel掉并置空属性动画。 若View内部动画较简单,建议使用补间动画。
5.3 OOM
帧动画中,图片资源过大易引起OOM。由于加在动画xml时系统会按照定义顺序把所有的图片都读取到内存中,易导致 OOM 的发生。
思路: 减少图片资源大小 建立异步线程逐个加载图片,并通过handler发送到主线程中,减少同时读取的情况 用属性动画 or lottie等动画代替帧动画
5.4 补充建议
- 尽量不要在刷新时做耗时操作,必须准备数据,创建图片,图片变换等,数据和图片都应该在之前就加载到内存中,图片变换用canvas的变换来实现。
- 同一个界面中多个动画重叠出现时,尽量将动画的刷新过程统一进行刷新,避免频繁的invalidate,尤其是多个动画有时序上的关系时更应该统一。
- 尽量使用带有参数的invalidate来刷新,这样可以减少很多运算量。
- 开启硬件加速,硬件加速由于采用了显示列表的概念,所以刷新过程也有很大的优化,但是会增加额外的8M内存占用。