在 Api Level <= 22 的 Android 系统中,会有一个属性动画 Bug,这个 Bug 只会在使用多个 interpolator 的时候出现,平时的开发如果不用 KeyFrame 来实现动画的话,是不会遇到这个 bug 的。

Bug 原因

系统的计算两个 KeyFrame 相对 fraction 出现错误。看看 Api 22 和 Api 23 的代码就一目了然了:

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
//IntKeyframeSet.java(Api22)
...
@Override
public int getIntValue(float fraction) {
...
for (int i = 1; i < mNumKeyframes; ++i) {
IntKeyframe nextKeyframe = (IntKeyframe) mKeyframes.get(i);
if (fraction < nextKeyframe.getFraction()) {
final TimeInterpolator interpolator = nextKeyframe.getInterpolator();
if (interpolator != null) {
fraction = interpolator.getInterpolation(fraction);
}
float intervalFraction = (fraction - prevKeyframe.getFraction()) /
(nextKeyframe.getFraction() - prevKeyframe.getFraction());
int prevValue = prevKeyframe.getIntValue();
int nextValue = nextKeyframe.getIntValue();
return mEvaluator == null ?
prevValue + (int)(intervalFraction * (nextValue - prevValue)) :
((Number)mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).
intValue();
}
prevKeyframe = nextKeyframe;
}
...
}
...
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
//IntKeyframeSet.java(Api23)
...
@Override
public int getIntValue(float fraction) {
...
for (int i = 1; i < mNumKeyframes; ++i) {
FloatKeyframe nextKeyframe = (FloatKeyframe) mKeyframes.get(i);
if (fraction < nextKeyframe.getFraction()) {
final TimeInterpolator interpolator = nextKeyframe.getInterpolator();
float intervalFraction = (fraction - prevKeyframe.getFraction()) /
(nextKeyframe.getFraction() - prevKeyframe.getFraction());
float prevValue = prevKeyframe.getFloatValue();
float nextValue = nextKeyframe.getFloatValue();
// Apply interpolator on the proportional duration.
if (interpolator != null) {
intervalFraction = interpolator.getInterpolation(intervalFraction);
}
return mEvaluator == null ?
prevValue + intervalFraction * (nextValue - prevValue) :
((Number)mEvaluator.evaluate(intervalFraction, prevValue, nextValue)).
floatValue();
}
prevKeyframe = nextKeyframe;
}
...
}
...

每个 interpolator 在 KeyFrame 之间应该是独立的,作用域只在 preKeyFrame 和 nextKeyFrame,而之前的计算直接把应当独立的 interpolator 和整个动画的 fraction 一起计算,得出的结果当然是错的,应该是先按比例计算出 preKeyFrame 和 nextKeyFrame 的相对 fraction,再和对应的 interpolator 相乘才对。

解决方法

幸运的是平时的开发中很少遇到这种直接使用多个 KeyFrame 的情况,而且即使是这样,如果使用的都是 LinearInterpolator 也不会有问题,所以大部分人可以不用理这个 Bug;不幸的是,这个 Bug 目前官方只在 Api 23 之后才修复了,而且对以前的版本还没有兼容方案,目前要解决这个 Bug 只有

  • 通过引入第三方的 NineOldAndroids 并手动修改 IntKeyframeSet 和 FloatKeyframeSet 对应的代码可以解决这个问题。 但是会有以下缺点:

    • NineOldAndroids 已经没有维护了,实际使用过程中发现,NineOldAndroids 在同时运行多个 KeyFrame 动画的时候经常出现崩溃问题。我直接参考 NineOldAndroids,把 Api 19 的 animation 移植过来,解决这个问题。
    • 不能享受到 Android 官方对动画的优化,比如官方 Animation 的实现通过 jni 反射进行优化,而 NineOldAndroids 则做不到。
  • 推导公式计算 ??
    可以假设:

    f = getIntValue 的形参 fraction
    pV = prevKeyframe.getFloatValue()
    nV = nextKeyframe.getFloatValue()
    pF = prevKeyframe.getFraction()
    nF = nextKeyframe.getFraction()
    iF = intervalFraction
    G(f) = interpolator.getInterpolation() 函数
    F22(f) = Api22 的 IntKeyframeSet.getIntValue() 函数
    F23(f) = Api23 的 IntKeyframeSet.getIntValue() 函数

    则根据代码可得:

    1
    2
    F22(f) = pV+(iF*(nV-pV)) = pV+((G(f) - pF)/(nF -pF)*(nV-pV))
    F23(f) = pV+(iF*(nV-pV)) = pV+(G((f - pF)/(nF -pF))*(nV-pV))

    设有函数 T(x),使得 T(F22(f)) = F23(f),只要求出 T(x),就可以在不用修改源码的基础上修复这个 bug。如果 G(f) 是简单的线性函数子类的还好,但是实际情况很多都是贝塞尔函数,一代入公式,整个头都大了,所以在实际项目中我直接选择了第一种方式解决。