Android学习手记之动画

Android框架提供了两种类型的动画:View Animation(也称视图动画)和Property Animation (也称属性动画),Property Animation只能在Android 3.0即以上版本才能使用。View Animation则不受版本限制,但是优先考虑使用Property Animation 。除此之外可以利用Drawable Animation(也称帧动画)实现一帧一帧地显示资源文件(多为图片)来展示动画。

1、View Animation

1.1、View Animation分类

View Animation通过在视图上面执行补间动画(tweened animation)生成动画效果,补间动画变换可以变换视图对象的位置、大小、旋转角度、透明度。Android的animation package提供了使用补间动画所有的类。目前View动画提供了平移动画、缩放动画、旋转动画、透明度动画。各自对应的类和相关标签如下表所示:

名 称 标 签 子 类 效 果
平移动画 <translate> TranslateAnimation 移动View
缩放动画 <scale> ScaleAnimation 缩放View
旋转动画 <rotate> RotateAnimation 旋转View
透明度动画 alpha AlphaAnimation 改变View的透明度

既可以在代码中定义动画,也可以在XML中配置,但优先采用在XML配置的方式,因为复用性、可读性更高。既可以顺序播放动画,也可同时播放动画。如果采用XML配置的方式,需要将XML文件保存到Android项目的res/anim/目录下面,XML只能有一个根元素,XML只能为单个的<alpha><scale><translate><rotate>、插值元素或者组织多个动画的<set>标签(内部还可以嵌套其他<set>标签).下面是XML中配置示例,具体语法说明参见Animation Resources.:

<set android:shareInterpolator="false">
    <scale
        android:interpolator="@android:anim/accelerate_decelerate_interpolator"
        android:fromXScale="1.0"
        android:toXScale="1.4"
        android:fromYScale="1.0"
        android:toYScale="0.6"
        android:pivotX="50%"
        android:pivotY="50%"
        android:fillAfter="false"
        android:duration="700" />
    <set android:interpolator="@android:anim/decelerate_interpolator">
        <scale
           android:fromXScale="1.4"
           android:toXScale="0.0"
           android:fromYScale="0.6"
           android:toYScale="0.0"
           android:pivotX="50%"
           android:pivotY="50%"
           android:startOffset="700"
           android:duration="400"
           android:fillBefore="false" />
        <rotate
           android:fromDegrees="0"
           android:toDegrees="-45"
           android:toYScale="0.0"
           android:pivotX="50%"
           android:pivotY="50%"
           android:startOffset="700"
           android:duration="400" />
    </set>
</set>

假设文件在res/anim/目录下保存为hyperspace_jump.xml,如果要想将其用在一个ImageView对象上面,下面是代码示例:

   ImageView spaceshipImage = (ImageView) findViewById(R.id.spaceshipImage);
   Animation hyperspaceJumpAnimation = AnimationUtils.loadAnimation(this, R.anim.hyperspace_jump);
   spaceshipImage.startAnimation(hyperspaceJumpAnimation);

如果是用硬编码的方式创建动画,如下所示:

  ImageView spaceshipImage = (ImageView) findViewById(R.id.spaceshipImage);
  AlphaAnimation animation=new AlphaAnimation(0,1);
  animation.setDuration(300);
   spaceshipImage.startAnimation(animation);

除了使用startAnimation()启动动画外,你也可以用Animation.setStartTime()设置动画的启动时间,然后用View.setAnimation()将动画注册到View上面。
注意:.不管动画如何移动或者调整,View的边界域不会自动地调整以便将动画包含在View边界域内。即便如此,当动画超出了View的边界域时仍然会绘制而不会发生剪切,但是如果超出了容纳该View的父容器View的边界域时候就会出现剪的现象

1.2、自定义View Animation

自定义动画要继承Animation,然后重写它的[initialize](http://developer.android.com/reference/android/view/animation/Animation.html#initialize(int, int, int, int))和[applyTransformation](http://developer.android.com/reference/android/view/animation/Animation.html#applyTransformation(float, android.view.animation.Transformation))方法。在initialize中做一些初始化工作,比如Camera,然后在[applyTransformation](http://developer.android.com/reference/android/view/animation/Animation.html#applyTransformation(float, android.view.animation.Transformation))方法中获取变换矩阵做相应的变换。参考ApiDemos中Rotate3dAnimation示例。

2、Property Animation

属性动画的强大之处在于它可以给所有的对象而不仅仅是View对象设置动画。属性动画通过在特定的时间段内改变对象的某个属性(需要为所添加动画的对象的该属性提供setter方法,getter方法则是可选),可以设置动画的持续时间(Duration)、动画在持续时间段内的属性改变规则(Time interpolation时间插值)、动画的重复次数、动画结束之后是否倒放、动画组合(顺序播放一组动画或者同时播放一组动画)、帧刷新的频率(默认每隔10ms刷新一次帧,设置刷新频率不一定就会按照所指定的频率来,取决于当前系统负载的任务数目、底层计时器有多快等)

2.1、属性动画的工作原理

属性动画根据动画流逝时间的百分比来计算出当前的属性值的改变的百分比,然后根据计算出来的属性值改变百分比、初值和终值来计算当前的当前时间点的属性值(调用所添加的对象的某个属性的set方法)。

如何计算动画

如上图所示,ValueAnimator通过封装定义动画插值的TimeInterpolator以及定义如何计算添加了动画的属性的值TypeEvaluator

线性动画示例图

要想启动一个动画,需要创建ValueAnimator,并设置初值、终值、动画的持续时间。调用start()方法启动。在整个动画的执行过程中,ValueAnimator会计算时间流逝率( elapsed fraction ),也就是动画已经消耗的时间所占总持续时间的百分比,0表示0%,1表示100%。以上面的线性动画示例图为例,当 t = 10 ms时,因为总时间为40ms,流逝率为fraction=(10-40)/40=0.25。当其计算出时间流逝率fraction后,会将该它传入当前的ValueAnimator所设置的TimeInterpolator(默认是LinearInterpolator)的getInterpolation方法,该方法只需一个参数,对传入的fraction通过某种算法生成一个插值率(interpolated fraction),不同的TimeInterpolator会有不同的实现,因此得到的插值率会不一样,对于LinearInterpolator,得到的插值率就是传入的流逝率,所以所以属性的变化会随着时间的变化是线性关系而,对于下面的图,采用的是AccelerateDecelerateInterpolator,也就是先加速后减速的时间插值,在 t = 10 ms时虽然时间流失率为 0.25,但是AccelerateDecelerateInterpolatorgetInterpolation方法实现如下所示:

public float getInterpolation(float fraction) {
        return (float)(Math.cos((fraction+ 1) * Math.PI) / 2.0f) + 0.5f;
    }

fraction = 0.25,返回的值约为0.1464,显然小于传入的0.25,最后将getInterpolation方法的返回值、初值、终值带入相应的TypeEvaluator的[evaluate](http://developer.android.com/reference/android/animation/TypeEvaluator.html#evaluate(float, T, T))方法得到此时的属性值,例如IntEvaluator的[evaluate](http://developer.android.com/reference/android/animation/TypeEvaluator.html#evaluate(float, T, T))的实现为:

   public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
        int startInt = startValue;
        return (int)(startInt + fraction * (endValue - startInt));
    }

因此当 t = 10 ms时,对于线性动画,X=(int)(0+0.25x(40-0))=10,对于非线性动画X=(int)(0+0.1464x(40-0))=6,如果是ObjectAnimator,还会在添加动画的对象上通过反射调用修改属性的set方法。

非线性动画示例图

2.2、Property Animation与View Animation的不同之处

Property Animation不仅仅可以对view对象添加动画,还能对非view对象添加动画,而View Animation如果要对非View对象添加动画,需要自己实现比较麻烦,并且view Animation所能操纵的属性也很少只能对view的缩放、旋转、平移设置动画,比如不能修改背景色。view animation只是修改了view所绘制的地方,但并没有修改view本身,比如对一个按钮添加平移动画,按钮仍然处在原来的地方,Property Animation则消除了这些限制。可以对view和非View对象添加动画,并且在动画播放过程中修改自身。提供了插值(参见2.9)、多动画功能(参见2.5)。

2.3、使用ValueAnimator设置动画

ValueAnimator提供了ofInt()ofFloat()和 [ofObject()](https://developer.android.com/reference/android/animation/ValueAnimator.html#ofObject(android.animation.TypeEvaluator, java.lang.Object...))三中工厂方法来创建对象,ofInt()ofFloat()方法类似,前者传入整数序列,后者传入浮点数序列,而 [ofObject()](https://developer.android.com/reference/android/animation/ValueAnimator.html#ofObject(android.animation.TypeEvaluator, java.lang.Object...))是用于自定义类型。
ofFloat()方法示例:

ValueAnimator animation = ValueAnimator.ofFloat(0f, 1f);
animation.setDuration(1000);
animation.start();

[ofObject()](https://developer.android.com/reference/android/animation/ValueAnimator.html#ofObject(android.animation.TypeEvaluator, java.lang.Object...))方法示例,其中MyTypeEvaluator需要实现 TypeEvaluator,参见2.8:

ValueAnimator animation = ValueAnimator.ofObject(new MyTypeEvaluator(), startPropertyValue, endPropertyValue);
animation.setDuration(1000);
animation.start();

由于上面两处示例代码并没有作用到某个对象上,因此该动画并没有效果,要想动画作用到某对象上上,需要添加动画监听器,调用getAnimatedValue()方法获取刷新到某个特定帧时计算所得属性值,然后做相应的逻辑处理。

2.4、使用ObjectAnimator设置动画

ObjectAnimatorValueAnimator的子类,它内部封装了计时引擎和ValueAnimator的值计算,可以自动更新设置动画的某个属性而无需实现ValueAnimator.AnimatorUpdateListeneronAnimationUpdate()方法获取每一帧的属性值来手动刷新,参见2.6节,从而让代码更加简洁,如下所示:

ObjectAnimator anim = ObjectAnimator.ofFloat(foo, "alpha", 0f, 1f);
anim.setDuration(1000);
anim.start();

注意

  • ObjectAnimator能够自动更新属性值,实现原理最终是通过反射调用在某个属性上的setter方法。因此当对此某个对象某个属性使用ObjectAnimator设置动画的时候一定要确保该属性有setter(setXxx()格式)方法。如果没有对应的setter方法,如果有权限,添加相关属性的setter方法,如果没有添加该属性setter方法的权限,则使用一个包装类,或者改用ValueAnimator
  • 如果在调用ObjectAnimator的工厂方法(比如ofFloat())方法在参数values...上只传入了一个值,则只会将该值作为终值,因此在设置动画的某个属性需要有getter方法(getXxx()格式),因为需要通过getter方法获取初值。
  • 属性的setter和getter方法操作的属性值必须是同一数据类型作为传入 ObjectAnimatorvalues...参数的初值和终值,比如当采用的是
ObjectAnimator.ofFloat(targetObject, "propName", 1f)

则属性的setter和getter参数必须是float类型.

  • 有些属性需要手动调用invalidate()刷新值。比如Drawable对象的颜色值。参见2.6

2.5、使用AnimatorSet同时设置多种动画

使用AnimatorSet类可以实现同时播放、顺序播放、延时播放多个动画。下面的代码是Android SDK中模拟小球落体压扁后有反弹起来一段距离,然后消失的动画,先播放bounceAnim,然后同时播放squashAnim1squashAnim2stretchAnim1stretchAnim2。接着播放bounceBackAnim,最后播放fadeAnim。所播放的动画均为ValueAnimator类型

AnimatorSet bouncer = new AnimatorSet();
bouncer.play(bounceAnim).before(squashAnim1);
bouncer.play(squashAnim1).with(squashAnim2);
bouncer.play(squashAnim1).with(stretchAnim1);
bouncer.play(squashAnim1).with(stretchAnim2);
bouncer.play(bounceBackAnim).after(stretchAnim2);
ValueAnimator fadeAnim = ObjectAnimator.ofFloat(newBall, "alpha", 1f, 0f);
fadeAnim.setDuration(250);
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.play(bouncer).before(fadeAnim);
animatorSet.start();

2.6、设置动画监听器

Android提供了监听动画播放事件的接口Animator.AnimatorListenerValueAnimator.AnimatorUpdateListener。下面是各自的源代码:
Animator.AnimatorListener源码:

/**
 * This is the superclass for classes which provide basic support for animations which can be
 * started, ended, and have <code>AnimatorListeners</code> added to them.
 */
public abstract class Animator implements Cloneable {
    ......
    /**
     * <p>An animation listener receives notifications from an animation.
     * Notifications indicate animation related events, such as the end or the
     * repetition of the animation.</p>
     */
    public static interface AnimatorListener {
        /**
         * <p>Notifies the start of the animation.</p>
         *
         * @param animation The started animation.
         */
        void onAnimationStart(Animator animation);

        /**
         * <p>Notifies the end of the animation. This callback is not invoked
         * for animations with repeat count set to INFINITE.</p>
         *
         * @param animation The animation which reached its end.
         */
        void onAnimationEnd(Animator animation);

        /**
         * <p>Notifies the cancellation of the animation. This callback is not invoked
         * for animations with repeat count set to INFINITE.</p>
         *
         * @param animation The animation which was canceled.
         */
        void onAnimationCancel(Animator animation);

        /**
         * <p>Notifies the repetition of the animation.</p>
         *
         * @param animation The animation which was repeated.
         */
        void onAnimationRepeat(Animator animation);
    }
   ......
}

ValueAnimator.AnimatorUpdateListener源码

public class ValueAnimator extends Animator {
   ......
    /**
     * Implementors of this interface can add themselves as update listeners
     * to an <code>ValueAnimator</code> instance to receive callbacks on every animation
     * frame, after the current frame's values have been calculated for that
     * <code>ValueAnimator</code>.
     */
    public static interface AnimatorUpdateListener {
        /**
         * <p>Notifies the occurrence of another frame of the animation.</p>
         *
         * @param animation The animation which was repeated.
         */
        void onAnimationUpdate(ValueAnimator animation);

    }
   ......
}

需要说明的是,对于某些设置动画的属性,需要手动调用invalidate()方法才能确保将屏幕区域用动画得到的新的属性值重新绘制自己。比如一个Drawable对象的颜色属性,然而对于setAlpha()setTranslationX()则不需要手动调用invalidate()
监听示例代码:

ValueAnimatorAnimator fadeAnim = ObjectAnimator.ofFloat(newBall, "alpha", 1f, 0f);
fadeAnim.setDuration(250);
fadeAnim.addListener(new AnimatorListenerAdapter() {
public void onAnimationEnd(Animator animation) {
    balls.remove(((ObjectAnimator)animation).getTarget());
}

2.7、给ViewGroup设置布局变换时动画

拼命写代码
拼命写代码

Android属性动画提供了给ViewGroups布局改变时添加动画的功能。所谓布局改变动画是指在ViewGroup里的View,当你添加或者删除它、或者调用它的setVisibility()方法将对应的值设为VISIBLEGONEINVISIBLE时候所呈现出的出场动画、退场动画效果,当对某个ViewGroup内部的view做添加或者删除的时候,还可以给在ViewGroup中的其他view从原来的位置到新的位置添加动画。
用法:创建一个LayoutTransition对象,然后调用该对象的[setAnimator](https://developer.android.com/reference/android/animation/LayoutTransition.html#setAnimator(int, android.animation.Animator))(int transitionType, Animator animator),然后在需要添加ViewGroup布局改变动画的某个ViewGroup对象上调用setLayoutTransition(LayoutTransition transition)方法。
其中[setAnimator](https://developer.android.com/reference/android/animation/LayoutTransition.html#setAnimator(int, android.animation.Animator))(int transitionType, Animator animator)方法的第一个参数只能为LayoutTransition类中下面四个静态常量。

  • APPEARING 某个View在ViewGroup中出现时的设置动画
  • CHANGE_APPEARING 当新的View出现在ViewGroup中出现时给已经存在于ViewGroup中的其他View设置动画
  • DISAPPEARING 某个View在ViewGroup中消失时的设置动画
  • CHANGE_DISAPPEARING 当某个View从ViewGroup中消失时给ViewGroup中的其他View设置动画。

代码示例如下:


/**
 * This application demonstrates how to use LayoutTransition to automate transition animations
 * as items are removed from or added to a container.
 */
public class LayoutAnimations extends Activity {

    private int numButtons = 1;
    ViewGroup container = null;
    Animator defaultAppearingAnim, defaultDisappearingAnim;
    Animator defaultChangingAppearingAnim, defaultChangingDisappearingAnim;
    Animator customAppearingAnim, customDisappearingAnim;
    Animator customChangingAppearingAnim, customChangingDisappearingAnim;
    Animator currentAppearingAnim, currentDisappearingAnim;
    Animator currentChangingAppearingAnim, currentChangingDisappearingAnim;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.layout_animations);

        container = new FixedGridLayout(this);
        container.setClipChildren(false);
        ((FixedGridLayout)container).setCellHeight(90);
        ((FixedGridLayout)container).setCellWidth(100);
        final LayoutTransition transitioner = new LayoutTransition();
        container.setLayoutTransition(transitioner);
        defaultAppearingAnim = transitioner.getAnimator(LayoutTransition.APPEARING);
        defaultDisappearingAnim =
                transitioner.getAnimator(LayoutTransition.DISAPPEARING);
        defaultChangingAppearingAnim =
                transitioner.getAnimator(LayoutTransition.CHANGE_APPEARING);
        defaultChangingDisappearingAnim =
                transitioner.getAnimator(LayoutTransition.CHANGE_DISAPPEARING);
        createCustomAnimations(transitioner);
        currentAppearingAnim = defaultAppearingAnim;
        currentDisappearingAnim = defaultDisappearingAnim;
        currentChangingAppearingAnim = defaultChangingAppearingAnim;
        currentChangingDisappearingAnim = defaultChangingDisappearingAnim;

        ViewGroup parent = (ViewGroup) findViewById(R.id.parent);
        parent.addView(container);
        parent.setClipChildren(false);
        Button addButton = (Button) findViewById(R.id.addNewButton);
        addButton.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
                Button newButton = new Button(LayoutAnimations.this);
                newButton.setText(String.valueOf(numButtons++));
                newButton.setOnClickListener(new View.OnClickListener() {
                    public void onClick(View v) {
                        container.removeView(v);
                    }
                });
                container.addView(newButton, Math.min(1, container.getChildCount()));
            }
        });

        CheckBox customAnimCB = (CheckBox) findViewById(R.id.customAnimCB);
        customAnimCB.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                setupTransition(transitioner);
            }
        });

        // Check for disabled animations
        CheckBox appearingCB = (CheckBox) findViewById(R.id.appearingCB);
        appearingCB.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                setupTransition(transitioner);
            }
        });
        CheckBox disappearingCB = (CheckBox) findViewById(R.id.disappearingCB);
        disappearingCB.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                setupTransition(transitioner);
            }
        });
        CheckBox changingAppearingCB = (CheckBox) findViewById(R.id.changingAppearingCB);
        changingAppearingCB.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                setupTransition(transitioner);
            }
        });
        CheckBox changingDisappearingCB = (CheckBox) findViewById(R.id.changingDisappearingCB);
        changingDisappearingCB.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                setupTransition(transitioner);
            }
        });
    }

    private void setupTransition(LayoutTransition transition) {
        CheckBox customAnimCB = (CheckBox) findViewById(R.id.customAnimCB);
        CheckBox appearingCB = (CheckBox) findViewById(R.id.appearingCB);
        CheckBox disappearingCB = (CheckBox) findViewById(R.id.disappearingCB);
        CheckBox changingAppearingCB = (CheckBox) findViewById(R.id.changingAppearingCB);
        CheckBox changingDisappearingCB = (CheckBox) findViewById(R.id.changingDisappearingCB);
        transition.setAnimator(LayoutTransition.APPEARING, appearingCB.isChecked() ?
                (customAnimCB.isChecked() ? customAppearingAnim : defaultAppearingAnim) : null);
        transition.setAnimator(LayoutTransition.DISAPPEARING, disappearingCB.isChecked() ?
                (customAnimCB.isChecked() ? customDisappearingAnim : defaultDisappearingAnim) : null);
        transition.setAnimator(LayoutTransition.CHANGE_APPEARING, changingAppearingCB.isChecked() ?
                (customAnimCB.isChecked() ? customChangingAppearingAnim :
                        defaultChangingAppearingAnim) : null);
        transition.setAnimator(LayoutTransition.CHANGE_DISAPPEARING,
                changingDisappearingCB.isChecked() ?
                (customAnimCB.isChecked() ? customChangingDisappearingAnim :
                        defaultChangingDisappearingAnim) : null);
    }

    private void createCustomAnimations(LayoutTransition transition) {
        // Changing while Adding
        PropertyValuesHolder pvhLeft =
                PropertyValuesHolder.ofInt("left", 0, 1);
        PropertyValuesHolder pvhTop =
                PropertyValuesHolder.ofInt("top", 0, 1);
        PropertyValuesHolder pvhRight =
                PropertyValuesHolder.ofInt("right", 0, 1);
        PropertyValuesHolder pvhBottom =
                PropertyValuesHolder.ofInt("bottom", 0, 1);
        PropertyValuesHolder pvhScaleX =
                PropertyValuesHolder.ofFloat("scaleX", 1f, 0f, 1f);
        PropertyValuesHolder pvhScaleY =
                PropertyValuesHolder.ofFloat("scaleY", 1f, 0f, 1f);
        customChangingAppearingAnim = ObjectAnimator.ofPropertyValuesHolder(
                        this, pvhLeft, pvhTop, pvhRight, pvhBottom, pvhScaleX, pvhScaleY).
                setDuration(transition.getDuration(LayoutTransition.CHANGE_APPEARING));
        customChangingAppearingAnim.addListener(new AnimatorListenerAdapter() {
            public void onAnimationEnd(Animator anim) {
                View view = (View) ((ObjectAnimator) anim).getTarget();
                view.setScaleX(1f);
                view.setScaleY(1f);
            }
        });

        // Changing while Removing
        Keyframe kf0 = Keyframe.ofFloat(0f, 0f);
        Keyframe kf1 = Keyframe.ofFloat(.9999f, 360f);
        Keyframe kf2 = Keyframe.ofFloat(1f, 0f);
        PropertyValuesHolder pvhRotation =
                PropertyValuesHolder.ofKeyframe("rotation", kf0, kf1, kf2);
        customChangingDisappearingAnim = ObjectAnimator.ofPropertyValuesHolder(
                        this, pvhLeft, pvhTop, pvhRight, pvhBottom, pvhRotation).
                setDuration(transition.getDuration(LayoutTransition.CHANGE_DISAPPEARING));
        customChangingDisappearingAnim.addListener(new AnimatorListenerAdapter() {
            public void onAnimationEnd(Animator anim) {
                View view = (View) ((ObjectAnimator) anim).getTarget();
                view.setRotation(0f);
            }
        });

        // Adding
        customAppearingAnim = ObjectAnimator.ofFloat(null, "rotationY", 90f, 0f).
                setDuration(transition.getDuration(LayoutTransition.APPEARING));
        customAppearingAnim.addListener(new AnimatorListenerAdapter() {
            public void onAnimationEnd(Animator anim) {
                View view = (View) ((ObjectAnimator) anim).getTarget();
                view.setRotationY(0f);
            }
        });

        // Removing
        customDisappearingAnim = ObjectAnimator.ofFloat(null, "rotationX", 0f, 90f).
                setDuration(transition.getDuration(LayoutTransition.DISAPPEARING));
        customDisappearingAnim.addListener(new AnimatorListenerAdapter() {
            public void onAnimationEnd(Animator anim) {
                View view = (View) ((ObjectAnimator) anim).getTarget();
                view.setRotationX(0f);
            }
        });

    }
}

2.8、使用类型求值器TypeEvaluator

通过实现TypeEvaluator接口可以创建Android系统没有实现的类型。Android系统已经实现的TypeEvaluator的有IntEvaluator, FloatEvaluatorArgbEvaluator。只需要 [evaluate()](http://developer.android.com/reference/android/animation/TypeEvaluator.html#evaluate(float, T, T))方法就可以计算出设置动画的属性当前时间点的相应的值。 FloatEvaluator类的实现如下所示:

public class FloatEvaluator implements TypeEvaluator {
     /**
      * @param fraction   实现TimeInterpolator接口的某个特定插值器的getInterpolation(float fraction)的返回值
      * @param startValue 添加动画效果的属性的起始值
      * @param endValue   添加动画效果的属性的结束值
      * @return 添加动画效果的属性在当前时间点的值
      */
    public Object evaluate(float fraction, Object startValue, Object endValue) {
        float startFloat = ((Number) startValue).floatValue();
        return startFloat + fraction * (((Number) endValue).floatValue() - startFloat);
    }
}

2.9、使用插值器Interpolators

在2.1节有关属性动画的原理中已经说到TimeInterpolator的作用是根据时间流逝率来计算当前属性的插值率。Android系统内置许多插值器如LinearInterpolator(匀速)、AccelerateInterpolator(一直加速)AccelerateDecelerateInterpolator(先加速后减速)、BounceInterpolator(回弹效果)等多种类型,如果要实现自定义类型,需要实现TimeInterpolator接口的getInterpolation方法,下面Android回弹效果插值器BounceInterpolator的源码:

/**
 * An interpolator where the change bounces at the end.
 */
@HasNativeInterpolator
public class BounceInterpolator extends BaseInterpolator implements NativeInterpolatorFactory {
    public BounceInterpolator() {
    }

    @SuppressWarnings({"UnusedDeclaration"})
    public BounceInterpolator(Context context, AttributeSet attrs) {
    }

    private static float bounce(float t) {
        return t * t * 8.0f;
    }

    public float getInterpolation(float t) {
        // _b(t) = t * t * 8
        // bs(t) = _b(t) for t < 0.3535
        // bs(t) = _b(t - 0.54719) + 0.7 for t < 0.7408
        // bs(t) = _b(t - 0.8526) + 0.9 for t < 0.9644
        // bs(t) = _b(t - 1.0435) + 0.95 for t <= 1.0
        // b(t) = bs(t * 1.1226)
        t *= 1.1226f;
        if (t < 0.3535f) return bounce(t);
        else if (t < 0.7408f) return bounce(t - 0.54719f) + 0.7f;
        else if (t < 0.9644f) return bounce(t - 0.8526f) + 0.9f;
        else return bounce(t - 1.0435f) + 0.95f;
    }

    /** @hide */
    @Override
    public long createNativeInterpolator() {
        return NativeInterpolatorFactoryHelper.createBounceInterpolator();
    }
}

看起来BounceInterpolator 并没有实现TimeInterpolator接口的getInterpolation方法,实际上BaseInterpolator是一个抽象类,它实现了Interpolator 接口但没有给出实现,而Interpolator 接口又是继承自TimeInterpolator的。

如果嫌自定义布局改变动画比较麻烦,可以使用默认的效果,方法是在某个XML布局中设置android:animateLayoutchanges属性为true。例如

<LinearLayout
    android:orientation="vertical"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:id="@+id/verticalContainer"
    android:animateLayoutChanges="true" />

现在对上面idverticalContainerLinearLayout内部的子view做添加、删除、显示和隐藏操作等就有布局发生改变的动画效果。

2.10、使用Keyframe设置动画

Keyframe的作用通过传入一对值(时间流失率与属性值)申明动画在特定时间点的特定状态。每一个keyframe 可以有自己的插值器来控制从前一帧到当前帧的动画表现形式。
可以使用KeyframeofInt()ofFloat()或者ofObject()工厂方法来实例化特定的Keyframe对象。然后通过PropertyValuesHolder 的工厂方法 [ofKeyframe()](http://developer.android.com/reference/android/animation/PropertyValuesHolder.html#ofKeyframe(android.util.Property, android.animation.Keyframe...))获取一个PropertyValuesHolder对象,然和将获取到的PropertyValuesHolder对象和需要添加动画的对象传入ObjectAnimator的工厂方法[ofPropertyValuesHolder](http://developer.android.com/reference/android/animation/ObjectAnimator.html#ofPropertyValuesHolder(java.lang.Object, android.animation.PropertyValuesHolder...))从而获取一个ObjectAnimator对象.

   Keyframe kf0 = Keyframe.ofFloat(0f, 0f);
   Keyframe kf1 = Keyframe.ofFloat(.5f, 360f);
   Keyframe kf2 = Keyframe.ofFloat(1f, 0f);
   PropertyValuesHolder pvhRotation =  PropertyValuesHolder.ofKeyframe("rotation", kf0, kf1, kf2);
   ObjectAnimator rotationAnim =  ObjectAnimator.ofPropertyValuesHolder(target, pvhRotation)
   rotationAnim.setDuration(5000ms);

具体示例参考APIDemos里的 MultiPropertyAnimation

2.11、使用ViewPropertyAnimator设置动画

使用ViewPropertyAnimator的好处是只需要一个底层的Animator对象就可以同时给各多个属性添加动画,效果跟于ObjectAnimator一样,都是通过实际通过修改view的属性,并且采用流式风格的代码,通过链式调用,代码更加精简易读。下面是同时给一个View的xy属性添加动画的三种不同的写法比较:

  • 多个ObjectAnimator对象方式
ObjectAnimator animX = ObjectAnimator.ofFloat(myView, "x", 50f);
ObjectAnimator animY = ObjectAnimator.ofFloat(myView, "y", 100f);
AnimatorSet animSetXY = new AnimatorSet();
animSetXY.playTogether(animX, animY);
animSetXY.start();
  • 一个ObjectAnimator对象方式
PropertyValuesHolder pvhX = PropertyValuesHolder.ofFloat("x", 50f);
PropertyValuesHolder pvhY = PropertyValuesHolder.ofFloat("y", 100f);
ObjectAnimator.ofPropertyValuesHolder(myView, pvhX, pvyY).start();
  • ViewPropertyAnimator方式
myView.animate().x(50f).y(100f);

通过比较可以明显看出ViewPropertyAnimator方式更加精简易读。更多有关ViewPropertyAnimator的内容需要参考Android开发官方博客Introducing ViewPropertyAnimator

2.12、XML声明方式设置动画

为了能够区分出View AnimationProperty Animation,配置View Animation的XML文件存放在Android项目的res/anim/目录下面,而配置Property Animation的XML文件存放在res/animator/目录下面。

类 名 称 标 签
ValueAnimator <animator>
ObjectAnimator <objectAnimator>
AnimatorSet <set>

XML配置动画集:

<set android:ordering="sequentially">
    <set>
        <objectAnimator
            android:propertyName="x"
            android:duration="500"
            android:valueTo="400"
            android:valueType="intType"/>
        <objectAnimator
            android:propertyName="y"
            android:duration="500"
            android:valueTo="300"
            android:valueType="intType"/>
    </set>
    <objectAnimator
        android:propertyName="alpha"
        android:duration="500"
        android:valueTo="1f"/>
</set>

配置 PropertyValuesHolder

<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
                android:duration="1000"
                android:repeatCount="1"
                android:repeatMode="reverse">
    <propertyValuesHolder android:propertyName="x" android:valueTo="400"/>
    <propertyValuesHolder android:propertyName="y" android:valueTo="200"/>
</objectAnimator>

配置Keyframe

<propertyValuesHolder android:propertyName="x" >
    <keyframe android:fraction="0" android:value="800" />
    <keyframe android:fraction=".2"
              android:interpolator="@android:anim/accelerate_interpolator"
              android:value="1000" />
    <keyframe android:fraction="1"
              android:interpolator="@android:anim/accelerate_interpolator"
              android:value="400" />
</propertyValuesHolder>
<propertyValuesHolder android:propertyName="y" >
    <keyframe/>
    <keyframe android:fraction=".2"
              android:interpolator="@android:anim/accelerate_interpolator"
              android:value="300"/>
    <keyframe android:interpolator="@android:anim/accelerate_interpolator"
              android:value="1000" />
</propertyValuesHolder>

加载动画资源:

   AnimatorSet set = (AnimatorSet) AnimatorInflater.loadAnimator(myContext, R.anim.property_animator);
   set.setTarget(myObject);
   set.start();

3、Drawable Animation

Drawable Animation(帧动画)通过加载一个接一个Drawable资源文件来生成动画,类似于老式电影通过连续旋转已经拍好的胶片放映电影。虽然可以使用AnimationDrawable来在代码中创建动画,但最常用的方式是在你的Android项目的res/drawable/目录下的XML文件定义动画,这个XML文件包含了构成该动画的所有帧以及每一帧的持续时间。如下所示:

<animation-list 
     xmlns:android="http://schemas.android.com/apk/res/android" 
     android:oneshot="true">  
   <item android:drawable="@drawable/rocket_thrust1" android:duration="200" />  
   <item android:drawable="@drawable/rocket_thrust2" android:duration="200" />  
   <item android:drawable="@drawable/rocket_thrust3" android:duration="200" />
</animation-list>

其中<animation-list>为该XML的根节点,android:oneshot属性设为true表示该动画只播放一次,并且当动画结束之后停留在最后一帧,如果为false则表示动画在播放完以后会循环播放。先假设该上面的XML文件在res/drawable/目录下的保存的文件名为rocket_thrust.xml,在一个Activity的ImageView中添加动画的实例代码如下:

AnimationDrawable rocketAnimation;

public void onCreate(Bundle savedInstanceState) { 
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    ImageView rocketImage = (ImageView) findViewById(R.id.rocket_image); 
    rocketImage.setBackgroundResource(R.drawable.rocket_thrust); 
    rocketAnimation = (AnimationDrawable) rocketImage.getBackground();
}
public boolean onTouchEvent(MotionEvent event) {
    if (event.getAction() == MotionEvent.ACTION_DOWN) { 
         rocketAnimation.start(); 
          return true;
     } 
    return super.onTouchEvent(event);
}

注意:不能在Activity的onCreate()方法中调用AnimationDrawable 的start()方法,因为AnimationDrawable 并没有完全加载到窗口里面来,如果想在没有交互的情况下(比如点击某按钮触发动画播放事件)就播放动画,在Activity的onWindowFocusChanged()方法中调用该方法。该方法会在窗口获取到焦点时被调用。

4、注意事项

  • OOM问题
    主要出现在Drawable Animation中,应尽量避免加载数量较多且较大的图片,尽量避免使用Drawable Animation
  • 内存泄漏
    属性动画中的无限循环动画需要在Activity退出的时候及时停止,否则将导致Activity无法释放而造成内存泄露。View Animation不存在这个问题。
  • 兼容问题
    某些动画在3.0以下系统上有兼容性问题,主要是Property Animation在Android 3.0以下不能使用。
  • View Animation的问题
    View Animation是对View的影像做动画,并不是真正的改变View的状态,因此有时候动画完成之后view无法隐藏,即setVisibility(View.GONE)失效了,此时需要调用view.clearAnimation()清除view动画才行。
  • 不要使用px
    尽量使用dp,px在不同的分辨率的手机下面会有不同的效果。
  • 动画交互问题
    在android3.0以前的系统上,view动画和属性动画,新位置均无法触发点击事件,同时,老位置仍然可以触发单击事件。从3.0开始,属性动画的单击事件触发位置为移动后的位置,view动画仍然在原位置。
  • 动画交互问题
    使用动画的过程中,建议开启硬件加速,这样会提高动画的流畅性。

5、参考资料

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

推荐阅读更多精彩内容