市面上的大多数应用,多多少少都会通过动画,让应用多一些灵动性和趣味性,并且在视图之间的切换会显得更加自然。例如许多应用都定制了自己的下拉刷新中的动画,让应用增色不少。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();


    }


}