深入理解Android属性动画的实现-1

导读

在文章开头我们先简单回忆一下常用的属性动画 API 有哪些?
1.动画相关的类:
ObjectAnimator,ValueAnimator,Property,PropertyValuesHolder,Keyframe,Keyframes,TimeInterpolator,TypeEvaluator
在剖析过程中我们会解析相关类的作用和实现方式
2.常用的 属性动画 方法:

1
2
3
4
5
ObjectAnimator.ofInt()
ObjectAnimator.ofFloat()
ObjectAnimator.ofArgb()
ObjectAnimator.ofObject()
ObjectAnimator.ofPropertyValuesHolder()

下面我们以一个常用的方法 ObjectAnimator.ofInt() 为切入点层层深入解析属性动画的创建过程。

动画的创建

早期版本的属性动画 ofInt 方法有两个实现,到Android O 为止 已经有 4 个实现了。
主要就是增加了 Path 参数。我们这里并不去解析新方法,因为它的实现方式和最基本的方法相同。

1
2
3
4
5
6
7
8
9
10
public static ObjectAnimator ofInt(Object target, String propertyName, int... values) {
ObjectAnimator anim = new ObjectAnimator(target, propertyName);
anim.setIntValues(values);
return anim;
}
public static <T> ObjectAnimator ofInt(T target, Property<T, Integer> property, int... values) {
ObjectAnimator anim = new ObjectAnimator(target, property);
anim.setIntValues(values);
return anim;
}

注意:我这里以Android-25 源码为代码源。
上述代码中ofInt 方法的第一行均是 创建 ObjectAnimator 对象。但分别使用了两种构造方式。
下面我们探索一下 ObjectAnimator 的构造方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
//无参构造
public ObjectAnimator() {
}
//指定 propertyName
private ObjectAnimator(Object target, String propertyName) {
setTarget(target);
setPropertyName(propertyName);
}
//指定 Property
private <T> ObjectAnimator(T target, Property<T, ?> property) {
setTarget(target);
setProperty(property);
}

无参构造方法没有什么可说的。另外两个构造方法第一行代码都是

1
setTarget(target);

setTarget方法的作用是记录需要做动画的对象,方便以后对该对象的相关属性做更改。

1.我们首先看看 propertyName 参数的构造方法的setPropertyName 方法

1
2
3
4
5
6
7
8
9
10
11
public void setPropertyName(@NonNull String propertyName) {
if (mValues != null) {
PropertyValuesHolder valuesHolder = mValues[0];
String oldName = valuesHolder.getPropertyName();
valuesHolder.setPropertyName(propertyName);
mValuesMap.remove(oldName);
mValuesMap.put(propertyName, valuesHolder);
}
mPropertyName = propertyName;
mInitialized = false;
}

我们这里的调用情形,mValues肯定为null,不会走 if分支,剩下的就是简单的记录我们设置的propertyNamemPropertyName保持。如果mValues不为空的情况下,会更新 mValues的第一个元素的属性值,并更新对应的map集合
2.我们再看看Property参数的构造方法的setProperty方法,同样也是简单的记录mPropertymPropertyName
那么 这个Property是何方神圣?

Property 解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public abstract class Property<T, V> {
private final String mName;
private final Class<V> mType;
public static <T, V> Property<T, V> of(Class<T> hostType, Class<V> valueType, String name) {
return new ReflectiveProperty<T, V>(hostType, valueType, name);
}
public Property(Class<V> type, String name) {
mName = name;
mType = type;
}
public boolean isReadOnly() {
return false;
}
public void set(T object, V value) {
throw new UnsupportedOperationException("Property " + getName() +" is read-only");
}
public abstract V get(T object);
public String getName() {
return mName;
}
public Class<V> getType() {
return mType;
}
}

这是一个 abstract修饰的类。这个类很简单,就是定义了一些需要被子类实现的方法。(为了减小博客的字数,我这里隐去源码中的相关注释,如有需要请自行到源码中查看)
通过对注释的理解我们知道 这个类的注意功能就是代表需要动画的属性,实现这个类有一些先决条件需要满足:
必须满足一下三个条件中的一个
1.被 Property 代表的类的属性必须有一个public的无参的get方法,和一个可选的set方法,set方法的参数和get方法的返回值一个类型
2.有一个public的无参的is 方法,和一个可选的set方法 set方法的参数和is方法的返回值一个类型
3.一个public的 属性 name
假如有 get/is方法,但是没有set方法,那么这个 Property 是一个只读的属性。
set 方法必须实现,都在抛出异常
到目前为止我们了解了 动画初始化时会初始化 mPropertyName变量,记录属性的名称;或初始化mProperty,来保持对属性的访问能力和更新能力。
这里说明一下,常规情况下需要满足这三个中一个即可,如果我们又其他方式来变量属性值,而且也在 Property 中做了相应的映射关系,不满足也可以。这个在之后文章中自定义属性动画中可以看出
我们回到之前的 ofInt方法
紧接着 执行了 代码

1
anim.setIntValues(values);

这行代码看似简单,其实包含了很多信息。从字面意思理解,就是保存动画的属性值范围。

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public void setIntValues(int... values) {
if (mValues == null || mValues.length == 0) {
if (mProperty != null) {
setValues(PropertyValuesHolder.ofInt(mProperty, values));
} else {
setValues(PropertyValuesHolder.ofInt(mPropertyName, values));
}
} else {
super.setIntValues(values);
}
}

第一次调用 mValuesnull走入if流程,假如我们调用的 propertyName参数的方法,会执行setValues(PropertyValuesHolder.ofInt(mPropertyName, values));,反之执行setValues(PropertyValuesHolder.ofInt(mProperty, values));
我们先看看PropertyValuesHolder.ofInt(mProperty, values)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 public static PropertyValuesHolder ofInt(Property<?, Integer> property, int... values) {
return new IntPropertyValuesHolder(property, values);
}

public IntPropertyValuesHolder(Property property, int... values) {
super(property);
setIntValues(values);
if (property instanceof IntProperty) {
mIntProperty = (IntProperty) mProperty;
}
}
@Override
public void setIntValues(int... values) {
super.setIntValues(values);
mIntKeyframes = (Keyframes.IntKeyframes) mKeyframes;
}
public void setIntValues(int... values) {
mValueType = int.class;
mKeyframes = KeyframeSet.ofInt(values);
}

该方法主要的作用就是创建一个PropertyValuesHolder,保持属性Property
并将 属性值保存成关键帧,我们关注核心如何生成关键帧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static KeyframeSet ofInt(int... values) {
int numKeyframes = values.length;
IntKeyframe keyframes[] = new IntKeyframe[Math.max(numKeyframes,2)];
if (numKeyframes == 1) {
keyframes[0] = (IntKeyframe) Keyframe.ofInt(0f);
keyframes[1] = (IntKeyframe) Keyframe.ofInt(1f, values[0]);
} else {
keyframes[0] = (IntKeyframe) Keyframe.ofInt(0f, values[0]);
for (int i = 1; i < numKeyframes; ++i) {
keyframes[i] =
(IntKeyframe) Keyframe.ofInt((float) i / (numKeyframes - 1), values[i]);
}
}
return new IntKeyframeSet(keyframes);
}

上述代码是生成关键帧的逻辑:
1.判断值的个数,如果只有一个值。那么分配长度为2的IntKeyframe数组,并将值保持在第二个位置。
2.如果多于一个值,会分配 值个数 长度的数组,将每一个数值都保持在数组对应的位置。
需要注意的是:除了第一帧,其他帧都分配了fraction,这个系数有这些数字的传入顺序决定。假如我们传入3个值,第一帧的系数默认为0,第二帧的系数为0.5,最后一个值得系数是1
。这个系数在后来动画的执行中为生成相应的属性值起到关键作用。

到目前为止,我们知道PropertyValuesHolder持有了Property并将动画的值保存成关键帧,并持有这些关键帧。

同样setValues(PropertyValuesHolder.ofInt(mPropertyName, values));方法也相同的方式生成了PropertyValuesHolder并持有 生成的关键帧

然后我们分析一下setValues方法。
这是父类 ValueAnimator的方法

1
2
3
4
5
6
7
8
9
10
11
public void setValues(PropertyValuesHolder... values) {
int numValues = values.length;
mValues = values;
mValuesMap = new HashMap<String, PropertyValuesHolder>(numValues);
for (int i = 0; i < numValues; ++i) {
PropertyValuesHolder valuesHolder = values[i];
mValuesMap.put(valuesHolder.getPropertyName(), valuesHolder);
}
// New property/values/target should cause re-initialization prior to starting
mInitialized = false;
}

主要功能:
1.保存前面生成的 PropertyValuesHolder,用 mValues持有引用。
2.将 PropertyValuesHolder数组按照属性名为key,PropertyValuesHolder为值保存至map中,并标记为初始化mInitialized = false,在下一篇文章中会提到何时将标记位mInitialized置为 true

到目前为止我们已经分析完了 ObjectAnimator.ofInt方法。
其中有几个未解释到的类:
1.PropertyValuesHolder
2.Keyframe

PropertyValuesHolder

由前面的分析我们大致知道 PropertyValuesHolder 持有了 PropertymPropertyName,以及持有 关键帧。
除此之外,它还有 属性mSetter,mGetter,mEvaluator

1
2
3
Method mSetter = null;
private Method mGetter = null;
private TypeEvaluator mEvaluator;

以及一堆 of方法

ofInt
ofMultiInt
ofFloat
ofMultiFloat
ofObject
ofKeyframe

这些 of方法我们前面解析过 ofInt 这里不再展开解释。
看到 mSettermGetter方法,我们联想到这个类肯定会操作 动画对象的属性。果不其然,让我们找到了 setupSettersetupGetter 方法

1
2
3
4
5
6
7
8
9
10
11
 void setupSetter(Class targetClass) {
Class<?> propertyType = mConverter == null ? mValueType : mConverter.getTargetType();
mSetter = setupSetterOrGetter(targetClass, sSetterPropertyMap, "set", propertyType);
}

/**
* Utility function to get the getter from targetClass
*/
private void setupGetter(Class targetClass) {
mGetter = setupSetterOrGetter(targetClass, sGetterPropertyMap, "get", null);
}

这两个方法通过 拼接setget,通过反射获取属性的 get set 方法,从而获得对象属性的控制权。
当然 一些PropertyValuesHolder的子类重载使用了 jni 方法获得反射方法。其实都是目的都是一样。

当然,获得了这些属性的控制权,自然会更新这些属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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());
}
}
}

这个 setAnimatedValue方法就是用于更新这些属性;如果有 mProperty就通过 Property更新属性,如果没有的话,就通过反射更新。
当然在更新属性之前需要计算出正确的属性值。

1
2
3
4
void calculateValue(float fraction) {
Object value = mKeyframes.getValue(fraction);
mAnimatedValue = mConverter == null ? value : mConverter.convert(value);
}

calculateValue方法就是用于计算这些值。计算又用到了Keyframes,下面分析关键帧相关的API。到此为止我们理解了 PropertyValuesHolder的作用

总结一下:
1.持有 Property 用于获取,更新属性的能力。
2.持有 mPropertyName 通过反射获取get , set 方法,获取更新属性的能力
3.计算属性的值
4.更新属性
所以 PropertyValuesHolder就是控制动画对象属性的关键。所以也很容易理解,为什么所有的属性动画的value都转化成PropertyValuesHolder来操作.

Keyframe

下面我们在详细了解下何为关键帧,它是如何运作的?
先看几个关于关键帧的类 Keyframe,Keyframes 以及它们的子类
Keyframe顾名思义就是记录关键帧数据,他默认有三个实现类IntKeyframe,FloatKeyframe,ObjectKeyframe,当然我们也能继承Keyframe实现自己的关键帧类型。不过大部分情况,提供的这三种方式已经够用了。
Keyframe 有三个重要的属性值:
1.mHasValue 用于记录关键帧是否初始化了值,以 子类 IntKeyframe 为例,它有两个构造方法:

1
2
3
4
5
6
7
8
9
10
11
IntKeyframe(float fraction, int value) {
mFraction = fraction;
mValue = value;
mValueType = int.class;
mHasValue = true;
}

IntKeyframe(float fraction) {
mFraction = fraction;
mValueType = int.class;
}

第一个构造初始化了 mValue ,同时 mHasValue会标记 为true;
第二个构造方法 只初始化了 mFraction,并未给 mValue赋值,默认 mHasValuefalse,假如我们使用了mHasValuefalse的 关键帧。那么在动画初始化时会调用PropertyValuesHolder.setupSetterAndGetter给每一个关键帧赋初始值。

2.mFraction记录该关键帧所有关键帧中的位置,float类型,值范围0 - 1
3.mValue记录value值,当然不同类型的Keyframe value值类型也不同,所以 mValue由子类实现

Keyframes

我们可以认为 Keyframes是一堆 Keyframe组成的集合。
先看看继承体系:
它的实现类有KeyframeSet,PathKeyframes,它的子接口 有IntKeyframes,FloatKeyframes
我们看看常用的KeyframeSet,继承它的两个子类IntKeyframeSet,FloatKeyframeSet,通知实现了对应的IntKeyframesFloatKeyframes接口。

其实 Keyframes最核心的方法是getValue方法,其子实现均是为了实现它。下面是KeyframeSetgetValue方法的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public Object getValue(float fraction) {
if (mNumKeyframes == 2) {
if (mInterpolator != null) {
fraction = mInterpolator.getInterpolation(fraction);
}
return mEvaluator.evaluate(fraction, mFirstKeyframe.getValue(),
mLastKeyframe.getValue());
}
if (fraction <= 0f) {
final Keyframe nextKeyframe = mKeyframes.get(1);
final TimeInterpolator interpolator = nextKeyframe.getInterpolator();
if (interpolator != null) {
fraction = interpolator.getInterpolation(fraction);
}
final float prevFraction = mFirstKeyframe.getFraction();
float intervalFraction = (fraction - prevFraction) /
(nextKeyframe.getFraction() - prevFraction);
return mEvaluator.evaluate(intervalFraction, mFirstKeyframe.getValue(),
nextKeyframe.getValue());
} else if (fraction >= 1f) {
final Keyframe prevKeyframe = mKeyframes.get(mNumKeyframes - 2);
final TimeInterpolator interpolator = mLastKeyframe.getInterpolator();
if (interpolator != null) {
fraction = interpolator.getInterpolation(fraction);
}
final float prevFraction = prevKeyframe.getFraction();
float intervalFraction = (fraction - prevFraction) /
(mLastKeyframe.getFraction() - prevFraction);
return mEvaluator.evaluate(intervalFraction, prevKeyframe.getValue(),
mLastKeyframe.getValue());
}
Keyframe prevKeyframe = mFirstKeyframe;
for (int i = 1; i < mNumKeyframes; ++i) {
Keyframe nextKeyframe = mKeyframes.get(i);
if (fraction < nextKeyframe.getFraction()) {
final TimeInterpolator interpolator = nextKeyframe.getInterpolator();
final float prevFraction = prevKeyframe.getFraction();
float intervalFraction = (fraction - prevFraction) /
(nextKeyframe.getFraction() - prevFraction);
// Apply interpolator on the proportional duration.
if (interpolator != null) {
intervalFraction = interpolator.getInterpolation(intervalFraction);
}
return mEvaluator.evaluate(intervalFraction, prevKeyframe.getValue(),
nextKeyframe.getValue());
}
prevKeyframe = nextKeyframe;
}
// shouldn't reach here
return mLastKeyframe.getValue();
}

该方法核心是 通过参数fraction(时间流逝比)来计算对应的属性值,计算出来的值如无TypeConverter转换需求,那就会把这个值设置到 动画Target的属性上,从而实现属性动画对属性的更改。
观察到这里还使用了一个TypeEvaluator,这哥们大家估计通过其他途径都了解过,我们经常把 TimeInterpolatorTypeEvaluator一起来解释属性动画的原理,即插值器和估值器对属性动画的作用。我们这里不展开讨论,只基于源码去分析。
我们先看第一个if分支

1
2
3
4
5
6
7
if (mNumKeyframes == 2) {
if (mInterpolator != null) {
fraction = mInterpolator.getInterpolation(fraction);
}
return mEvaluator.evaluate(fraction, mFirstKeyframe.getValue(),
mLastKeyframe.getValue());
}

对应当仅有两个关键帧时的情况。举个例子,假如我们调用ObjectAnimator.ofInt方法只传入了一个int值,这里的 mNumKeyframes会是2,而且看注释,只有在这种情况下才使用mInterpolator-插值器。通过插值器 得到修正后的系数fraction,然后拿这个系数,通过估值器mEvaluator计算出属性值。我们发现估值器是何等的重要,必须要被赋值的,否则无法计算出属性值。
继续 if (fraction <= 0f)if (fraction >= 1f)这两个分支是特殊情况,出现在动画第一帧和最后一帧的情况。
下面看看正常动画中的分支流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Keyframe prevKeyframe = mFirstKeyframe;
for (int i = 1; i < mNumKeyframes; ++i) {
Keyframe nextKeyframe = mKeyframes.get(i);
if (fraction < nextKeyframe.getFraction()) {
final TimeInterpolator interpolator = nextKeyframe.getInterpolator();
final float prevFraction = prevKeyframe.getFraction();
float intervalFraction = (fraction - prevFraction) /
(nextKeyframe.getFraction() - prevFraction);
// Apply interpolator on the proportional duration.
if (interpolator != null) {
intervalFraction = interpolator.getInterpolation(intervalFraction);
}
return mEvaluator.evaluate(intervalFraction, prevKeyframe.getValue(),
nextKeyframe.getValue());
}
prevKeyframe = nextKeyframe;
}

通过fraction查找符合fraction < nextKeyframe.getFraction()的第一帧,然后计算 fraction在前一帧到当前帧范围内的位置,例如查找到符合条件的当前帧 fraction =0.5;前一帧fraction = 0.2, 动画当前的 fraction =0.3,那么新的 intervalFraction = 1/3f。然后拿着这个值给估值器计算属性值。

到目前为止我们知道了,关键帧的作用如下:
1.保存每个关键帧的值
2.保存每个关键帧的 fraction
3.通过动画类传入的 fraction,利用估值器计算出 真正的属性值。

那么剩下的就是如何将这些计算出来的属性值设置到对应的Target上了。这个问题我们下一篇分析动画的启动原理时会再分析。

回顾

1.以ObjectAnimator.ofInt为切入点 分析 ObjectAnimator对象的创建
2.属性名称propertyNameProperty的作用:
propertyName 用于拼接 get/set方法,获得控制属性的能力;
Property 利用自己的getset方法直接控制属性。

  1. PropertyValuesHolder的作用:通过 propertyName 或 Property 获取更新属性的能力;根据动画时间流逝比计算属性的值;更新属性值到对应的Target
  2. 关键帧 keyframe用于保存动画的每一个关键数据帧,个数由传入的 value 个数决定,系数 fraction 由 value 传入顺序决定。
  3. keyframes以及它的子类KeyframeSet的作用:利用估值器和 fraction 计算出应该更新到 Target上的属性值。

基本API 解析完毕,下一篇我们会分析动画如何开始的,fraction系数如何得到的,以及何时更新 属性值。