在属性动画出现之前,Android的动画包括逐帧动画、补间动画两种。逐帧动画类似于gif图片,一帧一帧的播放准备好的图片。补间动画作用于一个View,可以实现平移、缩放、旋转、透明度这四种效果,也可以将多种动画同时执行,并控制执行的速度。这两种动画的使用非常简单,可以很轻松的实现一些简单的动画效果,关于具体的使用方法,不是本文的重点,不了解的同学可以自行百度~~
然而,这两种动画在使用简单的同时,也有很大的局限性。逐帧动画只能在准备好每一帧图片的情况下播放。补间动画只能实现上面提到的几种效果,如果希望能实现更复杂的自定义动画,那它就完全无能为力。另外,补间动画只是对View的视图进行改变,而没有真正的改变View的属性。例如使用补间动画将一个Button从A平移到B,这时你尝试点击这个Button,你会发现它的点击事件依然只能在A处响应。在移动端对UI的要求越来越高的今天,缺陷那么多的动画框架是肯定没法满足需求的,于是,属性动画应运而生了。
属性动画有多强大呢?可以这么说,属性动画几乎可以完全替代上面提到的两种动画,另外,如果你真正理解了属性动画的原理和用法,你会发现曾经感到难以实现、望而却步的自定义View / 自定义动画效果在属性动画面前原来是如此简单。
好了,吹了这么多,下面进入本文的重点:属性动画到底是什么?我对属性动画的理解可以概括为以下一句话:
属性动画的本质是对象属性的变化,即值的变化,而动画效果只不过是值变化的一种表现形式。
这么说可能有点难以理解,举个简单的例子:对于下图的这个动画效果,你想怎么去实现它?你可能会说这个简单,我问UI要一张图,然后不停地旋转它就实现了。但是如果有一天产品经理告诉你需求改了,现在要让小球的大小可以配置,并且在旋转的同时改变颜色,同时圆环中间要加个不停变化的文字……好吧,那现你只能带着手撕产品经理的想法重新实现一遍了,那么如果要用动画实现这个效果,应该怎么做呢?
Ring_Rotate.gif如果你想的是赶紧查一查动画的API,看看有没有这种View绕圆旋转的方法的话,那只能说你对动画的理解还停留在Android3.0之前的时代。那么,如果使用属性动画的话,应该怎么实现呢?不妨对这个动画进行分解,看看这个动画做了什么:首先画了个圆环,然后在圆环上有一个红色的小球绕圆心旋转。在这个过程中,唯一改变的是小球的坐标(x, y)。如果我们抛开视图上的动画,那么可以把这个动画看做是无数坐标点的集合,所谓的动画,只是在每个坐标点处重新绘制了小球。因此,如果我们得到小球坐标随时间变化的规律,这个动画自然就绘制出来了。带着这个思想,我们开始属性动画的学习。
ValueAnimator
ValueAnimator是属性动画的核心类。它实现的就是为动画的初始值和结束值在动画运行时间内提供一个平滑的过度。举个栗子:
ValueAnimatoranimator =ValueAnimator.ofInt(200, 400);
animator.setDuration(3000);
animator.start();
ofInt方法返回一个ValueAnimator对象,ValueAnimator运行之后的效果是在3秒的时间内将一个值从200平滑过度到400。ofInt方法传的参数没有限制,比如:
ValueAnimatoranimator =ValueAnimator.ofInt(0, 100,0,50);
就表示将一个值从1过度到100再到0再到50。
类似的方法还有ofFloat()、ofArgb()、ofObject()。ofFloat()和ofArgb()看着名字就应该知道是干啥的了,用法也是类似的~ofObject()等会再讲。
看到这里可能有人就着急了:这坑爹呢,说好的动画呢?连影子都没见着啊!
别急,还记得前面说的吗,属性动画的本质是值的变化,动画只是值变化的表现形式。制定了值变化的规则,在这基础上绘制动画,那简直是信手拈来。例如我们想让一个小球从x轴的200px处平移到400px处,只需要在以上代码中增加一个监听:
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
intcurrentValue = (int)animation.getAnimatedValue();
invalidate();//具体绘制逻辑需要在onDraw中实现
}
});
监听值从200变化到400的过程,在变化过程中可以通过getAnimatedValue()方法获取到当前的值。取到这个值以后,就可以在相应的位置调用invalidate() / postInvalidate()方法重绘小球,一个简单的平移动画就实现了~
当然,只满足于这么简单的动画当然是不够的,一个较为复杂的动画可能有不止一个变化的值,并且值的变化也不一定是线性的。这时就需要用到ValueAnimator更灵活的方法:ofObject()
public static ValueAnimator ofObject(TypeEvaluator evaluator, Object... values) {
ValueAnimator anim = new ValueAnimator();
anim.setObjectValues(values);
anim.setEvaluator(evaluator);
return anim;
}
可以看到,ofObject方法除了传递初始值和结束值之外,还需要传递一个TypeEvaluator类型的参数,这个TypeEvaluator是做什么的呢?由于ofObject方法的初始值和结束值都是自定义的对象,而ValueAnimator并不知道如何从初始的对象过度到结束的对象,所以需要调用者制定一个过度的规则,举个栗子:
Ring_Rotate.gif还是看这个小球绕圆环旋转的动画,现在定义一个对象来管理这个动画里的变量:
private class CirclePoint{
float angle;
public CirclePoint(float angle){
this.angle = angle;
}
}
小球坐标的变化其实就是小球在圆上角度的变化。暂时我们只放角度这一个变量。然后实现一下ofObject方法:
ValueAnimator animator = ValueAnimator.ofObject(new TypeEvaluator() {
@Override
public Object evaluate(float fraction, Object startValue, Object endValue) {
CirclePoint start = (CirclePoint) startValue;
CirclePoint end = (CirclePoint) endValue;
float startAngle = start.angle;
float endAngle = end.angle;
float currentAngle = startAngle + (endAngle - startAngle) * fraction;
return new CirclePoint(currentAngle);
}
}, new CirclePoint(0), new CirclePoint(360));
可以看到,我们传入了TypeEvaluator的实现类并重写了evaluate()方法。evaluate()方法中的第一个参数fraction就表示了动画的当前进度,范围是0到1,我们就根据这个进度值来计算对象当前应该是什么样的。后面两个参数就表示了动画的初始值和结束值。evaluate()方法里的逻辑还是非常简单的,之后我们让角度从0变化到360度。
好,以上我们就制定了从初始对象过度到结束对象时的规则,接下来就是监听动画的进度并实现动画了。全部代码:
public class RingRotateView extends View {
private Context context;
private Paint ringPaint, circlePaint;
private int width, height;//外圆弧宽高
private int outRadius, inRadius;//外圆弧和小圆半径
private float outX, outY, inX, inY;//外圆弧和小圆中心点
private float distance;//大圆中心点到小圆中心点的距离
private int PADDING = 50;
private int STROKEN_WIDTH = 4;//圆环宽度
private CirclePoint currentPoint;
public RingRotateView(Context context) {
super(context);
this.context = context;
init();
}
public RingRotateView(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
init();
}
public RingRotateView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
init();
}
private void init(){
ringPaint = new Paint();
ringPaint.setStrokeWidth(STROKEN_WIDTH);
ringPaint.setStyle(Paint.Style.STROKE);
ringPaint.setColor(Color.WHITE);
ringPaint.setAntiAlias(true);
circlePaint = new Paint();
circlePaint.setStyle(Paint.Style.FILL);
circlePaint.setColor(Color.RED);
circlePaint.setAntiAlias(true);
currentPoint = new CirclePoint(0);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
width = MeasureSpec.getSize(widthMeasureSpec);
height = MeasureSpec.getSize(heightMeasureSpec);
outRadius = width > height ? height / 2 - PADDING : width / 2 - PADDING;
inRadius = outRadius / 16;
outX = width / 2;
outY = height / 2;
distance = outRadius - inRadius -STROKEN_WIDTH / 2;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(outX, outY, outRadius, ringPaint);
inX = outX + distance * (float)Math.sin(currentPoint.angle / 360 * 2 * Math.PI);
inY = outY - distance * (float)Math.cos(currentPoint.angle / 360 * 2 * Math.PI);
canvas.drawCircle(inX, inY, inRadius, circlePaint);
}
public void startAnimation(){
ValueAnimator animator = ValueAnimator.ofObject(new TypeEvaluator() {
@Override
public Object evaluate(float fraction, Object startValue, Object endValue) {
CirclePoint start = (CirclePoint) startValue;
CirclePoint end = (CirclePoint) endValue;
float startAngle = start.angle;
float endAngle = end.angle;
float currentAngle = startAngle + (endAngle - startAngle) * fraction;
return new CirclePoint(currentAngle);
}
}, new CirclePoint(0), new CirclePoint(360));
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
currentPoint = (CirclePoint)valueAnimator.getAnimatedValue();
invalidate();
}
});
animator.setRepeatCount(-1);//无限重复
animator.setDuration(1000);
animator.start();
}
private class CirclePoint{
float angle;
public CirclePoint(float angle){
this.angle = angle;
}
}
}
如果现在我们需要让小球的颜色和大小不断变化,只需要修改一下对象:
private class CirclePoint{
float angle;
float radius;
int color;
public CirclePoint(float angle, float radius, int color){
this.angle = angle;
this.radius = radius;
this.color = color;
}
}
然后在TypeEvaluator中制定一下radius和color的改变规则,并在onDraw中绘制出来就可以了~
Interpolator
属性动画的Interpolator和补间动画的Interpolator意义相同,都是用来控制动画速率的。例如我们想让动画先加速播放,再减速播放,这时就可以用到Interpolator。Android为我们提供了几种Interpolator,比如LinearInterpolator可以让动画匀速播放,AccelerateDecelerateInterpolator是默认的Interpolator,可以让动画先加速后减速播放。在属性动画中,Interpolator直接控制了fraction的改变速率。我们也可以自己定义一个Interpolator,来让动画按照我们希望的速率变化。
animator.setInterpolator(new Interpolator() {
@Override
public float getInterpolation(float input) {
return 1 - (1 - input) * (1 - input) * (1 - input);
}
});
以上就实现了一个速率快速降低的Interpolator。
ObjectAnimator
其实ObjectAnimator才是属性动画中最常用的类,把它放到最后讲是因为它的原理和ValueAnimator完全相同,它也是继承于ValueAnimator的。ObjectAnimator的特点是可以操作对象拥有get/set方法的任意属性,相对于ValueAnimator,它使用起来更简洁。例如上文提到的将一个View从x轴200px移到400px处,只需要这么写:
ObjectAnimator animator = ObjectAnimator.ofFloat(view, "translationX", 200f, 400f);
animator.setDuration(3000);
animator.start();
由于ObjectAnimator是继承于ValueAnimator的,它也可以用到ValueAnimator中的所有方法,所以对于复杂一些的动画,用ObjectAnimator一样可以轻松搞定了~
总结
相对于视图动画,属性动画的核心在于它改变了对象的属性,而不仅仅是视图的变化。动画只是对象属性改变后在视图层面的表现形式。属性动画还有组合动画、ViewPropertyAnimator、xml实现等更多表现形式,但万变不离其宗,只要掌握了属性动画的原理,相信一切复杂的动画在你面前都是纸老虎!
网友评论