动画,分为三种,视图动画、帧动画以及属性动画。视图动画和帧动画出现的比较早,属性动画是3.0以后出来的。视图动画只能被用来设置View的动画,帧动画是像动图一样,设置背景图,属性动画是可扩展的,可以自定义任何类型和属性的动画。
视图动画分为四种:缩放、位移、伸缩、透明以及上述四种的任意组合动画。我比较早的文章中提到过这个视图动画,除了透明动画外,其他三种动画都需要一个坐标点来作为标准进行动画操作,这里面有三种:ABSOLUTE 针对自身左上角,像素 、 RELATIVE_TO_SELF 针对自身左上角,百分比 、 RELATIVE_TO_PARENT 针对自身左上角,父View的百分比,以前篇中有所描述。以位移动画为例,如果从A处移动到了B处,如果点击B处,点击事件是不会触发的,点击A处才会触发,这个有点尴尬,但设计就是如此。
帧动画像幻灯片一样,是图像可以动的动画,这个比较简单,一般现在的 gif和webP 格式图片也可以实现这个效果,使用帧动画要注意OOM的问题。
重点说说属性动画,视图动画能实现的功能,属性动画都能实现,并且扩展性更好,属性动画的点击事件随着视图走,内容从A移动到B,则点击B处有点击事件,点击A处则无反应。属性动画比较重要的两个类, ValueAnimator 和 ObjectAnimator, ValueAnimator 是基类,ObjectAnimator 是子类,但看名字,ValueAnimator 代表的是数值,重点在于数字;ObjectAnimator 代表的是物,重点在于对象。同时我们也知道,既然是父子关系,那么,基类包含的功能,子类肯定也有这个功能。举个简单的例子,我们看看 ValueAnimator 是怎么使用的。
public static interface AnimatorUpdateListener {
void onAnimationUpdate(ValueAnimator animation);
}
有个静态内部类,是个回调,看它的注释,通知动画的另一帧的出现,意思是动画执行过程中,会不断的回调它,那么我们可以在回调时通过参数 animation 获取到当前的getAnimatedValue() 对应的值,这个就是我们出入数据正在变化的当前值,如果我们想让一个view进行翻滚,那么可以
ValueAnimator animator = ValueAnimator.ofFloat(0.0f, 360.0f);
animator.setTarget(view);
animator.setDuration(5000).start();
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation){
float value = (float) animation.getAnimatedValue();
view.setRotation(value);
}
});
我们传进去的值是0和360,时间为5秒,这个监听就是在动画执行后,5秒内,动画里的值匀速从0变化到360,而getAnimatedValue()则会把当前这一帧的状态返回,我们获取到值后,然后调用view本身包含的旋转的方法,把数值传进去,view就旋转了。上面说了,基类支持的功能,子类都支持。我们如果把上述代码中的 ValueAnimator 替换成 ObjectAnimator,也一样可行。 ObjectAnimator.ofFloat(0.0f, 360.0f) 说白了,还是调用的 ValueAnimator 的 ofFloat()方法,那么 ObjectAnimator 有 ofFloat() 方法吗?有,并且更具体。
既然是属性动画,那么我们有没有不用自己调用view的属性的方式?我们看看 ObjectAnimator 类,它提供的ofFloat()方法,多了一个 String propertyName 参数,这个就是属性的名字的意思,我们知道,上述代码中是对view的旋转,如果用ObjectAnimator, 我们可以 ObjectAnimator.ofFloat(view, "rotation", 0.0f, 360.0f).setDuration(5000).start();就能实现上面一堆代码才实现的功能?我们 "rotation" 对应的就是 String propertyName ,这个方法里面,会把 propertyName 进行转换,会把名字的第一个字母变为大写,即把 "rotation"变为 "Rotation",然后在前面拼上 "set" 和 "get" 字段,组成了 "setRotation" 和 "getRotation",有没有发现很眼熟?这个就是View中的旋转方法的名字啊!实际上这里使用的是"setRotation" 方法,这样,"rotation" 变为 "setRotation",然后通过反射调用 setRotation() 方法,并且传入数据,数据就是5秒内匀速的从0到360的变化的值,这样,就相当于不停的调用setRotation()方法,所以view就旋转了。
我们这个是传入的值,那么view有没有提供这种方法呢? ObjectAnimator.ofFloat(this, View.ROTATION, 0.0f, 360.0f).setDuration(5000).start(); View 中提供了一系列静态内部类,都是 Property 的子类,里面封装好了 set 和 get 方法,我们直接使用即可。
组合动画呢,比如说想同时沿着x轴和y轴运动,可以使用 AnimatorSet 来实现,代码如下
ObjectAnimator animator1 = ObjectAnimator.ofFloat(view, "rotationX", 0.0f, 360.0f).setDuration(5000);
ObjectAnimator animator2 = ObjectAnimator.ofFloat(view, "rotationY", 0.0f, 360.0f).setDuration(5000);
AnimatorSet set = new AnimatorSet();
set.playTogether(animator1, animator2);
set.start();
AnimatorSet 继承于 Animator,里面有 start cancle 等方法,如果想让两个动画先后执行,可以
AnimatorSet s = new AnimatorSet();
s.play(anim1).before(anim2);
s.play(anim2).before(anim3);
关于这个类,网上有许多文章。关于 ObjectAnimator.ofFloat(this, "rotation", 0.0f, 360.0f).setDuration(5000).start();,点击进去看源码,发现进去就会调用下面的方法
@Override
public void setFloatValues(float... values) {
if (mValues == null || mValues.length == 0) {
// No values yet - this animator is being constructed piecemeal. Init the values with
// whatever the current propertyName is
if (mProperty != null) {
setValues(PropertyValuesHolder.ofFloat(mProperty, values));
} else {
setValues(PropertyValuesHolder.ofFloat(mPropertyName, values));
}
} else {
super.setFloatValues(values);
}
}
此时 mProperty 为 null, 会调用 setValues(PropertyValuesHolder.ofFloat(mPropertyName, values)); 方法,具体看 PropertyValuesHolder.ofFloat(mPropertyName, values),我们发现,包括上面提到的set方法,都是这里面执行的,我们发现了这一点,可以一试
PropertyValuesHolder a1 = PropertyValuesHolder.ofFloat("rotationY", 0.0f, 360.0f);
PropertyValuesHolder a2 = PropertyValuesHolder.ofFloat("rotationX", 0.0f, 360.0f);
PropertyValuesHolder a3 = PropertyValuesHolder.ofFloat("rotation", 0.0f, 360.0f);
ObjectAnimator.ofPropertyValuesHolder(view, a1, a2, a3).setDuration(5000).start();
我们发现,动画执行。我们看上面 setFloatValues() 方法,里面对 mProperty 有个判断,如果不为null,则会执行 setValues(PropertyValuesHolder.ofFloat(mProperty, values));操作,接着,我们再次尝试,把方法名替换成 View.ROTATION_X 来试试
PropertyValuesHolder a1 = PropertyValuesHolder.ofFloat(View.ROTATION_X, 0.0f, 360.0f);
PropertyValuesHolder a2 = PropertyValuesHolder.ofFloat(View.ROTATION_Y, 0.0f, 360.0f);
PropertyValuesHolder a3 = PropertyValuesHolder.ofFloat(View.ROTATION, 0.0f, 360.0f);
ObjectAnimator.ofPropertyValuesHolder(view, a1, a2, a3).setDuration(5000).start();
动画依旧执行。
android 在3.1中,增加了 ViewPropertyAnimator,这个也比较好用。比如想沿着x轴和y轴旋转,那么可以这样写
view.animate().rotationX(360).rotationY(360).setDuration(5000).start();
其实,最后的 start()方法没写上去也没关系,系统会隐式的替我们加上。执行后,这个也可以旋转,但是一直停留在该页面,旋转动画执行一次,再次点击时,不会执行,没找到原因。
我们知道,这个是自定义的动画,可以定制,怎么办呢?上段中提到,是通过拼接 "set" 和 方法名,然后反射调用的,那么,我们可以根据这个方式,来定义自己的动画。比如我们在一个自定义控件中,画一个小蓝球,控制球的颜色变化。先把简单的控件写出来
public class TranBlueView extends View {
public static final float RADIUS = 50f;
private PointF currentPoint;
private Paint mPaint;
public TranBlueView(Context context) {
this(context, null);
}
public TranBlueView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TranBlueView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.BLUE);
setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
startAnima();
}
});
}
@Override
protected void onDraw(Canvas canvas) {
if (currentPoint == null) {
currentPoint = new PointF(RADIUS, RADIUS);
}
drawCircle(canvas);
}
private void drawCircle(Canvas canvas) {
float x = currentPoint.x;
float y = currentPoint.y;
canvas.drawCircle(x, y, RADIUS, mPaint);
}
private void startAnima() {
...
}
public void setColor(int color) {
mPaint.setColor(color);
invalidate();
}
}
android:layout_height="match_parent"
/>
startAnima() 是动画方法,我们自定义了一个方法 setColor(int color),用来接收传进来的颜色值,然后传给 paint,然后绘制,改变球的颜色。看这段代码,当点击控件后,开始动画,小蓝球开始颜色变化。怎么写这个startAnima()方法呢,我们需要调用 ObjectAnimator.ofObject()方法,把 view 和 "color" 的值传进去,颜色值由 "#0000FF" 变化到 "#FF0000",怎么把这两个传进去并匀速变化呢? 幸好android提供了个接口 TypeEvaluator ,这里面控制值的变化,
public interface TypeEvaluator
public T evaluate(float fraction, T startValue, T endValue);
}
fraction 是当前进度的百分比,startValue 开始的状态值, endValue 的状态值,在这个例子中, startValue 是 "#0000FF" ,endValue 是 "#FF0000", fraction 是随着时间的进度条,从0.00 到 1.00 ,好了,我们可以写一个类,实现 TypeEvaluator 接口,来自定义颜色变化的方法,很幸运,android 也提供了一个匀速变化的类
public class ArgbEvaluator implements TypeEvaluator {
private static final ArgbEvaluator sInstance = new ArgbEvaluator();
public static ArgbEvaluator getInstance() {
return sInstance;
}
public Object evaluate(float fraction, Object startValue, Object endValue) {
int startInt = (Integer) startValue;
int startA = (startInt >> 24) & 0xff;
int startR = (startInt >> 16) & 0xff;
int startG = (startInt >> 8) & 0xff;
int startB = startInt & 0xff;
int endInt = (Integer) endValue;
int endA = (endInt >> 24) & 0xff;
int endR = (endInt >> 16) & 0xff;
int endG = (endInt >> 8) & 0xff;
int endB = endInt & 0xff;
return (int)((startA + (int)(fraction * (endA - startA))) << 24) |
(int)((startR + (int)(fraction * (endR - startR))) << 16) |
(int)((startG + (int)(fraction * (endG - startG))) << 8) |
(int)((startB + (int)(fraction * (endB - startB))));
}
}
似是而非的单利,构造方法没有私有,并且getInstance()方法加了个@hide的注解,能反射调用却不能直接调用,是专门给系统使用的吗?嗯,有点搞不懂想干嘛。重点看一下 evaluate() 方法, startValue, endValue 虽然是 Object ,但是都要转换成 int 类型,也就是这两个值都必须是 int 类型,然后经过一系列的转换,算出下一帧的颜色值,颜色是由 argb 组成,那么就计算下一个值就好说了,返回的值也是 int 类型,这也是我们自定义控件中 setColor(int color)中形参是int类型的原因。颜色值String转为为16进制的int值,用Color.parseColor("#0000FF") 方法,接下来我们就可以写出转换颜色的动画代码了
private void startAnima() {
ObjectAnimator anim = ObjectAnimator.ofObject(this, "color", new ArgbEvaluator(),
Color.parseColor("#0000FF"), Color.parseColor("#FF0000"));
anim.setDuration(5000);
anim.start();
}
然后运行一下,就可以看到小球的颜色变化。我在看 ObjectAnimator 源码时,发现还有一个 ofArgb() 方法,看名字就是颜色转化的,我就试了一下,把动画的代码替换为如下
private void startAnima() {
ObjectAnimator anim = ObjectAnimator.ofArgb(this, "color",
Color.parseColor("#0000FF"), Color.parseColor("#FF0000"));
anim.setDuration(5000);
anim.start();
}
这个方法是 21 以后才有的,使用需谨慎,需要做版本判断。
上面的例子,是改变一个变量,从A变化到B,如果我们想同时改变两个变量的值怎么办?我们可以添加 anim.addUpdateListener() 回到,在回调中拿到当前的值,根据当前值变化的程度来修改另外一个值;另一种方法就是封装成一个对象,把对象传进动画的修值器中,来改变相应的两个变量的值。例如我们如果想让小球从左上角滑到右下角,怎么办?我们在代码中drawCircle(Canvas canvas) 方法中,是根据 x 和 y 的值来确定圆心的,我们可以通过改变x和y的值,来移动小球的位置,这里就牵涉改变两个变量了。我们可以先封装一个对象,例如高版本系统中添加的对象 PointF,然后自定义一个 PointFEvaluator ,
public class PointFEvaluator implements TypeEvaluator
private PointF mPoint;
public PointFEvaluator() {
}
public PointFEvaluator(PointF reuse) {
mPoint = reuse;
}
@Override
public PointF evaluate(float fraction, PointF startValue, PointF endValue) {
float x = startValue.x + (fraction * (endValue.x - startValue.x));
float y = startValue.y + (fraction * (endValue.y - startValue.y));
if (mPoint != null) {
mPoint.set(x, y);
return mPoint;
} else {
return new PointF(x, y);
}
}
}
我们可以看一下 evaluate() 方法,根据起始位置和进度,计算出当前点的值,然后封装到一个对象中返回,我们在View中动画和自定动画方法
private void startAnima() {
PointF startPoint = new PointF(RADIUS, RADIUS);
PointF endPoint = new PointF(getWidth() - RADIUS, getHeight() - RADIUS);
ValueAnimator anim = ObjectAnimator.ofObject(this, "verpoint",new PointFEvaluator(), startPoint, endPoint);
anim.setDuration(5000);
anim.start();
}
public void setVerpoint(PointF pointF){
currentPoint.set(pointF);
invalidate();
}
RADIUS 是开始圆心的坐标和半径,所以开始位置是 startPoint,结束的位置需要控件的宽和高减去半径,即endPoint,我们自定义方法名为 "verpoint",所以对应的接收数据方法为setVerpoint(PointF pointF),由于 pointF 就是计算出的值,我们在View中定义的成员变量currentPoint来接收pointF的值,然后刷新界面即可。我们能看到小球匀速下降,如果想做小球加速运动,可以设置插值器 anim.setInterpolator(new AccelerateInterpolator(2f)); // 加速下降, 这样更自然点。如果我们想小球自由落体同时网上反弹两下再落地,可以使用 anim.setInterpolator(new BounceInterpolator()); // 球落地弹起效果,这都是系统封装好的,我们也可以自定义,不过一般用不到,我们可以把小球的宽一直定位到屏幕中间,然后做自由落体,如下
private void startAnima() {
PointF startPoint = new PointF(getWidth() / 2, RADIUS);
PointF endPoint = new PointF(getWidth() / 2, getHeight() - RADIUS);
ValueAnimator anim = ObjectAnimator.ofObject(this, "verpoint",new PointFEvaluator(), startPoint, endPoint);
anim.setInterpolator(new BounceInterpolator()); // 球落地弹起效果
anim.setDuration(5000);
anim.start();
}
如果我们想一边落体,一遍颜色变化,我们可以使用组合动画,示例如下
private void startAnima() {
PointF startPoint = new PointF(RADIUS, RADIUS);
PointF endPoint = new PointF(getWidth() - RADIUS, getHeight() - RADIUS);
ValueAnimator anim = ObjectAnimator.ofObject(this, "verpoint",new PointFEvaluator(), startPoint, endPoint);
anim.setInterpolator(new BounceInterpolator()); // 球落地弹起效果
ObjectAnimator anim2 = ObjectAnimator.ofObject(this, "color", new ArgbEvaluator(),
Color.parseColor("#0000FF"), Color.parseColor("#FF0000"));
AnimatorSet animSet = new AnimatorSet();
animSet.play(anim).with(anim2);
animSet.setDuration(5000);
animSet.start();
}
点击一下,就能看到效果。可以自豪的说,看,自由落体把小蓝球的脸都吓得变色了。