目录
1、前言
2、属性动画
3、插值器与估值器
4、其它动画方式
5、源码走读
前言
android中有三种类型的动画,帧动画、补间动画以及属性动画。
- 帧动画,原理类似于电影放映,将很多张图片顺序播放,形成动画效果
- 补间动画,继承自Animation,常通过xml描述动画,实现缩放、旋转、平移等
- 属性动画,使用最多的动画形式,也是最灵活的动画形式。
因为帧动画较为简单,本文不做介绍。补间动画功能较为强大,但自身有两个方面的缺陷导致全面不敌属性动画。
- 补间动画的只能作用到View上
- 补间动画的效果有问题,例如view平移,但view的点击区域仍然在原处,非常影响用户体验
属性动画
属性动画的使用方式较简单,非常灵活。属性动画最常用的就是ValueAnimator和ObjectAnimator。
ValueAnimator animator = ValueAnimator.ofFloat(1f, 0.5f);
animator.setDuration(1500);
animator.addUpdateListener(new AnimatorUpdateListener() {
public void onAnimationUpdate(ValueAnimator animation) {
float alpha = (Float) animation.getAnimatedValue();
mAniTextView.setAlpha(alpha);
}
});
animator.start();
ObjectAnimator animator = ObjectAnimator.ofFloat(mAniTextView, "alpha", 1f, 0.5f);
animator.setDuration(1500);
animator.start();
ValueAnimator,需要根据AnimatorUpdateListener的回调返回值实现动画。
ObjectAnimator使用方式更简单一点,不需要写监听回调也能实现,但ObjectAnimator操纵的属性一定要有get和set方法才行,否则动画无效。例如上述代码中,view中有setAlpha和getAlpha方法的。
插值器与估值器
插值器,即Interpolator,常见如线性插值器LinearInterpolator等。插值器的作用在于,动画执行过程中,存在表征动画执行情况的系数 input,从0到1,input默认是线性增长,插值器改变input的增长方式,但初始值和终值一定不变。
插值器必须实现接口TimeInterpolator,重写getInterpolation方法。
float getInterpolation(float input);
LinearInterpolator,线性插值器,它的getInterpolation非常简单,将input值返回即可,因此它是线性的
public float getInterpolation(float input) {
return input;
}
BounceInterpolator,Bounce有反弹跳跃的意思,它的返回值非常有意思,当返回值为接近于1时,它会反弹减小再增加,往复几次,最终返回值为1。可以想象有一个弹力球从空中掉落,当它落地时它会反弹几次最终才静止下来。
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;
}
BounceInterpolator的getInterpolation方法如上,其实它就是某个数学公式的描述,如果想要自定义插值器,重写getInterpolation即可,记住,初始值和终值必须为0和1。
插值器用于计算表征动画执行情况系数,那么估值器的作用是什么呢?以前文中alpha动画为例,插值器返回的值是【01】,但alpha的值却是【10.5】,显然不是拿插值器返回值直接使用的,估值器即是根据插值器返回值,计算动画最终参数值。
估值器必须实现TypeEvaluator接口,重写evaluate方法。
public T evaluate(float fraction, T startValue, T endValue);
常见的估值器有FloatEvaluator,IntEvaluator等,前文代码中的alpha动画传入的数值是float型,所以使用的估值器为FloatEvaluator,查看下FloatEvaluator的代码
public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
int startInt = startValue;
return (int)(startInt + fraction * (endValue - startInt));
}
fraction即为插值器的返回值,FloatEvaluator的逻辑非常简单,线性变化而已。一般估值器都较为简单,不像插值器一样复杂,目前只遇到过一类较复杂的估值器,颜色变换。因为颜色是由argb四个分量组成,需要对每个分量线性增长再合成,具体逻辑不细说了,读者可自行完成。
其它动画方式
如果从更宏观的角度看动画,除了在前言中谈到的三种动画形式,还有其它方法也能实现。
动态壁纸是一种动画吗?从宏观角度看,它确实也是一种动画。动态壁纸原理非常简单,不停地绘制,不停地刷新,壁纸上的元素永远在变化,所以才有动态的效果。本博中Camera与Matrix的那些事儿使用的3D容器,在滑动时也会产生动画效果。
属性动画的根本原理,也是改变了View的某些属性,比如alpha值,或者TranslationY值等等,再来刷新界面,重绘产生了动画效果。
所有动画的原理均是改变了绘制元素的属性,重绘时产生了动画效果。当某此场景中属性动画已无法完成时,可考虑绘制时动画。
根据个人经验,凡是有跟手的动画效果,都是绘制时动画形式完成的,例如Launcher中滑屏效果等
源码走读
ValueAnimator、ObjectAnimator的原理是什么呢?它们的调用流程是什么呢?
结合调用时序图,最关键的步骤是PropertyValuesHolder的calculateValue方法,此方法中调用插值器和估值器计算动画参数,并返回给ValueAnimator,ValueAnimator再调用监听器的onAnimationUpdate方法,将参数返回。
ObjectAnimator继承自ValueAnimator,大体上逻辑一致,但ObjectAnimator最终是通过反射,调用属性的set方法,设置view属性,达到动画的目的。那么反射调用在哪呢?
@Override
void animateValue(float fraction) {
final Object target = getTarget();
if (mTarget != null && target == null) {
// We lost the target reference, cancel and clean up.
cancel();
return;
}
super.animateValue(fraction);
int numValues = mValues.length;
for (int i = 0; i < numValues; ++i) {
mValues[i].setAnimatedValue(target);
}
}
ObjectAnimator重写animateValue方法,调用完父类方法后,还有如下逻辑:
mValues[i].setAnimatedValue(target);
查看它的具体实现
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());
}
}
}
原来反射调用就是在此处实现的。mSetter.invoke,这一看就是反射调用呐。
具体细节不再阐述,读源码就得不求甚解,否则细节会抠死人。