一、前言
关于什么是动画,动画的相关概念等等这里就不讲了。这里仅表述一下个人观点,个人认知是:
1.动画增加了 UI 展示的动态性,使得UI看起来更具生机。同时,一些酷炫的动画一定程度上也会提高应用的 Bigger。但这里要把握一个度,一个页面中不宜有过多的动画,1 到 2 个明显的即可,尤其动画不能掩盖业务的主旨。
2.动画更好的表达了用户操作的反馈,同时也能更好的给用户以指导操作。
下面的思维导图展示了 Android 原生所支持的主要动画分类,概念以及相关场景。
这里概括了 Android 中 8 种主要的动画,就目前而言,其中最常用的便是属性动画(视图动画现在应该用的少了),转场动画以及视图状态动画,其余动画基本上我们是很少接触的,主要是国内的应用开发中不实用。
当然,除了 Android 原生动画之外,目前第 3 方的 airbnb/lottie-android 动画方案,更是给我们提供了跨平台的动画解决方案。
在这篇文章里,我们先主要来看看属性动画。
二、属性动画概览
1.基本概念
属性动画系统是一个强大的框架,允许您使用几乎任何对象来作动画,甚至不用管它是否是绘制到屏幕上的一个View。其主要的原理就是通过定义动画以随时间变化而更改对象的某一属性。更简单地说,就是修改对象的某个属性值来实现动画的。
2.主要特征
持续时间:您可以指定动画的持续时间。默认长度为300毫秒。
时间插值:您可以指定如何计算属性值作为动画当前已用时间的函数。
重复计数和行为:您可以指定是否在到达持续时间结束时重复动画以及重复动画的次数。您还可以指定是否要反向播放动画。将其设置为反向向前播放动画然后反复播放动画,直到达到重复次数。
动画设置:您可以将动画分组为一起或按顺序或在指定延迟后播放的逻辑集。
帧刷新延迟:您可以指定刷新动画帧的频率。默认设置为每10毫秒刷新一次,但应用程序刷新帧的速度最终取决于系统整体的繁忙程度以及系统为基础计时器提供服务的速度。
这是翻译自官网对属性动画的特征概括,但稍有经验的你不难发现,其他类型的动画都会有此概念。
3.框架类图
如上面类图所见,属性动画主要被分成 3 个部分,用于组织动画的 Animator,用于计算时间插值的 插值器 以及用于计算结果的 估值器。
4.插值器Interpolator
插值器的作用是使得属性值根据时间的变化从初始值过渡到结束值的变化规律。其实质是一个数学函数 y = f(x),定义域 x 属于 (0.0,1.0) 的 float 值,值域 y 也是 (0.0,1.0) 的 float 值,曲线的斜率是速度。如上图框架类中,Android 为我们定义了 10 个内置的插值器,其基本上满足了我们大部分的需求。当然,我们也可以自定义来实现自己的插值器。
下面通过 2 个简单的 gif 动画来粗略的感受一下插值器。
5.估值器
估值器也是一个函数,插值器是得到时间的变化规律,而估值器则是根据时间的变化规律计算得到每一步的运算结果。在动画的监听中根据计算的结果来改变目标的属性值。利用估值器我们可以实现一些曲线运动如抛物线,贝塞尔曲线等动画。下图是通过自定义估值器实现的一个简单的曲线运动。
三、源码分析
不管是 Android 源码还是其他第三方的源码框架,其代码构成往往都是比较繁多且复杂的。我分析代码的一个简单的方法论就是先写一个最简单的 demo,然后根据 demo 沿着调用链来分析其主体流程,在分析的过程中再慢慢补齐相关的概念,模块甚至是重要的细节部分。
1.demo
以 ObjectAnimator 为例来写一个简单的右移动画。
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(imageViewMove,"translationX",0,100)
.setDuration(1 * 1000);
objectAnimator.setInterpolator(new LinearInterpolator());
objectAnimator.start();
2.代码分析
先来看看时序图,我们将按照时序的图的顺序一步一步来分析。
创建动画
创建动画就是创建动画运行的基础或者说是条件,前面 1 - 9 步都可以说是在创建动画的基础。从 ObjectAnimator.ofFloat()开始。
/**
* 构建一个返回值为 float 的 ObjectAnimator 的实例
*
* @param target 作用于动画的对象。
* @param propertyName 属性名称,要求对象须有setXXX() 方法,且是 public 的。
* @param values,属性变化的值,可以设置 1 个或者 多个。当只有 1 个时,起始值为属性值本身。当有 2 个值时,第 1 个为起始值,第 2 个为终止值。当超过 2 个时,首尾值的定义与 2 个时一样,中间值做需要经过的值。
*/
public static ObjectAnimator ofFloat(Object target, String propertyName, float... values) {
ObjectAnimator anim = new ObjectAnimator(target, propertyName);
anim.setFloatValues(values);
return anim;
}
该方法的参数以作用都已经放在注释里面了。而这个方法里面所做的事情是,首先创建一个 ObjectAnimator 的实例,然后为该实例设置 values。那么,继续看 ObjectAnimator 的构建。
构造 ObjectaAnimator
private ObjectAnimator(Object target, String propertyName) {
setTarget(target);
setPropertyName(propertyName);
}
分别调用了 setTarget() 方法和setPropertyName()
setTarget()
public void setTarget(@Nullable Object target) {
final Object oldTarget = getTarget();
if (oldTarget != target) {
if (isStarted()) {
cancel();
}
mTarget = target == null ? null : new WeakReference<Object>(target);
// New target should cause re-initialization prior to starting
mInitialized = false;
}
}
存在旧动画对象(也可为 null) 与新设置的动画对象不一致,如果旧动画是开始了的状态,则先取消动画,然后将动画对象以弱引用对象为记录下来。
setPropertyName()
public void setPropertyName(@NonNull String propertyName) {
// mValues could be null if this is being constructed piecemeal. Just record the
// propertyName to be used later when setValues() is called if so.
if (mValues != null) {
PropertyValuesHolder valuesHolder = mValues[0];
String oldName = valuesHolder.getPropertyName();
valuesHolder.setPropertyName(propertyName);
mValuesMap.remove(oldName);
mValuesMap.put(propertyName, valuesHolder);
}
mPropertyName = propertyName;
// New property/values/target should cause re-initialization prior to starting
mInitialized = false;
}
主要就是记录下 propertyName 的名字。而如果已经有这个 propertyName,则会替换其相应的 PropertyValuesHolder,这里用了一个 HashMap 来保存 propertyName 和 PropertyValuesHolder。关于 PropertyValuesHolder 先来看看其类图结构。对于属性动画来说,其属性相关的变量都被封装在了 PropertyValuesHolder 里。
这里我们还要记住的是 propertyName 是 "translationX"。接下来看 setFloatValues() 方法。
setFloatValues()
@Override
public void setFloatValues(float... values) {
if (mValues == null || mValues.length == 0) {
// 当前还没有任何值
if (mProperty != null) {
setValues(PropertyValuesHolder.ofFloat(mProperty, values));
} else {
setValues(PropertyValuesHolder.ofFloat(mPropertyName, values));
}
} else {
// 当前已经有值的情况,调用父类的 setFloatValues()
super.setFloatValues(values);
}
}
父类,即 ValueAnimator ,其方法setFloatValues() 如下。
ValueAnimator#setFloatValues()
public void setFloatValues(float... values) {
if (values == null || values.length == 0) {
return;
}
if (mValues == null || mValues.length == 0) {
setValues(PropertyValuesHolder.ofFloat("", values));
} else {
PropertyValuesHolder valuesHolder = mValues[0];
valuesHolder.setFloatValues(values);
}
// New property/values/target should cause re-initialization prior to starting
mInitialized = false;
}
这里可以看出,不管是否调用父类的 setFloatValues()。最后都是要将 values 逐个构造成 PropertyValuesHolder,最后存放在前面所说的 HashMap 里面。当然,如果这里的 hashMap 还没有初始化,则先会将其初始化。所以这里面最关键的是要构建出 PropertyValuesHolder 这个对象。那么就继续来看看 PropertyValuesHolder#ofFloat() 方法。
PropertyValuesHolder#ofFloat()
public static PropertyValuesHolder ofFloat(String propertyName, float... values) {
return new FloatPropertyValuesHolder(propertyName, values);
}
构造 FloatPropertyValuesHolder,当然,这里相应的还有 IntPropertyValuesHolder、MultiIntValuesHolder以及MultiFloatValuesHolder,都是 PropertyValuesHolder 的子类。这里就只关注 FloatPropertyValuesHolder 吧。
FloatPropertyValuesHolder
public FloatPropertyValuesHolder(String propertyName, float... values) {
super(propertyName);
setFloatValues(values);
}
FloatPropertyValuesHolder 构造函数比较简单,调用父类的构造方法并传递了 propertyName,关键是进一步 setFloatValues() 方法的调用,其又进一步调用了父类的 setFloatValues(),在父类的 setFloatValues() 方法里初始化了动画的关键帧。
PropertyValuesHolder#setFloatValues()
public void setFloatValues(float... values) {
mValueType = float.class;
mKeyframes = KeyframeSet.ofFloat(values);
}
进一步调用了 KeyframeSet#ofFloat() 方法以完成关键帧的构造。KeyframeSet 是接口 Keyframe 的实现类。
KeyframeSet#ofFloat()
public static KeyframeSet ofFloat(float... values) {
boolean badValue = false;
int numKeyframes = values.length;
// 至少要 2 帧
FloatKeyframe keyframes[] = new FloatKeyframe[Math.max(numKeyframes,2)];
// 然后构造出每一帧,每一帧中主要有 2 个重要的参数 fraction 以及 value
if (numKeyframes == 1) {
keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f);
keyframes[1] = (FloatKeyframe) Keyframe.ofFloat(1f, values[0]);
if (Float.isNaN(values[0])) {
badValue = true;
}
} else {
keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f, values[0]);
for (int i = 1; i < numKeyframes; ++i) {
keyframes[i] =
(FloatKeyframe) Keyframe.ofFloat((float) i / (numKeyframes - 1), values[i]);
if (Float.isNaN(values[i])) {
badValue = true;
}
}
}
if (badValue) {
Log.w("Animator", "Bad value (NaN) in float animator");
}
// 最后将所有的 关键帧 汇集到一个集合中
return new FloatKeyframeSet(keyframes);
}
这段代码看起来多,但其实结构很简单,其主要内容是:
(1) 构造动画的关键帧,且动画里至少要有 2 个关键帧。
(2) 关键帧中有 2 个重要的参数,fraction这个可以看成是关键帧的序号,value 关键帧的值,可能是起始值,也可能是中间的某个值。
(3) 最后将关键帧汇集成一个关键帧集返回给 PropertyValuesHolder。
到这里就完成创建动画的 1 ~ 6 步,其主要是 2 件事情,属性封装与关键帧构建。接下来继续看 setDuration() 与 setInterpolator() 方法。
setDuration()
@Override
@NonNull
public ObjectAnimator setDuration(long duration) {
super.setDuration(duration);
return this;
}
调用了父类 ValueAnimator 的 setDuration()。
ValueAnimator#setDuration()
@Override
public ValueAnimator setDuration(long duration) {
if (duration < 0) {
throw new IllegalArgumentException("Animators cannot have negative duration: " +
duration);
}
mDuration = duration;
return this;
}
setDuration() 只是简单的存储下 duration 的值,仅此而已,那么继续分析 setInterpolator()。
setInterpolator()
@Override
public void setInterpolator(TimeInterpolator value) {
if (value != null) {
mInterpolator = value;
} else {
mInterpolator = new LinearInterpolator();
}
}
setInterpolator() 方法也很简单,只是简单的存储,并且如果传递的是 null 的话,则默认使用的便是 LinearInterpolator,即线性插值器。我们这里的假设的场景也是设置了 LinearInterpolator,这是最简单的插值器,其作用就是完成匀速运动。这里借助 LinearInterpolator 来分析一下插值器。
关于插值器的概念已经在第二节第 4 小节里面有介绍了。且在第 3 小节中的框架类图中也完整的描述了插值器的继承关系。其最关键的定义就在 TimeInterpolator 这个接口中,来看看这个接口。
/**
* 插值器定义了动画变化的频率,其可以是线性的也可以是非线性的,如加速运动或者减速运动。
*/
public interface TimeInterpolator {
/**
* 这里传进来的 input 代表当前时间与总时间的比,根据这个时间占比返回当前的变化频率。其输出与输值都在 [0,1] 之间。
*/
float getInterpolation(float input);
}
插值器的关键定义便是实现 getInterpolation() 方法,即根据当前动画运行的时间占比来计算当前动画的变化频率。那么来看看 LinearInterpolator 的 getInterpolation() 实现。
LinearInterpolator#getInterpolation()
public float getInterpolation(float input) {
return input;
}
对,就是返回原值,因为时间的变化肯定始终都是匀速的。到这里,创建动画的 1 ~ 9 步都已经完成了。可时间究竟是怎么样变化的,getInterpolation() 又是怎样被调用的?这就是接下来要分析的启动动画。
启动动画
启动动画从 start() 方法开始。
@Override
public void start() {
AnimationHandler.getInstance().autoCancelBasedOn(this);
if (DBG) {
Log.d(LOG_TAG, "Anim target, duration: " + getTarget() + ", " + getDuration());
for (int i = 0; i < mValues.length; ++i) {
PropertyValuesHolder pvh = mValues[i];
Log.d(LOG_TAG, " Values[" + i + "]: " +
pvh.getPropertyName() + ", " + pvh.mKeyframes.getValue(0) + ", " +
pvh.mKeyframes.getValue(1));
}
}
super.start();
}
先确认动画已经取消。这个方法里的重要的那句代码就是调用父类 ValueAnimator 的 start()。父类对外的 start() 方法很简单,其主要的实现在另一个重载的私有 start() 方法上,来继续分析。
// 参数 playBackwards 代表动画是否是逆向的
private void start(boolean playBackwards) {
.....
mReversing = playBackwards;
// 重置脉冲为 "true"
mSelfPulse = !mSuppressSelfPulseRequested;
.....
// 添加脉冲回调用
addAnimationCallback(0);
if (mStartDelay == 0 || mSeekFraction >= 0 || mReversing) {
// If there's no start delay, init the animation and notify start listeners right away
// to be consistent with the previous behavior. Otherwise, postpone this until the first
// frame after the start delay.
startAnimation();
if (mSeekFraction == -1) {
// No seek, start at play time 0. Note that the reason we are not using fraction 0
// is because for animations with 0 duration, we want to be consistent with pre-N
// behavior: skip to the final value immediately.
setCurrentPlayTime(0);
} else {
setCurrentFraction(mSeekFraction);
}
}
}
这个方法原本很多,但我们只需要关注关键的调用,其中之一是 addAnimationCallback(),其主要是向 AnimationHander 添加一个回调接口AnimationHandler.AnimationFrameCallback。如下代码。
addAnimationFrameCallback
/**
* 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));
}
}
ValueAnimator 就实现了 AnimationFrameCallback,所以这里添加的是 ValueAnimator 的实例。且最终被添加到 mAnimationCallbacks 这个队列中。这个是很重要的,后面还会再重点关注的。而接下来是另一个调用 startAnimation()
startAnimation()
private void startAnimation() {
......
mAnimationEndRequested = false;
initAnimation();
mRunning = true;
if (mSeekFraction >= 0) {
mOverallFraction = mSeekFraction;
} else {
mOverallFraction = 0f;
}
if (mListeners != null) {
// 通过动画监听器动画开始了
notifyStartListeners();
}
}
关键调用 initAnimation()
void initAnimation() {
if (!mInitialized) {
int numValues = mValues.length;
for (int i = 0; i < numValues; ++i) {
mValues[i].init();
}
mInitialized = true;
}
}
mValues 是 PropertyValuesHolder 数组,这里的目的是初始化 PropertyValuesHolder。
void init() {
if (mEvaluator == null) {
// We already handle int and float automatically, but not their Object
// equivalents
mEvaluator = (mValueType == Integer.class) ? sIntEvaluator :
(mValueType == Float.class) ? sFloatEvaluator :
null;
}
if (mEvaluator != null) {
// KeyframeSet knows how to evaluate the common types - only give it a custom
// evaluator if one has been set on this class
mKeyframes.setEvaluator(mEvaluator);
}
}
init() 方法的主要目的是就是给关键帧设置估值器。因为我们前面调用的是 ObjectAnimator#ofFloat() 方法,所以这里默认给的就是 FloatEvaluator。这里也来分析一下估值器。估值器的相关概念已经在第二节第 5 小节中有所描述,并且根据框架类图,其主要是定义了 TypeEvaluator 接口。
TypeEvaluator
public interface TypeEvaluator<T> {
public T evaluate(float fraction, T startValue, T endValue);
}
fraction 代表了startValue 到 endValue 之间的比例,startValue 与 endValue 是我们自己在 ofFloat() 调用时设定的那个。返回结果就是一个线性的结果,在 startValue 与 endValue 之间。那么来看看 FloatEvaluator 的实现。
FloatEvaluator
public class FloatEvaluator implements TypeEvaluator<Number> {
public Float evaluate(float fraction, Number startValue, Number endValue) {
float startFloat = startValue.floatValue();
return startFloat + fraction * (endValue.floatValue() - startFloat);
}
}
很简单,就是起点值加上当前的段值。到这里,我们应该对估值器有一个更深的认知了。那么,再回到最初的 start() 方法里,经 startAnimation() 设置了 KeyFrame 的估值器后,接下来就会进一步调用 setCurrentPlayTime() 来开始动画。
setCurrentPlayTime()
public void setCurrentPlayTime(long playTime) {
float fraction = mDuration > 0 ? (float) playTime / mDuration : 1;
setCurrentFraction(fraction);
}
初始时调用的是setCurrentPlayTime(0),也就是 playTime 为 0,而 mDuration 就是我们自己通过 setDuration() 来设置的。所以这里得到的 fraction 也是 0。进一步看 setCurrentFraction() 方法。
public void setCurrentFraction(float fraction) {
// 再次调用 initAnimation() ,前面初始化过了,所以这里是无用的
initAnimation();
// 校准 fraction 为 [0, mRepeatCount + 1]
fraction = clampFraction(fraction);
mStartTimeCommitted = true; // do not allow start time to be compensated for jank
if (isPulsingInternal()) {
// 随机时间?
long seekTime = (long) (getScaledDuration() * fraction);
// 获取动画的当前运行时间
long currentTime = AnimationUtils.currentAnimationTimeMillis();
// Only modify the start time when the animation is running. Seek fraction will ensure
// non-running animations skip to the correct start time.
// 得到开始时间
mStartTime = currentTime - seekTime;
} else {
// If the animation loop hasn't started, or during start delay, the startTime will be
// adjusted once the delay has passed based on seek fraction.
mSeekFraction = fraction;
}
mOverallFraction = fraction;
final float currentIterationFraction = getCurrentIterationFraction(fraction, mReversing);
// 执行动画,注意这里会先调用子类的 animateValue() 方法
animateValue(currentIterationFraction);
}
前面都是一些时间的计算,得到当前真正的currentIterationFraction,最后会通过调用animateValue() 来执行动画。而这里需要同时关注父类与子类的 animateValue() 方法。
子类 ObjectAnimator#animateValue()
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;
}
// 调用父类的 animateValue() ,这个很关键,时间插值与估值器的计算都在父类的 animateValue() 方法中进行的。
super.animateValue(fraction);
int numValues = mValues.length;
for (int i = 0; i < numValues; ++i) {
// 这里的 mValues 的是PropertyValuesHolder[],也就是在 PropertyValuesHolder 里面来改变了目标 target 的属性值。
mValues[i].setAnimatedValue(target);
}
}
父类 ValueAnimator#animateValue()
void animateValue(float fraction) {
// 获取时间插值
fraction = mInterpolator.getInterpolation(fraction);
mCurrentFraction = fraction;
int numValues = mValues.length;
// 将时间插值送给估值器,计算出 values
for (int i = 0; i < numValues; ++i) {
mValues[i].calculateValue(fraction);
}
// 发出通知
if (mUpdateListeners != null) {
int numListeners = mUpdateListeners.size();
for (int i = 0; i < numListeners; ++i) {
mUpdateListeners.get(i).onAnimationUpdate(this);
}
}
}
animateValue() 的主要功能都已经在注释中说明了,其主要就是 2 步,一步计算时间插值和估值器,另一步是调用 PropertyValuesHolder 来改变属性。
void setAnimatedValue(Object target) {
if (mProperty != null) {
mProperty.set(target, getAnimatedValue());
}
if (mSetter != null) {
try {
mTmpValueArray[0] = getAnimatedValue();
// 通过反射调用来修改属性值
mSetter.invoke(target, mTmpValueArray);
} catch (InvocationTargetException e) {
Log.e("PropertyValuesHolder", e.toString());
} catch (IllegalAccessException e) {
Log.e("PropertyValuesHolder", e.toString());
}
}
}
这里就是通过属性的 Setter 方法来修改属性的。
分析到这里,就完成了动画的一帧关键帧的执行。那么你一定会感到奇怪了,剩下的帧是怎么驱动的呢?还是得回到 start() 方法里面,在这里最初分析到 addAnimationFrameCallback() 方法。这个方法里等于是向AnimationHandler注册了AnimationHandler.AnimationFrameCallback。这个 callback 中其中之一的方法是 doAnimationFrame()。在 ValueAnimator 的实现中如下。
public final boolean doAnimationFrame(long frameTime) {
.....
boolean finished = animateBasedOnTime(currentTime);
if (finished) {
endAnimation();
}
return finished;
}
这段代码原来也是很长的,我们只看关键调用 animateBasedOnTime()
boolean animateBasedOnTime(long currentTime) {
boolean done = false;
if (mRunning) {
.....
float currentIterationFraction = getCurrentIterationFraction(
mOverallFraction, mReversing);
animateValue(currentIterationFraction);
}
return done;
}
前面的计算过程,这里就省略了,其主要的目的也还是计算出 currentIterationFraction。然后再通过 animateValue() 方法来执行动画。可以看到只要 doAnimationFrame() 被不断的调用,就会产生动画的一个关键帧。如果关键帧是连续的,那么最后也就产生了我们所看到的动画。
再来分析doAnimationFrame() 是如何被不断调用的。这个需要回到 AnimationHandler 中来,在 AnimationHandler 中有一个非常重要的 callback 实现——Choreographer.FrameCallback。
private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
doAnimationFrame(getProvider().getFrameTime());
if (mAnimationCallbacks.size() > 0) {
getProvider().postFrameCallback(this);
}
}
};
了解 VSync 的同学都知道,Andorid 中的重绘就是由Choreographer在 1 秒内产生 60 个 vsync 来通知 view tree 进行 view 的重绘的。而 vsync 产生后会调用它的监听者回调接口 Choreographer.FrameCallback,也就是说,只要向Choreographer注册了这个接口,就会每 1 秒里收到 60 次回调。因此,在这里就实现了不断地调用 doAnimationFrame() 来驱动动画了。想必看到这里,你应该明白了同学们常说的动画掉帧的原因了吧。如果 view 的绘制过于复杂,即在 15 ms 内无法完成,那么就会使得中间某些帧跳过从而造成掉帧。
到这里,属性动画的整个过程以及原理都分析完了。下面来总结一下这个过程:
(1) 动画是由许多的关键帧组成的,这是一个动画能够动起来的最基本的原理。
(2) 属性动画的主要组成是 PropertyValuesHolder,而 PropertyValuesHolder 又封装了关键帧。
(3) 动画开始后,其监听了 Choreographer 的 vsync,使得其可以不断地调用 doAnimationFrame() 来驱动动画执行每一个关键帧。
(4) 每一次的 doAnimationFrame() 调用都会去计算时间插值,而通过时间插值器计算得到 fraction 又会传给估值器,使得估值器可以计算出属性的当前值。
(5) 最后再通过 PropertyValuesHolder 所记录下的 Setter 方法,以反射的方式来修改目标属性的值。当属性值一帧一帧的改变后,形成连续后,便是我们所见到的动画。
四、后记
Android 原生动画中,除了属性动画还有其他几种动画,在对属性动画有了一定的认知后,再来分析其他动画的实现原理,相信不会太难。后面有时间或者有需要会再来分析。
最后,感谢你能读到并读完此文章,如果分析的过程中存在错误或者疑问都欢迎留言讨论。如果我的分享能够帮助到你,还请记得帮忙点个赞吧,谢谢。