可能有点小标题党的嫌疑,不过忽略吧。
双十一来了,接到在购物车里面做一个蛋的需求。蛋还要加上动画。
针棒,又可以做动画了,心里是开心的,真的,不骗人[蜜汁微笑]。
动画涉及到几个状态之间的切换,跟本文主要思想不在一个“轮次”,就不细说,主要说一下这个蛋的摇摆动画以及使用插值器(Interpolator)的优化。
摇摆的动画视觉要求是这个样子的,如图所示:
上图其实视觉已经把动画变化速率做了处理,代码上只需要按照视觉稿匀速处理即可(此图已得视大许可。为视觉大佬低头,大佬辛苦了)。
ok,有了上图,这么做起来就很简单,啪啪啪(新买的机械键盘真爽)撸出一段代码,show you the code!
/**
* 摇摆动画
*
* @param view
*/
void eggShake(ImageView view) {
// 如果还在动画 就返回 因为empty2ShakeEgg在快速点击的时候,每次动画结束都会调用onEnd, 所以这里会执行很多次,且动画重合
if (mState == EggConstants.HATCH && eggShakeAnim != null && eggShakeAnim.isRunning()) {
return;
}
setEggPivot(view);
// 原有逻辑 先不改
if (eggShakeAnim != null) {
eggShakeAnim.cancel();
}
eggShakeAnim = ObjectAnimator.ofFloat(view, View.ROTATION,
EggConstants.SHAKE_START, EggConstants.SHAKE_LEFT_1,
EggConstants.SHAKE_START, EggConstants.SHAKE_RIGHT_3,
EggConstants.SHAKE_RIGHT_6, EggConstants.SHAKE_RIGHT_7,
EggConstants.SHAKE_RIGHT_6, EggConstants.SHAKE_RIGHT_3,
EggConstants.SHAKE_START, EggConstants.SHAKE_LEFT_3,
EggConstants.SHAKE_LEFT_6, EggConstants.SHAKE_LEFT_7,
EggConstants.SHAKE_LEFT_6, EggConstants.SHAKE_LEFT_3,
EggConstants.SHAKE_START,
EggConstants.SHAKE_RIGHT_3,
EggConstants.SHAKE_RIGHT_6, EggConstants.SHAKE_RIGHT_7,
EggConstants.SHAKE_RIGHT_6, EggConstants.SHAKE_RIGHT_3,
EggConstants.SHAKE_START, EggConstants.SHAKE_LEFT_3,
EggConstants.SHAKE_LEFT_6, EggConstants.SHAKE_LEFT_7,
EggConstants.SHAKE_LEFT_6, EggConstants.SHAKE_LEFT_3,
EggConstants.SHAKE_START, EggConstants.SHAKE_RIGHT_1, EggConstants.SHAKE_START,
// 这里为了不使用handler去延迟4秒,特地这么处理
EggConstants.SHAKE_START, EggConstants.SHAKE_START,EggConstants.SHAKE_START,
EggConstants.SHAKE_START, EggConstants.SHAKE_START,EggConstants.SHAKE_START,
EggConstants.SHAKE_START, EggConstants.SHAKE_START,EggConstants.SHAKE_START,
EggConstants.SHAKE_START, EggConstants.SHAKE_START,EggConstants.SHAKE_START,
EggConstants.SHAKE_START, EggConstants.SHAKE_START,EggConstants.SHAKE_START,
EggConstants.SHAKE_START, EggConstants.SHAKE_START,EggConstants.SHAKE_START,
EggConstants.SHAKE_START, EggConstants.SHAKE_START,EggConstants.SHAKE_START,
EggConstants.SHAKE_START, EggConstants.SHAKE_START,EggConstants.SHAKE_START,
EggConstants.SHAKE_START, EggConstants.SHAKE_START,EggConstants.SHAKE_START,
EggConstants.SHAKE_START, EggConstants.SHAKE_START,EggConstants.SHAKE_START,
EggConstants.SHAKE_START, EggConstants.SHAKE_START,EggConstants.SHAKE_START,
EggConstants.SHAKE_START, EggConstants.SHAKE_START,EggConstants.SHAKE_START,
EggConstants.SHAKE_START, EggConstants.SHAKE_START,EggConstants.SHAKE_START,
EggConstants.SHAKE_START, EggConstants.SHAKE_START,EggConstants.SHAKE_START,
EggConstants.SHAKE_START, EggConstants.SHAKE_START,EggConstants.SHAKE_START,
EggConstants.SHAKE_START, EggConstants.SHAKE_START,EggConstants.SHAKE_START
);
eggShakeAnim.setDuration(EggConstants.SHAKE_TWICE_DURATION + EggConstants.SHAKE_PAUSE_DURATION);
eggShakeAnim.setInterpolator(null);
eggShakeAnim.setRepeatCount(ValueAnimator.INFINITE);
if(eggShakeAnim != null && mState == EggConstants.HATCH) {
eggShakeAnim.start();
}
}
讲真,写出这段代码我自己都佩服我自己,此处也只有一个表情能描述此刻的心情了吧
彩蛋: (我是不会告诉你我一开始是用AnimatorSet实现,后面发现状态切换过程中,使用Set实现会有bug,才改用ObjectAnimator实现的)
ok,最后效果是这个样子的(稍等一下,蛋会动的),如果还是不会动,链接在这里( http://ww1.sinaimg.cn/large/0060lm7Tly1fl8drgvs56g30ds0b07wj.gif )
就是开始和结束前各有一个1度的回弹,用时2.4秒,再等待4秒,重复上面摇摆的动画。
就这样,到了代码review的那一天,真的不出意外,谁看到都想怼这段代码,决心要把这段代码优化一下,但是还是觉得无从下手,知道组内龙神说可以用Interpolator
实现,感觉好像突然明白了什么(其实好像并不是很懂的样子)。感谢龙神。
其实动画的基本原理就是从动画开始的时间到结束的时间一帧帧的播放静态图像,而Interpolator接收的唯一的一个参数input(下面会说到)就是我们设置的DURATION转化而来,并且是在[0, 1]之间而且匀速变化的。我们可以理解成,当input=0.0的时候就是动画的开始,input=1.0的时候就是动画的结束,那么动画的每一个时刻都可以用input的一个值来表示。比如如果使用LinearInterpolator来平移的话,那么0.5这个时间点就是对应target刚好平移整个距离的50%的一个时刻。
按照这种说法的话,Interpolator可以看成动画的变化率,描述了随着时间的流逝动画应该变化到什么程度,再配合估值器(Interpolator计算的结果就是估值器输入参数fraction),根据动画开始值和结束值,计算出应该动画的具体值。
简单来说:插值器就是描述了动画从初始时间到结束时间的变化规律,而估值器是描述了动画从初始值到结束值的变化规律。
为了方便,Google为我们内置了9个插值器,列举如下:
表现 | 资源ID | Java类 |
---|---|---|
动画加速进行 | @android:anim/accelerate_interpolator | AccelerateInterpolator |
快速完成动画,超出再回到结束样式 | @android:anim/overshoot_interpolator | OvershootInterpolator |
先加速再减速 | @android:anim/accelerate_decelerate_interpolato | AccelerateDecelerateInterpolator |
先退后再加速前进 | @android:anim/anticipate_interpolator | AnticipateInterpolator |
先退后再加速前进,超出终点后再回终点 | @android:anim/anticipate_overshoot_interpolator | AnticipateOvershootInterpolator |
最后阶段弹球效果 | @android:anim/bounce_interpolator | BounceInterpolator |
周期运动 | @android:anim/cycle_interpolator | CycleInterpolator |
减速 | @android:anim/decelerate_interpolator | DecelerateInterpolator |
匀速 | @android:anim/linear_interpolator | LinearInterpolator |
从上述这些插值器的代码上来看,其实他们本质上都是一个以input为因变量的一个(分段)函数
举个栗子:
核心代码如下:
public float getInterpolation(float input) {
return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f;
}
可以看到,其实就是这样一个公式:y=cos((t+1)π)/2+0.5
对应图像如下:
我们上面说了,插值器可以看成动画的变化率,从图上我们可以轻松看出,斜率是先变大后变小,再将fraction传递给估值器,可以知道,最终动画的变化值,也是先变快,后变慢。
核心代码如下:
public float getInterpolation(float t) {
// _b(t) = t * t * 8
// bs(t) = _b(t) for t < 0.3535
// bs(t) = _b(t - 0.54719) + 0.7 for t < 0.7408
// bs(t) = _b(t - 0.8526) + 0.9 for t < 0.9644
// bs(t) = _b(t - 1.0435) + 0.95 for t <= 1.0
// b(t) = bs(t * 1.1226)
t *= 1.1226f;
if (t < 0.3535f) return bounce(t);
else if (t < 0.7408f) return bounce(t - 0.54719f) + 0.7f;
else if (t < 0.9644f) return bounce(t - 0.8526f) + 0.9f;
else return bounce(t - 1.0435f) + 0.95f;
}
公式如下:
对应图像如下:
最后在动画结束值之间来回变化,就有了弹簧效果,图像表示的应该是很形象了。
public interface TimeInterpolator {
// 内部只有一个方法
float getInterpolation(float input) {
// 参数说明
// input值值变化范围是0-1,且随着动画进度 均匀变化
// 即动画开始时,input值 = 0;动画结束时input = 1
// 而中间的值则是随着动画的进度(0% - 100%)在0到1之间均匀增加
...// 插值器的计算逻辑
return xxx;
// 返回的值就是用于估值器继续计算的fraction值,下面会详细说明
}
public interface TypeEvaluator {
/**
* 参数说明
* fraction:插值器getInterpolation()的返回值
* startValue:动画的初始值
* endValue:动画的结束值
*/
public Object evaluate(float fraction, Object startValue, Object endValue) {
....// 估值器的计算逻辑
return xxx;
// 使用反射机制改变属性变化 赋给动画属性的具体数值
}
}
/**
* This evaluator can be used to perform type interpolation between <code>float</code> values.
*/
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);
}
}
从图2中的视觉稿我们可以知道,摇摆的动画分成了28段,我们用每一段代表是个时间片(TIME_SLICE),这28段用时2.4秒,那么我们就可以算出静止的4秒是多少个时间片了,这样我们可以得到下面的数据表格(为了节省空间,省略部分数据)
进而根据上面的数据表格,我们可以得到带坐标轴的散点图,如下:
如果直接把上面的坐标图表示成函数,放入插值器,需要分成很多段,程序上要用很多if-else处理,看起来也是麻烦(其实精确的就是应该用折线图来表示,因为根据视觉稿,变化是线性的)。所以将其近似处理为三角函数
可以得到公式如下:x取值已经归一化处理
公式 | 自变量取值 |
---|---|
S1 = sin((π + x) | 0<x<=π |
S2 = 7 * sin(x - π) | x<=5π |
S3 = sin(π + x) | x<=6π |
S4 = 0 | x>6π |
对应的数学图形如下(图中字母标记在代码中可以用到):
将input的值归一化处理,然后直接套入上述公式中,得到代码如下?
public class EggShakeInterpolator implements TimeInterpolator {
// EggConstants.SHAKE_PAUSE_DURATION = 2400; 摇摆动画时间
// EggConstants.SHAKE_TWICE_DURATION = 4000; 静止时间
int wholeTime = EggConstants.SHAKE_PAUSE_DURATION + EggConstants.SHAKE_TWICE_DURATION;
float TIME_SLICE = ((float) EggConstants.SHAKE_TWICE_DURATION / 28.f);// 摇摆动画2.4s分成了28断,代表每一段的一个时间片
float S1 = 2 * TIME_SLICE; // 阶段一,向左摆动一度,占有两个时间片
float s1 = 12 * TIME_SLICE; // 阶段二,第一个循环 左右摆动一个来回,占有12个时间片
float s2 = s1; // 阶段二,第二个循环 左右摆动一个来回,占有12个时间片
float S2 = s1 + s2; // 阶段二,包含上面两个小阶段
float S3 = S1; //阶段三,向右摆动一度,占有两个时间片
@Override
public float getInterpolation(float input) {
input = input * wholeTime;
if (input <= EggConstants.SHAKE_TWICE_DURATION) {
if (input <= S1) {
double x = (input / S1 * Math.PI);//归一化处理
return (float) Math.sin(Math.PI + x);
} else if (input <= S1 + S2) {
double x = (input - S1) / s1 * 2 * Math.PI;
return (float) (7 * Math.sin(x));
} else {
double x = (input - S1 - S2) / S3 * Math.PI;
return (float) Math.sin(x);
}
} else {
return 0f;
}
}
}
/**
* 摇摆动画
*
* @param view
*/
void eggShake(ImageView view) {
// 如果还在动画 就返回 因为empty2ShakeEgg在快速点击的时候,每次动画结束都会调用onEnd, 所以这里会执行很多次,且动画重合
if (mState == EggConstants.HATCH && eggShakeAnim != null && eggShakeAnim.isRunning()) {
return;
}
setEggPivot(view);
// 原有逻辑 先不改
if (eggShakeAnim != null) {
eggShakeAnim.cancel();
}
// 这里startValue设置为0,endValue设置为1,需要特殊处理,不然估值器得到的值就不对了
eggShakeAnim = ObjectAnimator.ofFloat(view, View.ROTATION, 0, 1);
eggShakeAnim.setDuration(EggConstants.SHAKE_TWICE_DURATION + EggConstants.SHAKE_PAUSE_DURATION);
// 只需要设置插值器
eggShakeAnim.setInterpolator(new EggShakeInterpolator());
eggShakeAnim.setRepeatCount(ValueAnimator.INFINITE);
if(eggShakeAnim != null && mState == EggConstants.HATCH) {
eggShakeAnim.start();
}
}
最终通过Interpolator实现的效果与未优化之前效果一致,这里就不再贴gif图片了。
其实平时做动画的时候,一般都是可以通过ObjectAnimator或者再加上AnimatorSet混用就可以解决,一般的插值器也都可以满足需求,很少去自定义,而且我们一般理解的插值器只是在动画速率上做一些改变,并没有想到可以配合数学公式来完成复杂动画。通过这次学习,深入了解了Interpolator以及Evaluator的原理,进一步加深了动画的认识。
鉴于个人能力有限,文章中难免有错误之处,如有发现,还请不吝赐教及时指出。
谷歌Android Developer之ObjectAnimator