市面上的大多数应用,多多少少都会通过动画,让应用多一些灵动性和趣味性,并且在视图之间的切换会显得更加自然。例如许多应用都定制了自己的下拉刷新中的动画,让应用增色不少。Android动画有三种:
- View 动画
- 属性动画
- 帧动画
View动画
View动画是Android里面常用的动画方式。View动画直接作用于View上面。View动画有且只有四种:平移、缩放、旋转、透明度。平移就是左右上下位置移动,缩放就是大小的变换,旋转就是视图的2D旋转,透明度就不用多说了,由于View动画采用xml方式实现较多,所以这里我们着重介绍下xml方式下的View动画标签。
动画 | 标签 | 类 |
平移动画 | TranslateAnimation | |
缩放动画 | ScaleAnimation | |
旋转动画 | RotateAnimation | |
透明度动画 | AlphaAnimation | |
动画集合 | AnimationSet |
<set xmlns:android="http:///apk/res/android"
android:interpolator="@[package:]anim/interpolator_resource"
android:shareInterpolator=["true" | "false"] >
<!--
fromAlpha 起始透明度
toAlpha 结束透明度
1.0完全不透明 0.0完全透明
-->
<alpha
android:fromAlpha="float"
android:toAlpha="float" />
<!--
fromXScale X坐标缩放起始值
toXScale X坐标缩放结束值
fromYScale Y坐标缩放起始值
toYScale Y坐标结束值
1.0则表示原大小
privotX&privotY 缩放中心点,有三种模式 %,%p,固定值,其中%是相对于本身的一个比例,
如果privotX&privotY都是50%,那么就是视图中心点,%p则是相对于父容器的一个比例。
-->
<scale
android:fromXScale="float"
android:toXScale="float"
android:fromYScale="float"
android:toYScale="float"
android:pivotX="float"
android:pivotY="float" />
<!--
fromXDelta X轴相对于当前位置的起始值
toXDelta X轴相对于当前位置的结束值
fromYDelta Y轴相对于当前位置的起始值
toYDelta Y轴相对于当前位置的结束值%p则是相对于父容器长宽的一个比例。
-->
<translate
android:fromXDelta="float"
android:toXDelta="float"
android:fromYDelta="float"
android:toYDelta="float" />
<!--
fromDegrees 起始角度
toDegrees 结束角度
pivotX&pivotY 旋转中心点,有三种模式 %,%p,固定值,其中%是相对于本身的一个比例,
如果privotX&privotY都是50%,那么就是视图中心点,%p则是相对于父容器的一个比例。
-->
<rotate
android:fromDegrees="float"
android:toDegrees="float"
android:pivotX="float"
android:pivotY="float" />
<set>
...
</set>
</set>
插值器
我们可以看到这里有一个 android:interpolator 属性。这个在动画中十分重要,interpolator 插值器,控制动画是以一定的速度播放还是先快后慢等动画播放效果。android内置了丰富的插值器,不同的插值器对整个动画过程会产生不同的影响,默认的插值器是常量速度插值器,我们可以通过插值器让动画执行过程由慢到快的变换。下面是Android系统提供的部分常见插值器:
名称 | 效果 |
AccelerateInterpolator | 动画由慢向快加速 |
DecelerateInterpolator | 动画由快到慢减速 |
AccelerateDecelerateInterpolator | 动画中间快两端慢 |
AnticipateInterpolator | 动画反效果后再按照效果执行,例如动画是向右平移100px,那么会先向左平移一段事件再向右平移到终点 |
BounceInterpolator | 动画结束的时候会有一个弹跳的效果,自行脑补弹珠在地上弹起的效果 |
LinearInterpolator | 常量速率执行动画,默认 |
OvershootInterpolator | 动画结束时候,会反效果执行一段时间,例如向右平移100px,向右平移到终点,会反方向平移一定距离 |
CycleInterpolator | 动画按照一定形式循环播放一定的次数,例如向右平移100px,这时候会向右平移100px后,会反方向,向左平移100px,这样重复一定次数 |
AnticipateOvershootInterpolator | 组合了AnticipateInterpolator和OvershootInterpolator的效果 |
View动画实现
View动画有两种实现方式,一种是硬编码,一种是通过XML文件,建议使用XML方式。如果采用XML文件的话,那么需要在/res文件夹下面新建anim文件夹,并且将XML动画文件放在文件夹下面。
XML文件形式
<!--放置在res/anmi文件夹下面, 实现由小到大,并且从透明到可见的动画-->
<!-- res/anim/alpha_scale.xml -->
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http:///apk/res/android">
<alpha
android:fromAlpha="0"
android:toAlpha="1"/>
<scale
android:fromXScale="0"
android:toXScale="1"
android:fromYScale="0"
android:toYScale="1"
android:pivotX="50%"
android:pivotY="50%"
/>
</set>
//加载XML动画并且播放
public void loadAnimation(){
//加载动画
Animation animation = AnimationUtils.loadAnimation(this,R.anim.alpha_scale);
//设定动画播放时长
animation.setDuration(3000);
//通过View.startAnimation(Animation)执行动画
findViewById(R.id.iv).startAnimation(animation);
}
纯硬编码方式
//纯硬编码方式
public void loadAnimation2(){
//动画效果由透明动画与缩放动画组合成,所以需要用AnimationSet将他们装起来
AnimationSet animationSet = new AnimationSet(true);
//创建透明度动画
AlphaAnimation alphaAnimation = new AlphaAnimation(0, 1);
//创建缩放动画
ScaleAnimation scaleAnimation = new ScaleAnimation(0f, 1f, 0f, 1f,
Animation.RELATIVE_TO_SELF,0.5f, Animation.RELATIVE_TO_SELF,0.5f);
animationSet.addAnimation(alphaAnimation);
animationSet.addAnimation(scaleAnimation);
animationSet.setDuration(3000);
//通过View.startAnimation(Animation)播放动画
findViewById(R.id.iv).startAnimation(animationSet);
}
View动画常用方法#####:
方法 | 描述 |
AnimationUtils.loadAnimation(int) | 加载xml动画 |
Animation.setDuration(long) | 设置动画播放时间 |
Animation.setRepeatCount(int) | 设置动画重复次数 |
Animation.setFillAfter(boolean) | 设置动画播放后是否停留在当前位置 |
Animation.start() | 播放动画 |
Animation.cancel() | 停止动画 |
View.startAnimation(Animation) | 播放动画 |
需要特别注意,View动画的交互事件只在动画前的位置有效。例如我向右平移了100px,并停留在该位置,但是在该位置无法触发触摸等事件,因为View动画的交互事件只在原来的位置有效!
属性动画
属性动画是Android 从API 11(Android3.0)开始支持的动画类型。虽然官方只支持API11以上(包括),但是我们依旧可以使用NineOldAndroids在API11下面采用属性动画的用法(部分动画效果实质还是View动画)。属性动画十分强大,几乎可以在任何对象上产生动画效果。属性动画的原理是:在一定的动画时长内,在不同的时间点(帧)下,不断改变对象的属性值,从而产生动画效果。属性动画由三部分构成:动画控制器、插值器、估值器。
属性动画控制器
属性动画控制器,控制着动画的播放&停止,动画的时间等等,并提供一系列的动画回调。控制器是我们使用属性动画的入口。我们经常使用的是ObjectAnimator来实现动画效果,如果需要更加灵活的控制动画,那么可以通过ValueAnimator实现。
类 | 标签 | 描述 |
ValueAnimator | 属性动画的基础类,本身并不实现任何动画效果,主要充当属性动画的控制器,控制播放、暂停、插值器的应用、时长设置、属性动画的回调方法等,如果我们需要通过它实现动画效果的话,那么需要配合回调方法手动更新对象的属性值,下面将会给出例子 | |
ObjectAnimator | 继承于ValueAnimator,实现属性动画的常用类。需要动画目标对象提供Setter封装方法。ObjectAnimator的实现原理是:当ValueAnimator根据插值器和估值器计算出下一个时间点需要的属性值之后,ObjectAnimator会通过Java的反射机制,自动调用设置对象属性的方法,以此达到动画的效果。 | |
AnimatorSet | 属性动画集,将多个属性动画聚集起来,可以根据需求控制动画的播放顺序 |
属性动画插值器
这里的插值器与上面View动画的插值器是一致的,属性动画的原理是不停的改变属性值,来实现动画效果,这个属性值的计算是由插值器与估值器共同计算出来的,而插值器是主要的影响因素。例如上面有一AccelerateInterpolator(动画由慢到快)插值器,插值的增长速率会慢慢变快。以此才能达到由慢到快的动画效果。下图是AccelerateInterpolator插值器每两个时间点的差值。
可以很清晰的看到,越往下,每两个时间点的差值越大,也就是插值随着时间加速增长,因此,才能让动画由慢到快。不同的插值器,在动画开始到结束过程中,产生的插值数值也不一样,需要注意的是,这里的插值数值是随着动画时间变化的一个比例值,不是具体应该被设置的属性值。
属性动画估值器
上面我们提到动画播放过程中的属性值由插值器和估值器共同计算,由于插值器只得出了随着时间变化而改变的一个插值比例值,所以我们还需要通过动画估值器来计算出该时间点最终的属性值。Android系统提供的默认估值器有:
估值器 | 描述 |
IntEvaluator | 当作用属性值是Int类型时候,可以使用该估值器计算最终属性值 |
FloatEvaluator | 当作用属性值是Float类型时候,可以使用该估值器计算最终属性值 |
ArgbEvaluator | 当作用属性值是颜色类型时候,可以使用该估值器计算最终属性值 |
TypeEvaluator | 如果当作用的属性值系统并未提供相应的估值器,那么就需要通过TypeEvaluator自定义估值器 |
ObjectAnimator 属性动画实例
我们比较常用ObjectAnimator实现属性动画,使用ObjectAnimator的时候,有一个前提条件,那就是需要目标对象提供作用属性的Setter封装方法。下面是Android 11(Android 3.0)之后。我们常用且View基类中提供了Setter方法的属性。
| 属性名称 |描述|
| ----- |------|
|alpha|透明度|
|scaleX|X轴缩放值|
|scaleY|Y轴缩放值|
|rotation|平面旋转值|
|rotationX|X轴3D旋转|
|rotationY|Y轴3D旋转|
|pivotX|旋转/缩放中心X轴坐标|
|pivotY|旋转/缩放中心Y轴坐标|
|x|X坐标|
|y|Y坐标|
|translationX| X轴 平移偏移量|
|translationY |Y轴 平移偏移量|
|scrollX|X轴 滚动偏移量|
|scrollX|Y轴 滚动偏移量|
利用ObjectAnimator实现上面View动画从0到1透明并且逐渐放大的效果:
private void playObjectPropertyAnimation() {
final ImageView iv = (ImageView) findViewById(R.id.iv);
final AnimatorSet animatorSet = new AnimatorSet();
//作用在ImageView的alpha、scaleX、scaleY属性上
ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(iv, "alpha", 0, 1);
ObjectAnimator scaleXAnimator = ObjectAnimator.ofFloat(iv, "scaleX", 0, 1);
ObjectAnimator scaleYAnimator = ObjectAnimator.ofFloat(iv, "scaleY", 0, 1);
//这里同一时间播放上面三个动画
animatorSet.playTogether(alphaAnimator, scaleXAnimator, scaleYAnimator);
animatorSet.setDuration(3000);
animatorSet.start();
}
如果目标对象的作用属性没有提供Setter方法,那么我们需要创建包装类,提供相应的Setter方法。我们如果要将动画作用于宽度&高度上,但是View对象并没有提供宽度&高度的Setter方法,那么我们需要创建相应的包装类。
//View宽度包装类
public class ViewWidthWrapper {
private View v;
public ViewWidthWrapper(View v){
this.v = v;
}
//宽度Setter方法
public void setWidth(int width){
ViewGroup.LayoutParams layoutParams = v.getLayoutParams();
layoutParams.width = width;
v.setLayoutParams(layoutParams);
v.invalidate();
}
public int getWidth(){
return v.getWidth();
}
}
//播放宽度属性
private void playCustomPropertyAnimation() {
final ImageView iv = (ImageView) findViewById(R.id.iv);
//创建View宽度包装类
final ViewWidthWrapper widthWrapper = new ViewWidthWrapper(iv);
iv.post(new Runnable() {
@Override
public void run() {
//播放宽度属性动画
final ObjectAnimator widthAnimator = ObjectAnimator.ofInt(widthWrapper,"width",widthWrapper.getWidth(),widthWrapper.getWidth()+100);
widthAnimator.setDuration(3000);
widthAnimator.start();
}
});
}
ValueAnimator 属性动画实例
我们也可以利用ValueAnimator更加灵活的实现动画,但是相对于ObjectAnimator会相对复杂一点。我们需要联合ValueAnimator.AnimatorUpdateListener回调来实现属性动画。下面是用ValueAnimator实现上面ObjectAnimator实现的动画效果。
public void loadValueAnimation(){
final ImageView iv = (ImageView)findViewById(R.id.iv);
//估值器
final FloatEvaluator floatEvaluator = new FloatEvaluator();
//透明度动画
ValueAnimator alphaAnimator = ValueAnimator.ofFloat(0,1);
alphaAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
//这里我们需要自己计算最终属性值
float value = floatEvaluator.evaluate(valueAnimator.getAnimatedFraction(),0,1);
//自己更新作用属性值
iv.setAlpha(value);
}
});
//X轴缩放动画
ValueAnimator scaleXAnimator = ValueAnimator.ofFloat(0,1);
scaleXAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
float value = floatEvaluator.evaluate(valueAnimator.getAnimatedFraction(),0,1);
iv.setScaleX(value);
}
});
//Y轴缩放值
ValueAnimator scaleYAnimator = ValueAnimator.ofFloat(0,1);
scaleXAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
float value = floatEvaluator.evaluate(valueAnimator.getAnimatedFraction(),0,1);
iv.setScaleY(value);
}
});
final AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(alphaAnimator,scaleXAnimator,scaleYAnimator);
animatorSet.setDuration(3000);
iv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
animatorSet.start();
}
});
}
通过代码我们可以看到,ValueAnimator只是帮我们计算了插值并提供了一个插值变化的回调方法,我们需要自己手动在回调方法里面,根据估值器计算最终属性值,并且自己更新View的属性值,最终才能实现动画效果。相比较ObjectAnimator来说,实现同一个效果会相对麻烦,但是灵活性增高了。例如ValueAnimator不需要我们提供Setter方法。
属性动画常用方法
方法名称 | 描述 |
ObjectAnimator.ofXX | 属性动画的主要创建方法,根据作用属性值来创建相应的属性动画,有ofInt,ofFloat等方法 |
ObjectAnimator.setDuration(int) | 设置动画播放时长 |
ObjectAnimator.start() | 播放动画 |
ObjectAnimator.cancel() | 停止播放动画,会暂停在当前这一帧 |
ObjectAnimator.end() | 停止播放动画集,直接跳到动画最后一帧 |
ObjectAnimator.setRepeatCount(int) | 设置动画重复次数,ValueAnimator.INFINITE表示无限次数 |
ObjectAnimator.setRepeatMode(int) | 设置动画重复模式,有两种,一种是ValueAnimator.RESTART[重新播放],一直是ValueAnimator.REVERSE[反序播放] |
ObjectAnimator.setInterpolator(Interpolator) | 设置插值器 |
ObjectAnimator.setEvaluator(TypeEvaluator) | 设置估值器 |
AnimatorInflater.loadAnimator(Context,int) | 加载Xml文件形式的属性动画 |
AnimatorSet.play(Animator) | 将某个属性动画添加进入属性动画集,这个方法会返回一个Builder对象,通过这个Builder对象,我们可以简单控制它与其他属性动画的播放顺序,可以通过Builder.after(Animator),Builder.before(Animator),Builder.with(Animator)这三个方法来控制它与其他属性动画的播放顺序 |
AnimatorSet.playSequentially(Animator...) | 按照添加的顺序播放属性动画集 |
AnimatorSet.playTogether(Animator...) | 同一时间播放动画集 |
AnimatorSet.setInterpolator(Interpolator) | 设置插值器 |
AnimatorSet.setDuration(int) | 设置动画集的播放时长,动画集设置的时长会覆盖掉单个动画设置的时长 |
AnimatorSet.start() | 播放动画集 |
AnimatorSet.cancel() | 停止播放动画集,会停止在当前这一帧 |
AnimatorSet.end() | 停止播放动画集,会跳到动画最后一帧 |
帧动画
帧动画是直接由一副一副图片组成,并且按照一定的顺序和间隔进行播放,这就构成了帧动画。Android帧动画的实现十分简单,通过AnimationDrawable就可以很简单的实现帧动画:
<!--帧动画的XML文件,放置在res/drawable下面 -->
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http:///apk/res/android">
<!--这里是一副一副帧画面,duration是每幅图停留的时间 -->
<item android:drawable="@drawable/a1" android:duration="150"/>
<item android:drawable="@drawable/a2" android:duration="150"/>
</animation-list>
private void playFrameAnimation(){
ImageView iv = (ImageView) findViewById(R.id.iv);
//这里跟我们设置其他图片资源是一样的
iv.setImageResource(R.drawable.ani);
//将ImageView图片资源转换成AnimationDrawable
AnimationDrawable animationDrawable = (AnimationDrawable) iv.getDrawable();
//播放帧动画
animationDrawable.start();
}
其他方式实现动画效果
除了用以上几种Android提供的方式实现动画之外,还可以通过自定义View实现动画效果。
我们可以在onDraw()方法里面,不停调用invaildate()来产生动画效果,有一些动画效果就是通过这种方式实现的。例如下面就是通过自定义View方式粗糙的仿荔枝FM加载稍等提示框的效果。
public class LizhiLoadingView extends View {
private float lineOneStartY,lineOneEndY;
private float lineSecondStartY,lineSecondEndY;
private float lineThirdStartY,lineThirdEndY;
private boolean onePlus;
private Paint mPaint;
public LizhiLoadingView(Context context) {
super(context);
initPaint();
}
public LizhiLoadingView(Context context, AttributeSet attrs) {
super(context, attrs);
initPaint();
}
public LizhiLoadingView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initPaint();
}
private void initPaint(){
mPaint = new Paint();
mPaint.setColor(Color.BLACK);
mPaint.setStrokeWidth(20);
mPaint.setStyle(Paint.Style.FILL);
lineOneStartY = 0;
lineOneEndY = 100;
lineSecondStartY = 10;
lineSecondEndY = 80;
lineThirdStartY = 0;
lineThirdEndY = 100;
onePlus = true;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawLine(80,lineOneStartY,80,lineOneEndY,mPaint);
canvas.drawLine(150,lineSecondStartY,150,lineSecondEndY,mPaint);
canvas.drawLine(220,lineThirdStartY,220,lineThirdEndY,mPaint);
//下面改变绘画参数
if(onePlus){
lineOneStartY++;
lineThirdStartY++;
lineSecondStartY --;
lineOneEndY--;
lineThirdEndY--;
lineSecondEndY++;
}else{
lineOneStartY--;
lineThirdStartY--;
lineSecondStartY ++;
lineOneEndY++;
lineThirdEndY++;
lineSecondEndY--;
}
if(lineOneStartY==25){
onePlus = false;
}else if(lineOneStartY ==0){
onePlus = true;
}
//这里导致onDraw()被不停调用
invalidate();
}
}