Android动画深入分析

340 阅读11分钟

导语

本章学习内容:介绍View动画和自定义View动画,View动画一些特殊的使用场景,对属性动画全面性的介绍,使用动画的一些注意事项。

主要内容

  • View动画
  • View动画的特殊使用场景
  • 属性动画
  • 使用动画的注意事项

具体内容

View动画

View动画的作用对象是View,支持四种动画效果:

  • 平移
  • 缩放
  • 旋转
  • 透明
View动画的种类

上述四种变换效果对应着Animation四个子类: TranslateAnimation 、 ScaleAnimation 、 RotateAnimation 和 AlphaAnimation 。这四种动画皆可以通过XML定义,也可以通过代码来动态创建。

名称标签子类效果
平移动画TranslateAnimation移动View
缩放动画ScaleAnimation放大或缩小View
旋转动画RotateAnimation旋转View
透明度动画AlphaAnimation改变View的透明度

xml定义动画:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="300"
  //动画插值器影响动画的播放速度
android:interpolator="@android:anim/accelerate_interpolator"
  //表示集合中的动画是否和集合共享一个插值器
android:shareInterpolator="true" >
//透明度动画,对应 AlphaAnimation 类,可以改变 View 的透明度
  <alpha
        android:duration="3000"
        android:fromAlpha="0.0"
        android:toAlpha="1.0" />
          //旋转动画,对应着 RotateAnimation ,它可以使 View 具有旋转的动画效果
    <rotate
        android:duration="2000"
        android:fromDegrees="0"
        android:interpolator="@android:anim/accelerate_decelerate_interpolator"
        android:pivotX="50%"
        android:pivotY="50%"
        android:startOffset="3000"
        android:toDegrees="180" />
       <!--通过设置第一个alpha动画播放3s后启动rotate动画实现组合动画,如果不设置startOffset则同时播放
    pivotX:表示旋转时候的相对轴的坐标点,即围绕哪一点进行旋转,默认情况下轴点是 View 中心
    -->
      //平移动画,对应 TranslateAnimation 类,可以使 View 完成垂直或者水平方向的移动效果。
    <translate
        android:fromXDelta="500"
        android:toXDelta="0" />
          //缩放动画,对应 ScaleAnimation 类,可以使 View 具有放大和缩小的动画效果。
    <scale
        android:duration="1000"
        android:fromXScale="0.0"
        android:fromYScale="0.0"
        android:interpolator="@android:anim/accelerate_decelerate_interpolator"
        android:pivotX="50"
        android:pivotY="50"
        android:toXScale="2"
        android:toYScale="2" />
    </set>
  1. 标签表示动画集合,对应AnimationSet类,可以包含一个或若干个动画,内部还可以嵌套其他动画集合。
    1. android:interpolator 表示动画集合所采用的插值器,插值器影响动画速度,比如非匀速动画就需要通过插值器来控制动画的播放过程。
    2. android:shareInterpolator 表示集合中的动画是否和集合共享同一个插值器,如果集合不指定插值器,那么子动画就需要单独指定所需的插值器或默认值。
  2. 、、  、  这几个子标签分别代表四种变换效果。
  3. android:fillAfter属性表示动画结束以后, View 是否停留在结束动画的位置,如果为 false , View 会回到动画开始的位置。这个参数在动画 XML 文件的  节点中设置或在程序 Java 代码中进行设置:setFillAfter(true)。
  4. 定义完View动画的xml后,通过以下代码应用动画:
 Animation anim = AnimationUtils.loadAnimation(context,R.anim.animation_test);
 view.startAnimation(anim);

代码动态创建动画:

public static interface AnimationListener {
    void onAnimationStart(Animation animation);
    void onAnimationEnd(Animation animation);
    void onAnimationRepeat(Animation animation);
}
自定义View动画

除了系统提供的四种动画外,我们可以根据需求自定义动画,自定义一个新的动画只需要继承 Animation 这个抽象类,然后重写它的 inatialize 和 applyTransformation 这两个方法,在 initialize 方法中做一些初始化工作,在 Transformation 方法中进行矩阵变换即可,很多时候才有 Camera 来简化矩阵的变换过程,其实自定义动画的主要过程就是矩阵变换的过程,矩阵变换是数学上的概念,需要掌握该方面知识方能轻松实现自定义动画,例子可以参考 Android 的 APIDemos 中的一个自定义动画 Rotate3dAnimation ,这是一个可以围绕 Y 轴旋转并同时沿着 Z 轴平移从而实现类似一种 3D 效果的动画。

帧动画

帧动画是顺序播放一组预先定义好的图片,使用简单,但容易引起OOM,所以在使用帧动画时应尽量避免使用过多尺寸较大的图片。系统提供了另一个类 AnimationDrawble 来使用帧动画,使用的时候,需要通过 XML 定义一个 AnimationDrawble ,如下:

//\res\drawable\frame_animation_list.xml
<?xml version="1.0" encoding="utf-8"?>
<!--
    根标签为 animation-list,其中 oneshot 代表着是否只展示一遍,设置为 false 会不停的循环播放动画
    根标签下,通过 item 标签对动画中的每一个图片进行声明
    android:duration 表示展示所用的该图片的时间长度
 -->
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="false">
    <item
        android:drawable="@drawable/one"
        android:duration="2000"/>
    <item
        android:drawable="@drawable/two"
        android:duration="2000"/>
    <item
        android:drawable="@drawable/three"
        android:duration="2000"/>
</animation-list

View动画的特殊使用场景

View 动画除了可以实现的四种基本的动画效果外,还可以在一些特殊的场景下使用,比如在 ViewGroup 中可以控制子元素的出场效果,在 Activity 中可以实现不同 Activity 之间的切换效果。

LayoutAnimation

作用于ViewGroup,为ViewGroup指定一个动画,当它的子元素出场时都会具有这种动画效 果,一般用在ListView上。

//res/anim/layout_animation.xml
<?xml version="1.0" encoding="utf-8"?>
<layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
    android:delay="0.5"
    android:animationOrder="normal"
    android:animation="@anim/zoom_in">
</layoutAnimation>
  • android:delay 表示子元素开始动画的延时时间,取值为子元素入场动画时间 duration 的倍数,比如子元素入场动画时间周期为 300ms ,那么 0.5 表示每个子元素都需要延迟 150ms 才能播放入场动画,即第一个子元素延迟 150ms 开始播放入场动画,第二个子元素延迟 300ms 开始播放入场动画,依次类推进行。
  • android:animationOrder 表示子元素动画的开场顺序,normal(正序)、reverse(倒序)、random(随机)。
  • 为 ViewGroup 指定属性 android:layoutAnimation="@anim/layout_animation"。

通过 LayoutAnimationController 来实现:

//用于控制子 view 动画效果
LayoutAnimationController layoutAnimationController= new LayoutAnimationController(AnimationUtils.loadAnimation(this,R.anim.zoom_in));
layoutAnimationController.setOrder(LayoutAnimationController.ORDER_NORMAL);
listView.setLayoutAnimation(layoutAnimationController);
listView.startLayoutAnimation();
Activity的切换效果

我们可以自定义Activity的切换效果,主要通过overridePendingTransition(int enterAnim , int exitAnim) 方法。该方法必须要在 startActivity(intent) 和 finish() 方法之后调用才会有效。

//启动新的Activity带动画
Intent intent=new Intent(MainActivity.this,Main2Activity.class);
startActivity(intent);
overridePendingTransition(R.anim.zoom_in,R.anim.zoom_out);
//退出Activity本身带动画
@Override
public void finish() {
    super.finish();
    overridePendingTransition(R.anim.zoom_in,R.anim.zoom_out);
}

Fragment 也可以添加切换动画,通过 FragmentTransation 中的 setCustomAnimations() 方法来实现切换动画,这个动画需要的是 View 动画,不能使用属性动画,因为属性动画也是 API11 才引入的,不兼容。

属性动画

属性动画是 API 11 引入的新特性,属性动画可以对任何对象做动画,甚至还可以没有对象。可以在一个时间间隔内完成对象从一个属性值到另一个属性值的改变。与View动画相比,属性动画几乎无所不能,只要对象有这个属性,它都能实现动画效果。API11以下可以通过 nineoldandroids 库来兼容以前版本。

属性动画有ValueAnimator、ObjectAnimator和AnimatorSet等概念。其中ObjectAnimator继承自ValueAnimator,AnimatorSet是动画集合。

举例:

  1. 改变一个对象 TranslationY 属性,让其沿着 Y 轴平移一段距离:
private void translateViewByObjectAnimator(View targetView){
//TranslationY 目标 View 要改变的属性
//ivShow.getHeight() 要移动的距离
    ObjectAnimator objectAnimator=ObjectAnimator.ofFloat(targetView,"TranslationY",ivShow.getHeight());
    objectAnimator.start();
}
  1. 改变一个对象的背景色属性,3秒内从0xFFFF8080到0xFF8080FF渐变,无限循环且有反转效果:
 private void changeViewBackGroundColor(View targetView){
     ValueAnimator valueAnimator=ObjectAnimator.ofInt(targetView,"backgroundColor", Color.RED,Color.BLUE);
     valueAnimator.setDuration(3000);
     //设置估值器,该处插入颜色估值器
     valueAnimator.setEvaluator(new ArgbEvaluator());
     //无限循环
     valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
     //反转模式
     valueAnimator.setRepeatMode(ValueAnimator.REVERSE);
     valueAnimator.start();
 }

动画集合,5 秒内对 View 旋转、平移、缩放和透明度进行了改变:

 private void startAnimationSet(View targetView){
     AnimatorSet animatorSet=new AnimatorSet();
     animatorSet.playTogether(ObjectAnimator.ofFloat(targetView,"rotationX",0,360),
              //旋转
             ObjectAnimator.ofFloat(targetView,"rotationY",0,360),
             ObjectAnimator.ofFloat(targetView,"rotation",0,-90),
              //平移
             ObjectAnimator.ofFloat(targetView,"translationX",0,90),
             ObjectAnimator.ofFloat(targetView,"translationY",0,90),
              //缩放
             ObjectAnimator.ofFloat(targetView,"scaleX",1,1.5f),
             ObjectAnimator.ofFloat(targetView,"scaleY",1,1.5f),
              //透明度
             ObjectAnimator.ofFloat(targetView,"alpha",1,0.25f,1));
             animatorSet.setDuration(3000).start();
 }

也可以通过在xml中定义在 res/animator/ 目录下。具体如下:

 \res\animator\value_animator.xml
 <?xml version="1.0" encoding="utf-8"?><!--set 标签对应着 AnimatorSet-->
 <set xmlns:android="http://schemas.android.com/apk/res/android"
     android:ordering="together">
     <!--对应着 ObjectAnimator-->
     <objectAnimator
         android:propertyName="x"
         android:repeatCount="infinite"
         android:repeatMode="reverse"
         android:startOffset="10"
         android:valueTo="300"
         android:valueType="floatType" />
     <!--其中propertyName 属性设置为translationX ,valueType 设置为floatType 可以正常启动
        如果 valueType 设置为 intType 将报错,即属性类型必须为 floatType 类型,并且android:propertyName="translationX" 表示移动 300 ,而  android:propertyName="x"表示移动到300 ,是两个不同属性-->
     <!--startOffset 指定延迟多久开始播放动画-->
     <!--valueType 表示指定的 android:propertyName 所指定属性的类型,intType 表示指定属性是整型的,
      如果指定属性为颜色,那么不需要指定 valueType 属性,系统会自动处理
      repeatCount 表示动画循环次数,默认值为0,-1 表示无限循环-->
 
     <!--对应着 ValueAnimator-->
    <animator
     android:duration="300"
     android:valueFrom="0"
     android:valueTo="360"
     android:startOffset="10"
     android:repeatCount="infinite"
     android:repeatMode="reverse"
     android:valueType="intType"/>
 </set>

使用动画:

 AnimatorSet set = (AnimatorSet) AnimatorInflater.loadAnimator(context , R.animator.ani
 m);
 set.setTarget(view);
 set.start();

实际开发中建议使用代码实现属性动画。很多时候一个属性的起始值是无法提前确定的。

理解差值器和估值器
  • 时间插值器( TimeInterpolator) 的作用是根据时间流逝的百分比来计算出当前属性值改变的百分比,系统预置的有LinearInterpolator( 线性插值器:匀速动画) ,AccelerateDecelerateInterpolator( 加速减速插值器:动画两头慢中间快) ,DecelerateInterpolator(减速插值器:动画越来越慢) 。

  • 估值器(类型估值算法, TypeEvaluator) 的作用是根据当前属性改变的百分比来计算改变后的属性值。系统预置有IntEvaluator(针对整型属性) 、FloatEvaluator(浮点型属性) 、ArgbEvaluator(针对 Color 属性)。

如图所示,表示一个匀速动画,采用了线性插值器和整型估值算法,在 40ms 内,View 的 X 属性实现了从 0 到 40 的变化。

属性动画要求对象的该属性有 set 和 get(可选) 方法,插值器和估值算法除了系统提供的外,我们还可以自己定义,插值器或者估值算法都是一个接口,且内部只有一个方法,我们只需要派生一个类实现该接口即可,然后就可以做出千变万化的动画效果了。具体而言是:自定义插值器需要实现 Interpolator 或者 TimeInterpolator ,自定义估值算法需要实现 TypeEvaluator 。如果要对其他类型(非int,float,color)做动画,必须要自定义类型估值算法。

属性动画的监听器

属性动画提供了监听器用于监听动画的播放过程,主要有如下两个接口:AnimatorUpdateListener 和 AnimatorListener 。

public static interface AnimatorListener {
void onAnimationStart(Animator animation); //动画开始
void onAnimationEnd(Animator animation); //动画结束
void onAnimationCancel(Animator animation); //动画取消
void onAnimationRepeat(Animator animation); //动画重复播放
}

为了方便开发,系统提供了AnimatorListenerAdapter类,它是AnimatorListener的适配器类,可以有选择的实现以上4个方法。

public static interface AnimatorUpdateListener {
void onAnimationUpdate(ValueAnimator animation);
}
对任意属性做动画

属性动画原理:属性动画要求动画作用的对象提供 get 方法和 set 方法,属性动画根据外界传递该属性的初始值和最终值以动画的效果去多次调用 set 方法,每次传递给 set 方法的值都不一样,确切的来说是随着时间的推移,所传递的值越来越接近最终值。总结一下,我们对 object 对象属性 abc 做动画,如果想要动画生效,要同时满足两个条件:

  • object 必须要提供 setAbc() 方法,如果动画的时候没有传递初始值,那么还要提供 getAbc() 方法,因为系统要去取 abc 属性的初始值(如果这条不满足,程序直接crash)。
  • object 的 setAbc() 对属性 abc 所做的改变必须能够通过某种方法反应出来(即最终体现了 UI 的变化),比如会带来 UI 的改变之类(如果这条不满足,动画无效果,但是程序不会crash)。

我们给 Button 的 width 属性做动画无效果但是没有crash的原因就是 Button 内部提供了 setWidth 和 getWidth 方法,但是这个 setWidth 方法并不是改变 UI 大小的,而是用来设置最大宽度和最小宽度的。对于上面属性动画的两个条件来说,这个例子只满足了条件 1 而未满足条件 2。

针对上面问题,官方文档给出了 3 种解决方法:

  • 请给你的对象加上get和set方法,如果你有权限的话 对于SDK或者其他第三方类库的类无法加上的。
  • 用一个类来包装原始对象,间接为其提供get和set方法:
 /**
  * 将 Button 沿着 X 轴方向放大
  * @param button
  */
 private void performAnimationByWrapper(View button){
     ViewWrapper viewWrapper=new ViewWrapper(button);
     ObjectAnimator.ofInt(viewWrapper,"width",800)
             .setDuration(5000)
             .start();
 }
  private class ViewWrapper {
         private View targetView;
         public ViewWrapper(View targetView) {
             this.targetView = targetView;
         }
         public int getWidth() {
             //注意调用此函数能得到 View 的宽度的前提是, View 的宽度是精准测量模式,即不可以是 wrap_content
             //否则得不到正确的测量值
             return targetView.getLayoutParams().width;
         }
         public void setWidth(int width) {
           //重写设置目标 view 的布局参数,使其改变大小
             targetView.getLayoutParams().width = width;
           //view 大小改变需要调用重新布局
             targetView.requestLayout();
         }
     }
  • 采用ValueAnimator,监听动画过程,自己实现属性的改变 ValueAnimator 本身不作用于任何对象,也就是说直接使用它没有任何动画效果(所以系统提供了它的子类 ObjectAnimator 供我们直接使用,作用于对象直接执行动画效果,而 ValueAnimator 只是提供改变一个值的过程,并能监听到整个值的改变过程,我们基于这个过程可以自己去实现动画效果,在这个过程中做想要达到的效果,自己去实现)。它可以对一个值做动画,然后我们可以监听其动画过程,在动画过程中修改我们对象的属性,这样我们自己就实现了对对象做了动画。
 //new 一个整型估值器,用于下面比例值计算使用(可以自己去计算,这里直接使用系统的)
 private IntEvaluator intEvaluator = new IntEvaluator();
 private void performAnimatorByValue(final View targetView, final int start, final int end) {
     ValueAnimator valueAnimator = ValueAnimator.ofInt(1, 100);
     valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
         @Override
         public void onAnimationUpdate(ValueAnimator animation) {
             //获取当前动画进度值
             int currentValue = (int) animation.getAnimatedValue();
             //获取当前进度占整个动画比例
             int fraction = (int) animation.getAnimatedFraction();
             //直接通过估值器根据当前比例计算当前 View 的宽度,然后设置给 View
             targetView.getLayoutParams().width = intEvaluator.evaluate(fraction, start, end);
             targetView.requestLayout();
         }
     });
     valueAnimator.setDuration(5000)
             .start();
 }
属性动画的工作原理

属性动画需要运行在有Looper的线程中,系统通过反射调用被作用对象get/set方法。

使用动画的注意事项

  • OOM问题 使用帧动画时,当图片数量较多且图片分辨率较大的时候容易出现OOM,需注意,尽量避免使用帧动画。
  • 内存泄漏 使用无限循环的属性动画时,在Activity退出时即使停止,否则将导致Activity无法释放从而造成内存泄露。
  • 兼容性问题 动画在3.0以下的系统存在兼容性问题,特殊场景可能无法正常工作,需做好适配工作。
  • View动画的问题 View动画是对View的影像做动画,并不是真正的改变了View的状态,因此有时候会出现动画完成后View无法隐藏( setVisibility(View.GONE) 失效) ,这时候调用 view.clearAnimation() 清理View动画即可解决。
  • 不要使用px 使用px会导致不同设备上有不同的效果。
  • 动画元素的交互 View动画是对View的影像做动画,View的真实位置没有变动,动画完成后的新位置是无法触发点击事件的。属性动画是真实改变了View的属性,所以动画完成后的位置可以接受触摸事件。
  • 硬件加速 使用动画的过程中,使用硬件加速可以提高动画的流畅度。