开发艺术之Animation

Android 动画可分为三种:View动画、帧动画和属性动画。帧动画也属于 View 动画的一种,只不过它和平移、旋转等常见的 View 动画在表现形式上不同。

一、View 动画

  • 作用对象:View

  • 支持变换效果:平移、缩放、旋转、透明度变换

1、View 动画的种类

View 动画的四种变换效果对应 Animation 的四个子类:TranslateAnimationScaleAnimationRotateAnimationAlphaAnimation

View 动画的四种变换.png

创建方式:可以通过 XML 定义,也可以通过代码来动态创建,对于 View 动画来说,推荐采用 XML定义动画,因为 XML 格式的动画可读性更好。使用方法如下:

创建动画的 XML 文件,文件路径为:res/anim/view_animation.xml,内容如下:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true"
    android:shareInterpolator="true">

    <translate
        android:fromXDelta="float"
        android:fromYDelta="float"
        android:toXDelta="float"
        android:toYDelta="float" />
    <scale
        android:fromXScale="float"
        android:fromYScale="float"
        android:pivotX="float"
        android:pivotY="float"
        android:toXScale="float"
        android:toYScale="float" />
    <rotate
        android:fromDegrees="float"
        android:pivotX="float"
        android:pivotY="float"
        android:toDegrees="float" />
    <alpha
        android:fromAlpha="float"
        android:toAlpha="float" />

</set>
  • < set >
    表示动画集合,对应 AnimationSet 类,它可以包含若干个动画和动画集合

    • android:interpolator

      插值器它会影响动画的速度,可以不指定,默认为@android:anim/accelerate_decelerate_interpolator

    • android:shareInterpolator

      表示集合中的动画是否和集合共享同一个插值器。如果集合不指定插值器,那么子动画就需要单独指定所需的插值器或者使用默认值

    • android:fillAfter

      表示动画结束以后 View 是否停留在结束位置,true 表示停留在结束为止,false 不停留

  • < translate >

    平移动画,对应 TranslateAnimation 类,它的属性含义如下:

    • android:fromXDelta,表示 x 的起始值,比如0
    • android:toXDelta,表示 x 的结束值,比如100
    • android:fromYDelta,表示 y 的起始值
    • android:toYDelta,表示 y 的结束值
  • < scale >

    缩放动画,对应 ScaleAnimation 类,它的属性含义如下:

    • android:fromXscale,水平方向缩放的起始值,比如0.5
    • android:toXScale,水平方向缩放的结束值,比如1.2
    • android:fromYscale,竖直方向缩放的起始值
    • android:toYScale,竖直方向缩放的结束值
    • android:pivotX,缩放轴点的 x 坐标
    • android:pivotY,缩放轴点的 y 坐标
  • < rotate >

    旋转动画,对应 RotateAnimation 类,它的属性含义如下:

    • android:fromDegrees,旋转开始的角度,比如0
    • android:toDegrees,旋转结束的角度,比如180
    • android:pivotX,旋转轴点的x坐标
    • android:pivotY,旋转轴点的y坐标
  • < alpha >

    透明度动画,对应 AlphaAnimation 类,它的属性含义如下:

    • android:fromAlpha,透明度的起始值,比如 0.1
    • android:toAlpha,透明度的结束值,比如1

在定义完 xml 之后,在代码中应用如下代码,就可以实现 View 动画效果

Button mButton = (Button) findViewById(R.id.button);
Animation animation = AnimationUtils.loadAnimation(this, R.anim.view_animation);
mButton.startAnimation(animation);

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

AlphaAnimation alphaAnimation = new AlphaAnimation(0, 1);
alphaAnimation.setDuration(300);
mButton.startAnimation(alphaAnimation);

最后,可以通过 Animation 的 setAnimationListener 方法给 View 添加过程监听,接口如下所示

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

当上面的四种动画无法满足我们的需求时,可以通过继承 Animation 这个抽象类,然后重写它的 initializeapplyTransformation 方法来自定义 View 动画。

一般开发中很少用到自定义 View 动画

3、帧动画

帧动画是顺序播放一组预先定义好的图片,系统提供了另外一个类 AnimationDrawable 来使用帧动画。

使用方法:通过 XML 定义一个 AnimationDrawable,如下所示:

<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
  android:oneshot="false">
  <item android:drawable="@drawable/image1" android:duration="200"/>
  <item android:drawable="@drawable/image2" android:duration="200"/>
  <item android:drawable="@drawable/image3" android:duration="200"/>
</animation-list>

然后将上述 Drawable 作为 View 的背景通过 Drawable 来播放动画即可:

    Button mButton = (Button) findViewById(R.id.button);
    mButton.setBackgroundResource(R.drawable.frame_drawable);
    AnimationDrawable animationDrawable = (AnimationDrawable) mButton.getBackground();
    animationDrawable.start();

帧动画比较容易引起 OOM,所以在使用帧动画时应尽量避免使用尺寸较大大图片


二、View 动画的特殊使用场景

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

1、LayoutAnimation

作用对象:ViewGroup。比如让 ListView 的每个 item 以一定的动画形式出现

使用步骤:

  • 定义 LayoutAnimation,如下所示:

    // 在 res/anim/ 下创建 anim_layout.xml 文件
    <?xml version="1.0" encoding="utf-8"?>
    <layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
        android:animation="@anim/view_animation"
        android:animationOrder="normal"
        android:delay="0.5">
    </layoutAnimation>
    

    Android:delay,代表动画的延迟。比如子元素入场动画的时间周期是300ms,那么 0.5 就表示每个子元素都需要延迟 150ms 才能播放入场动画。

    Android:animationOrder,代表子元素动画的顺序,有三种选项:normal、reverse 和 random

    Android:animation,为子元素指定具体的入场动画

  • 为子元素指定具体的入场动画,如下所示:

    // 在 res/anim/ 下创建 anim_item.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/linear_interpolator"
        android:shareInterpolator="true">
    
        <translate
            android:fromXDelta="500"
            android:toXDelta="0" />
    
        <alpha
            android:fromAlpha="0"
            android:toAlpha="1" />
    
    </set>
    
  • 为 ViewGroup 指定 android:layoutAnimation 属性。对于 ListView 来说,这样 ListView 的 item 就具有出场动画了,这种方式适用于所有的 ViewGroup,如下所示:

      <ListView
            android:id="@+id/list_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layoutAnimation="@anim/anim_layout" />
    

除了在 XML 中指定 LayoutAnimation 外,还可以通过 LayoutAnimationController 来实现,具体代码如下:

        Animation animation = AnimationUtils.loadAnimation(this, R.anim.anim_item);
        LayoutAnimationController controller = new LayoutAnimationController(animation);
        controller.setDelay(0.5f);
        controller.setOrder(LayoutAnimationController.ORDER_NORMAL);
        mListView.setLayoutAnimation(controller);
2、Activity 的切换效果

Activtiy 有默认切换效果,但是这个效果我们也可以自定义,主要用到 overridePendingTransition(int enterAnim, int exitAnim) 这个方法,此方法需要在 startActivity() 或者 finish() 之后被调用才能生效,它的参数含义如下:

  • enterAnim,Activity 被打开时,所需的动画资源 id
  • exitAnim,Activity 被关闭时,所需的动画资源 id

使用方法如下:

    // 当启动一个 Activity 时
    Intent intent = new Intent(this, Main2Activity.class);
    startActivity(intent);
    overridePendingTransition(R.anim.enter_anim, R.anim.exit_anim);

    // 当 Activity 退出时
    @Override
    public void finish() {
        super.finish();
        overridePendingTransition(R.anim.enter_anim, R.anim.exit_anim);
    }

三、属性动画

属性动画和 View 动画不同,它对作用对象进行了扩展,属性动画可以对任何对象做动画。属性动画中有 ValueAnimator、ObjectAnimator 和 AnimatorSet 等概念。

1、使用属性动画

属性动画默认时间间隔 300ms,默认帧率 10ms/帧。我们可以在一个时间间隔内完成对象从一个属性值到另一个属性值的改变,比如:

  • 改变一个对象的 translationY 属性,让其沿着 Y 轴向上平移一段距离

    ObjectAnimator.ofFloat(mButton, "translationY", -mButton.getHeight()).start();
    
  • 改变一个对象的背景色属性,下面的动画可以让背景色在 3s 内实现从 0xFFFF8080 到 0xFF8080FF 的渐变,动画会无限循环而且具有反转效果

            ValueAnimator colorAnimator = ObjectAnimator.ofInt(mButton, "backgroundColor", 0xFFFF8080, 0xFF8080FF);
            colorAnimator.setDuration(3000);
            colorAnimator.setEvaluator(new ArgbEvaluator()); // 渐变
            colorAnimator.setRepeatCount(ValueAnimator.INFINITE); // 循环
            colorAnimator.setRepeatMode(ValueAnimator.REVERSE); // 反转
            colorAnimator.start();
    
  • 动画集合,5s 内对 View 的旋转、平移、缩放、透明度都进行改变

            AnimatorSet set = new AnimatorSet();
            set.playTogether(
                    ObjectAnimator.ofFloat(mButton, "rotationX", 0, 360),
                    ObjectAnimator.ofFloat(mButton, "rotationY", 0, 180),
                    ObjectAnimator.ofFloat(mButton, "rotation", 0, 90),
                    ObjectAnimator.ofFloat(mButton, "translationX", 0, 90),
                    ObjectAnimator.ofFloat(mButton, "translationY", 0, 90),
                    ObjectAnimator.ofFloat(mButton, "scaleX", 1, 1.5f),
                    ObjectAnimator.ofFloat(mButton, "scaleY", 1, 0.5f),
                    ObjectAnimator.ofFloat(mButton, "alpha", 1, 0.25f, 1)
            );
            set.setDuration(5000).start();
    

属性动画除了通过代码实现外,还可以通过 XML 来定义,属性动画需要定义在 res/animator/ 目录下,语法如下:

<set
  android:ordering=["together" | "sequentially"]>

    <objectAnimator
        android:propertyName="string"
        android:duration="int"
        android:valueFrom="float | int | color"
        android:valueTo="float | int | color"
        android:startOffset="int"
        android:repeatCount="int"
        android:repeatMode=["repeat" | "reverse"]
        android:valueType=["intType" | "floatType"]/>

    <animator
        android:duration="int"
        android:valueFrom="float | int | color"
        android:valueTo="float | int | color"
        android:startOffset="int"
        android:repeatCount="int"
        android:repeatMode=["repeat" | "reverse"]
        android:valueType=["intType" | "floatType"]/>

    <set>
        ...
    </set>
</set>
  • < set >

    对应 AnimatorSet

    • android:ordering,该属性有两个可选值 "together" 和 "sequentially",前者表示动画集合中的子元素同时播放,后者表示动画集合中的子元素按顺序播放。默认属性是"together"。
  • < objectAnimator >

    对应 ObjectAnimator

    • android:propertyName,属性名称
    • android:duration,动画时长
    • android:valueFrom,属性起始值
    • android:valueTo,属性结束值
    • android:startOffset,动画延迟时间
    • android:repeatCount,动画重复次数,默认值为 0,-1 表示无限循环
    • android:repeatMode,动画重复模式,repeat 表示连续重复,reverse 表示逆向重复
    • android:valueType,表示 android:propertyName 所指定的属性的类型,有 "intType" 和 "floatType" 两个可选项
  • < animator >

    对应 ValueAnimator类,只是比 < objectAnimator > 少了 android:propertyName 属性,其他都一样

定义完 XML 后,就可以在代码中使用上面的属性动画了:

        AnimatorSet set = (AnimatorSet) AnimatorInflater.loadAnimator(this, R.animator.property_animator);
        set.setTarget(mButton);
        set.start();

建议:在实际开发中建议采用代码来实现属性动画,因为通过代码实现比较简单,而且很多时候一个属性的起始值是无法提前确定的。比如让一个按钮从屏幕左边移动到右边,由于我们无法提前知道屏幕的宽度,就无法将属性动画定义在 XML 中。

2、理解插值器和估值器
  • 插值器:TimeInterpolator,它的作用是根据时间流逝的百分比来计算出当前属性值改变的百分比,系统预置的有 LinearInterpolator(线性插值器),AccelerateDecelerateInterpolator(加速减速插值器),DecelerateInterpolator(减速插值器)等。

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

3、属性动画的监听器

属性动画提供了监听器,主要有如下两个接口:

public static interface AnimatorListener {
        default void onAnimationStart(Animator animation, boolean isReverse) {
            onAnimationStart(animation);
        }
        default void onAnimationEnd(Animator animation, boolean isReverse) {
            onAnimationEnd(animation);
        }
        void onAnimationStart(Animator animation);
        void onAnimationEnd(Animator animation);
        void onAnimationCancel(Animator animation);
        void onAnimationRepeat(Animator animation);
    }
    public static interface AnimatorUpdateListener {
        // 每播放一帧就会被调用一次
        void onAnimationUpdate(ValueAnimator animation);
    }
4、对任意属性做动画

我们对 object 的属性 abc 作属性动画,如果想要动画生效,要同时满足下面两个条件:

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

来看一个问题:对 Button 的宽度做属性动画:

ObjectAnimator.ofInt(mButton, "width", 500).setDuration(5000).start();

代码运行后发现没效果,这是因为 Button 的 setWidth 方法并不是设置它的宽度:

    @android.view.RemotableViewMethod
    public void setWidth(int pixels) {
        mMaxWidth = mMinWidth = pixels;
        mMaxWidthMode = mMinWidthMode = PIXELS;

        requestLayout();
        invalidate();
    }

这里只设置了最大宽度和最小宽度,对应于 XML 中的 android:width 属性,而真正的宽度对应 android:layout_width。也就是这里只满足了条件 1 而未满足条件 2。

针对上述问题,有 3 种解决方法:

  1. 给你的对象加上 get 和 set 方法,如果你有权限的话

    这个方法最简单,但是往往是可不行的。因为大部分都是 Android SDK 内部实现的。

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

    这是一个很有用的解决方法,用起来很方便,示例代码如下:

     public void onClick(View view) {
            ViewWrapper wrapper = new ViewWrapper(mButton);
            ObjectAnimator.ofInt(wrapper, "width", 500).setDuration(5000).start();
        }
    
        public class ViewWrapper {
            private View mTarget;
    
            public ViewWrapper(View mTarget) {
                this.mTarget = mTarget;
            }
    
            public int getWidth() {
                return mTarget.getLayoutParams().width;
            }
    
            public void setWidth(int width) {
                mTarget.getLayoutParams().width = width;
                mTarget.requestLayout();
            }
        }
    
  3. 采用 ValueAnimator,监听动画过程,自己实现属性的改变

public void onClick(View view) {
    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() {
        private IntEvaluator mEvaluator = new IntEvaluator();

        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            float fraction = animation.getAnimatedFraction();
            target.getLayoutParams().width = mEvaluator.evaluate(fraction, start, end);
            target.requestLayout();
        }
    });
    valueAnimator.setDuration(5000).start();
}

它会在 5000ms 内将一个数从 1 变到 100,然后动画的每一帧都会回调 onAnimationUpdate 方法。


四、使用动画的注意事项

  • OOM 问题

    主要出现在帧动画中,当图片数量较多且图片较大时就极易出现 OOM,尽量避免使用帧动画

  • 内存泄漏

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

  • View 动画的问题

    View 动画是对 View 的影像做动画,并不是真正地改变 View 的状态。因此有时会出现动画完成后 View 无法隐藏的现象,即 setVisibility(View.GONE)失效了,这时只需要调用 view.clearAnimation() 清除 View 动画即可

  • 不要使用 px

    尽量使用 dp,使用 px 会导致在不同设备上有不同的效果

  • 动画元素的交互

    属性动画的单击事件触发位置为移动后的位置,但是 View 动画仍然在原位置

  • 硬件加速

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

推荐阅读:HenCoder Android 自定义 View 1-6:属性动画 Property Animation(上手篇)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342