Android 开发艺术探索读书笔记 7 -- Android 动画深入分析

本篇文章主要介绍以下几个知识点:

  • View 动画
  • View 动画的特殊使用场景
  • 属性动画
  • 使用动画的注意事项
hello,夏天 (图片来源于网络)

Android 的动画可分三种:View 动画、帧动画、属性动画。

7.1 View 动画

View 动画的作用对象是 View,支持4种效果:平移动画、缩放动画、旋转动画、透明度动画。

帧动画也属于 View 动画,但其表现形式不同。

7.1.1 View 动画的种类

View 动画可通过 XML 来定义(可读性好),也可通过代码来动态创建,其4种动画效果如下:

View 动画的四种变换

要使用 View 动画,首先要创建动画的 XML 文件,其路径为:res/anim/filename.xml。其固定语法如下:

<?xml version="1.0" encoding="utf-8"?>

<!-- set标签  动画集合,对应 AnimationSet 类,可包含若干个动画
     interpolator  动画集合采用的插值器,影响动画的速度(可不指定)
     shareInterpolator  是否和集合共享同一个插值器
     duration  持续时间
     fillAfter  动画结束后 View 是否停留在结束位置 -->
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:shareInterpolator="true"
    android:duration="32"
    android:fillAfter="true">

    <!-- alpha标签  透明度动画,对应 AlphaAnimation 类
         fromAlpha  透明度的起始值
         toAlpha    透明度的结束值  -->
    <alpha
        android:fromAlpha="0.1"
        android:toAlpha="1"/>

    <!-- scale标签  缩放动画,对应 ScaleAnimation 类
        fromXScale  水平方向缩放的起始值
        toXScale    水平方向缩放的结束值
        fromYScale  竖直方向缩放的起始值
        toYScale    竖直方向缩放的结束值
        pivotX      缩放的轴点的 x 坐标
        pivotY      缩放的轴点的 y 坐标 -->
    <scale
        android:fromXScale="0.5"
        android:toXScale="1.2"
        android:fromYScale="32"
        android:toYScale="32"
        android:pivotX="32"
        android:pivotY="32"/>

    <!-- translate标签  平移动画,对应 TranslateAnimation 类
         fromXDelta  x 的起始值
         toXDelta    x 的结束值
         fromYDelta  y 的起始值
         toYDelta    y 的结束值 -->
    <translate
        android:fromXDelta="0"
        android:toXDelta="32"
        android:fromYDelta="0"
        android:toYDelta="32"/>

    <!-- rotate标签  旋转动画,对应 RotateAnimation 类
         fromDegrees  旋转开始的角度
         toDegrees    旋转结束的角度
         pivotX       旋转的轴点的 x 坐标
         pivotY       旋转的轴点的 y 坐标  -->
    <rotate
        android:fromDegrees="0"
        android:toDegrees="180"
        android:pivotX="32"
        android:pivotY="32" />

    <set>
        <!-- ... -->
    </set>

</set>

下面举个例子,创建动画xml文件如下:

<!-- res/anim/chapter_07_view_animation_test.xml -->
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:zAdjustment="normal"
    android:fillAfter="true">

    <translate
        android:duration="100"
        android:fromXDelta="0"
        android:fromYDelta="0"
        android:interpolator="@android:anim/linear_interpolator"
        android:toXDelta="100"
        android:toYDelta="100"/>

    <rotate
        android:duration="400"
        android:fromDegrees="0"
        android:toDegrees="90" />

</set>

然后在代码中应用上面的动画如下:

ImageView mImage01 = (ImageView) findViewById(R.id.iv_view_anim_01);
Animation animation = AnimationUtils.loadAnimation(this, R.anim.chapter_07_view_animation_test);
mImage01.startAnimation(animation);

运行效果:

view 的动画效果 xml

除在 XML 中定义动画外,还可以通过代码来应用动画,如下:

 // 将一张图片的透明度在1000ms内由0变1。
 ImageView mImage02 = (ImageView) findViewById(R.id.iv_view_anim_02);
 AlphaAnimation alphaAnimation = new AlphaAnimation(0, 1);
 alphaAnimation.setDuration(1000);
 mImage02.startAnimation(alphaAnimation);

运行效果:

view 的动画效果代码设置

另外,通过 Animation 的 setAnimationListener 可给 View 动画添加过程监听,接口如下:

    animation.setAnimationListener(new Animation.AnimationListener() {
        @Override
        public void onAnimationStart(Animation animation) {

        }

        @Override
        public void onAnimationEnd(Animation animation) {

        }

        @Override
        public void onAnimationRepeat(Animation animation) {

        }
    });

7.1.2 自定义 View 动画

自定义动画需要继承 Animation 这个类,重写它的 initialize (初始化工作)和 applyTransformation (进行相应的矩阵变换)方法即可。(矩阵变换是数学上的概念

这里提供一个来自 Android 的 ApiDemos 中的一个自定义 View 动画的例子:

/**
 * Function:类3D效果:围绕y轴旋转并同时沿着z轴平移
 * Author:Wonderful on 2017/8/21 11:19
 */

public class Rotate3dAnimation extends Animation{

    private final float mFromDegrees;
    private final float mToDegrees;
    private final float mCenterX;
    private final float mCenterY;
    private final float mDepthZ;
    private final boolean mReverse;
    private Camera mCamera;

    public Rotate3dAnimation(float mFromDegrees, float mToDegrees, float mCenterX,
                             float mCenterY, float mDepthZ, boolean mReverse) {
        this.mFromDegrees = mFromDegrees;
        this.mToDegrees = mToDegrees;
        this.mCenterX = mCenterX;
        this.mCenterY = mCenterY;
        this.mDepthZ = mDepthZ;
        this.mReverse = mReverse;
    }

    @Override
    public void initialize(int width, int height, int parentWidth, int parentHeight) {
        super.initialize(width, height, parentWidth, parentHeight);
        mCamera = new Camera();
    }

    @Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        final float fromDegrees = mFromDegrees;
        float degrees = fromDegrees + ((mToDegrees - fromDegrees) * interpolatedTime);

        final float centerX = mCenterX;
        final float centerY = mCenterY;
        final Camera camera = mCamera;

        final Matrix matrix = t.getMatrix();

        camera.save();
        if (mReverse){
            camera.translate(0.0f, 0.0f, mDepthZ * interpolatedTime);
        } else {
            camera.translate(0.0f, 0.0f, mDepthZ * (1.0f - interpolatedTime));
        }
        camera.rotateY(degrees);
        camera.getMatrix(matrix);
        camera.restore();

        matrix.preTranslate(-centerX, -centerY);
        matrix.postTranslate(centerX, centerY);
    }
}

运行效果:

自定义 view 动画效果

7.1.3 帧动画

帧动画是顺序播放一组预先定义好的图片,类似于电影播放,用类 AnimationDrawable 来使用帧动画。

首先通过 XML 定义一个 AnimationDrawable 如下:

<!-- res/drawable/chapter_07_frame_animation.xml -->
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="false">

    <item android:drawable="@drawable/summer_01" android:duration="500" />
    <item android:drawable="@drawable/summer_02" android:duration="500" />
    <item android:drawable="@drawable/summer_03" android:duration="500" />

</animation-list>

然后在代码中应用上面的动画如下:

ImageView mImage04 = (ImageView) findViewById(R.id.iv_frame_anim);
mImage04.setBackgroundResource(R.drawable.chapter_07_frame_animation);
AnimationDrawable drawable = (AnimationDrawable) mImage04.getBackground();
drawable.start();

运行效果:

针动画效果

帧动画使用简单,但容易引起 OOM,应尽量少使用过多尺寸大的图片。

7.2 View 动画的特殊使用场景

View 动画还可以在一些特殊场景使用,如在 ViewGroup 中控制子元素的出场效果,在 Activity 中实现其切换效果。

7.2.1 LayoutAnimation

LayoutAnimation 也是一个 View 动画,作用于 ViewGroup,为 ViewGroup 指定一个动画,控制子元素的出场动画效果。使用步骤如下:

1. 定义 LayoutAnimation

<!-- res/anim/chapter_07_view_animation_layout.xml -->

<!-- delay  子元素开始动画的时间延迟
     animationOrder  子元素的动画顺序
     animation  为子元素指定具体的入场动画-->
<layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
    android:delay="0.5"
    android:animationOrder="normal"
    android:animation="@anim/chapter_07_view_animation_layout_item"/>

2. 为子元素指定具体的入场动画

<!-- res/anim/anim/chapter_07_view_animation_layout_item.xml -->
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="500"
    android:interpolator="@android:anim/accelerate_interpolator"
    android:shareInterpolator="true">

    <alpha
        android:fromAlpha="0.0"
        android:toAlpha="1.0"/>

    <translate
        android:fromXDelta="500"
        android:toXDelta="0"/>

</set>

3. 为 ViewGroup 指定 android:layoutAnimation 属性

    <!-- 指定 layoutAnimation 属性,ListView 的 item 就具有出场动画了 
         这种方式适用于所有的 ViewGroup -->
    <ListView
        android:id="@+id/lv_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layoutAnimation="@anim/chapter_07_view_animation_layout"
        android:background="#fff4f7f9"
        android:cacheColorHint="#00000000"
        android:divider="#dddbdb"
        android:dividerHeight="1.0px"
        android:listSelector="@android:color/transparent"/>

当然也可以通过 LayoutAnimationController 在代码中实现:

// 为 listView 指定入场动画
Animation animation = AnimationUtils.loadAnimation(this, R.anim.chapter_07_view_animation_layout_item);
LayoutAnimationController controller = new LayoutAnimationController(animation);
controller.setDelay(0.5f);
controller.setOrder(LayoutAnimationController.ORDER_NORMAL);
mListView.setLayoutAnimation(controller);

运行效果:

layoutAnimation 效果

7.2.2 Activity 的切换效果

Activity 有默认的切换效果,也可用 overridePendingTransition(int enterAnim, int exitAnim) 自定义切换效果,这个方法必须在 startActivity(Intent)finish() 后调用才能生效,其两参数含义如下:

  • enterAnim
    Acitivity 被打开时所需的动画资源 id;

  • exitAnim
    Activity 被暂停时所需的动画资源 id。

下面举个例子,创建动画xml文件如下:

<!-- res/anim/enter_anim.xml -->
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromYDelta="100%"
    android:toYDelta="0"
    android:duration="1000" />
<!-- res/anim/exit_anim.xml -->
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromYDelta="0"
    android:toYDelta="100%"
    android:duration="1000" />

在代码中使用如下:

// 启动 AnimViewActivity 时
IntentUtils.to(this, AnimViewActivity.class);
overridePendingTransition(R.anim.enter_anim, R.anim.exit_anim);
@Override
public void finish() {
   super.finish();
   // 退出 AnimViewActivity 时
   overridePendingTransition(R.anim.enter_anim, R.anim.exit_anim);
}

运行效果:

Activity 切换效果

Fragment 可通过 FragmentTransaction 中的 setCustomAnimations() 方法来添加切换动画。

7.3 属性动画

属性动画是 API 11 新加入的特性,对作用对象进行了扩展,可对任何对象做动画。在 API 11 之前的系统上使用属性动画,可采用开源动画库 nineoldandroids

7.3.1 使用属性动画

下面先简单介绍几个例子看看如何使用属性动画。

(1)

// 改变一个对象的 translationY 属性,让其沿着 Y 轴向上平移一段距离
ObjectAnimator.ofFloat(ivObject01, "translationY", -ivObject01.getHeight()).start();

(2)

 // 改变一个对象的背景色属性
ValueAnimator colorAnim = ObjectAnimator.ofInt(tvObject02, "backgroundColor",0xFFFF8080, 0xFF8080FF);
colorAnim.setDuration(3000);
colorAnim.setEvaluator(new ArgbEvaluator());
colorAnim.setRepeatCount(ValueAnimator.INFINITE);
colorAnim.setRepeatMode(ValueAnimator.REVERSE);
colorAnim.start();

(3)

// 动画集合,5 秒内对 View 的旋转、平移、缩放、透明度改变
AnimatorSet set = new AnimatorSet();
set.playTogether(
        ObjectAnimator.ofFloat(ivObject03, "rotationX", 0, 360),
        ObjectAnimator.ofFloat(ivObject03, "rotationY", 0, 180),
        ObjectAnimator.ofFloat(ivObject03, "rotation", 0, -90),
        ObjectAnimator.ofFloat(ivObject03, "translationX", 0, 90),
        ObjectAnimator.ofFloat(ivObject03, "translationY", 0, 90),
        ObjectAnimator.ofFloat(ivObject03, "scaleX", 1, 1.5f),
        ObjectAnimator.ofFloat(ivObject03, "scaleY", 1, 0.5f),
        ObjectAnimator.ofFloat(ivObject03, "alpha", 1, 0.25f, 1)
);
set.setDuration(5 * 1000).start();

运行效果:

属性动画例子效果

属性动画也可以通过 XML 来定义(res/animator 目录下),其语法如下:

<?xml version="1.0" encoding="utf-8"?>

<!-- set标签  动画集合,对应 AnimationSet 类,可包含若干个动画
     ordering  1. together 同时播放(默认) 2. sequentially 依次播放 -->
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:ordering="together">

    <!-- objectAnimator标签  对应 ObjectAnimator 类
         propertyName  作用对象的属性的名称(若指定颜色则不需要指定属性valueType)
         duration      动画的时长
         valueFrom     属性的起始值
         valueTo       属性的结束值
         startOffset   动画的延迟时间
         repeatCount   动画的重复次数(默认0,-1 表无限循环)
         repeatMode    动画的重复模式
         valueType     propertyName 所指定的属性的类型
                       1. intType 整型 2.floatType 浮点型-->
    <objectAnimator
        android:propertyName="string"
        android:duration="1"
        android:valueFrom="float|int|color"
        android:valueTo="float|int|color"
        android:startOffset="1"
        android:repeatCount="1"
        android:repeatMode="restart"
        android:valueType="intType"/>

    <!-- animator 标签  对应 ValueAnimator 类
        其属性含义和 objectAnimator 的一样 -->
    <animator
        android:duration="1"
        android:valueFrom="float|int|color"
        android:valueTo="float|int|color"
        android:startOffset="1"
        android:repeatCount="1"
        android:repeatMode="restart"
        android:valueType="intType"/>

    <set>
        <!-- ... -->
    </set>

</set>

下面举个例子,定义属性动画如下:

<!-- res/animator/property_animator.xml -->
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:ordering="together">

    <objectAnimator
        android:propertyName="x"
        android:duration="300"
        android:valueTo="200"
        android:valueType="intType"/>

    <objectAnimator
        android:propertyName="y"
        android:duration="300"
        android:valueTo="200"
        android:valueType="intType"/>

</set>

然后在代码中应用上面的动画如下:

AnimatorSet animatorSet = (AnimatorSet) AnimatorInflater.loadAnimator(this, R.animator.property_animator);
animatorSet.setTarget(ivObject04);
animatorSet.start();

在实际开发中建议采用代码来实现属性动画。

7.3.2 理解插值器和估值器

TimeInterpolator,时间插值器,其作用是根据时间流逝的百分比来计算出当前属性值改变的百分比,系统预置的有:

  • LinearInterpolator
    线性插值器:匀速动画

  • AccelerateDecelerateInterpolator
    加速减速插值器:动画两头慢中间快

  • DecelerateInterpolator
    减速插值器:动画越来越慢

TypeEvaluator,类型估值算法,即估值器,其作用是根据当前属性改变的百分比来计算改变后的属性值,系统预置的有:

  • IntEvaluator 针对整形属性
  • FloatEvaluator 浮点型
  • ArgbEvaluator Color

例子,一个匀速动画,采用线性插值器和整形估值算法,在 40ms内,View 的 x 属性实现从0到40的转变:

插值器的工作原理

自定义插值器需实现 InterpolatorTimeInterpolator,自定义估值算法需实现 TypeEvaluator

7.3.3 属性动画的监听器

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

  • AnimatorListener
    public static interface AnimatorListener {
        /**
         * 动画开始
         */
        void onAnimationStart(Animator animation);

        /**
         * 动画结束
         */
        void onAnimationEnd(Animator animation);

        /**
         * 动画取消
         */
        void onAnimationCancel(Animator animation);

        /**
         * 动画重复播放
         */
        void onAnimationRepeat(Animator animation);
    }

针对上述方法,系统还提供了类 AnimatorListenerAdapter,用于有选择性的实现上面的4个方法。

  • AnimatorUpdateListener
    public static interface AnimatorUpdateListener {
        /**
         * 监听整个动画过程,每播放一帧,此方法调用一次
         */
        void onAnimationUpdate(ValueAnimator animation);

    }

7.3.4 对任意属性做动画

情景:实现让Button的宽度从当前宽度增加到500px的动画。用属性动画如下(View 动画不支持对宽度进行动画):

@Override
public void onClick(View v){
    if(v == mButton){
        ObjectAnimator.ofInt(mButton, "width", 500).setDuration(5000).start();
    }
}

运行后发现没效果(下面解释)。

对 object 的属性 O 做动画,让动画生效需同时满足两个条件:

(1)object 必须提供 setO 方法,若没传递初始值,还需提供 getO 方法。(不满足,程序直接 Crash)

(2)object 的 setO 对属性 O 所做的改变必须能通过某种方法反映除了,如会改变 UI 之类的。(不满足,动画无效果但不会 Crash)

Button 继承 TextView 有 setWidth 方法,但 TextView 和 Button 的 setWidthgetWidth 做的不是同一件事情,无法通过 setWidth 改变控件宽度。

因此上面情景中满足了条件1而未满足条件2,动画不生效。

针对上面问题,有如下3中解决方法:

1. 给你的对象加上 getset 方法,如果你有权限的话。

2. 用一个类来包装原始对象,间接为其提供 getset 方法。

具体代码如下:

    @Override
    public void onClick(View v){
        if(v == mButton){
             // 先将 Button 包装
             ViewWrapper wrapper = new ViewWrapper(mButton);
             ObjectAnimator.ofInt(wrapper , "width", 500).setDuration(5000).start();
        }
    }

   /**
     * 此类用于包装 View,间接为 View 提供 get 和 set 方法
     */
    private static class ViewWrapper{
        private View mTarget;

        public ViewWrapper(View mTarget) {
            this.mTarget = mTarget;
        }

        public int getWidth(){
            return mTarget.getLayoutParams().width;
        }

        public void setWidth(int width){
            // 修改 target 的宽度
            mTarget.getLayoutParams().width = width;
            mTarget.requestLayout();
        }
    }

运行效果:

按钮变宽动画效果

3. 采用 ValueAnimator,监听动画过程,自己实现属性的改变。

ValueAnimator 本身不作用于任何对象,即直接使用它无动画效果。它可对一个值做动画,监听其动画过程,修改对象的属性值,从而达到对象的动画效果。具体代码如下:

    @Override
    public void onClick(View v){
        if(v == mButton){
             performAnimate(mButton, mButton.getWidth(), 500);
        }
    }

    private void performAnimate(final View target, final int start, final int end){
        ValueAnimator valueAnimator = ValueAnimator.ofInt(1, 100);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            // 持有一个 IntEvaluator 对象,方便下面估值时使用
            private IntEvaluator mEvaluator = new IntEvaluator();

            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                // 获得当前动画的进度值,整形,1-100之间
                int currentValue = (int) animation.getAnimatedValue();
                // 获得当前进度占整个动画过程的比例,浮点型,0-1之间
                float fraction = animation.getAnimatedFraction();
                // 直接调用整型估值器,通过比例计算出宽度,然后再设给 Button
                target.getLayoutParams().width = mEvaluator.evaluate(fraction, start, end);
                target.requestLayout();
            }
        });
        valueAnimator.setDuration(5000).start();
    }

运行效果和上面一样。

7.3.5 属性动画的工作原理

属性动画要求动画作用的对象提供该属性的 set 方法,属性动画根据传递该属性的初始值和最终值,以动画的效果多次去调用 set 方法。

每次传递给 set 方法的值都不一样,确切来时是随着时间的推移,所传递的值越来越接近最终值。

若动画时没传递初始值,则还要提供 get 方法,因为系统要去获取属性的初始值。

7.4 使用动画的注意事项

1. OOM 问题

在帧动画中使用较多的大图片时容易出现 OOM,尽量避免使用帧动画。

2. 内存泄漏

在 Activity 退出时及时停止属性动画中的无限循环动画。

3. 兼容性问题

动画在 3.0 以下系统有兼容性问题,要做好适配工作。

4. View 动画的问题

View 动画并不是真正改变 View 的状态,有时完成动画后无法隐藏 View,即 setVisibility(View.GONE) 无效,此时只要调用 view.clearAnimation() 清除即可。

5. 不要使用 px

执行动画过程中尽量使用 dp,避免在不同设备上出现不同的效果。

6. 动画元素的交互

将 view 移动后,在 3.0 以下系统上,不管是 View 动画还是属性动画,新位置均无法触发点击事件,但老位置仍然可以触发点击事件。

7. 硬件加速

使用动画过程中,建议开启硬件加速,提高流畅性。

本篇文章就介绍到这。

推荐阅读更多精彩内容