Android學(xué)習(xí)手記之動(dòng)畫

Android框架提供了兩種類型的動(dòng)畫:View Animation(也稱視圖動(dòng)畫)和Property Animation (也稱屬性動(dòng)畫),Property Animation只能在Android 3.0即以上版本才能使用。View Animation則不受版本限制,但是優(yōu)先考慮使用Property Animation 。除此之外可以利用Drawable Animation(也稱幀動(dòng)畫)實(shí)現(xiàn)一幀一幀地顯示資源文件(多為圖片)來展示動(dòng)畫。

1、View Animation

1.1、View Animation分類

View Animation通過在視圖上面執(zhí)行補(bǔ)間動(dòng)畫(tweened animation)生成動(dòng)畫效果,補(bǔ)間動(dòng)畫變換可以變換視圖對象的位置、大小、旋轉(zhuǎn)角度、透明度。Android的animation package提供了使用補(bǔ)間動(dòng)畫所有的類。目前View動(dòng)畫提供了平移動(dòng)畫、縮放動(dòng)畫、旋轉(zhuǎn)動(dòng)畫、透明度動(dòng)畫。各自對應(yīng)的類和相關(guān)標(biāo)簽如下表所示:

名 稱 標(biāo) 簽 子 類 效 果
平移動(dòng)畫 <translate> TranslateAnimation 移動(dòng)View
縮放動(dòng)畫 <scale> ScaleAnimation 縮放View
旋轉(zhuǎn)動(dòng)畫 <rotate> RotateAnimation 旋轉(zhuǎn)View
透明度動(dòng)畫 alpha AlphaAnimation 改變View的透明度

既可以在代碼中定義動(dòng)畫,也可以在XML中配置,但優(yōu)先采用在XML配置的方式,因?yàn)閺?fù)用性、可讀性更高。既可以順序播放動(dòng)畫,也可同時(shí)播放動(dòng)畫。如果采用XML配置的方式,需要將XML文件保存到Android項(xiàng)目的res/anim/目錄下面,XML只能有一個(gè)根元素,XML只能為單個(gè)的<alpha><scale><translate><rotate>、插值元素或者組織多個(gè)動(dòng)畫的<set>標(biāo)簽(內(nèi)部還可以嵌套其他<set>標(biāo)簽).下面是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>

假設(shè)文件在res/anim/目錄下保存為hyperspace_jump.xml,如果要想將其用在一個(gè)ImageView對象上面,下面是代碼示例:

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

如果是用硬編碼的方式創(chuàng)建動(dòng)畫,如下所示:

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

除了使用startAnimation()啟動(dòng)動(dòng)畫外,你也可以用Animation.setStartTime()設(shè)置動(dòng)畫的啟動(dòng)時(shí)間,然后用View.setAnimation()將動(dòng)畫注冊到View上面。
注意:.不管動(dòng)畫如何移動(dòng)或者調(diào)整,View的邊界域不會自動(dòng)地調(diào)整以便將動(dòng)畫包含在View邊界域內(nèi)。即便如此,當(dāng)動(dòng)畫超出了View的邊界域時(shí)仍然會繪制而不會發(fā)生剪切,但是如果超出了容納該View的父容器View的邊界域時(shí)候就會出現(xiàn)剪的現(xiàn)象

1.2、自定義View Animation

自定義動(dòng)畫要繼承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))方法中獲取變換矩陣做相應(yīng)的變換。參考ApiDemos中Rotate3dAnimation示例。

2、Property Animation

屬性動(dòng)畫的強(qiáng)大之處在于它可以給所有的對象而不僅僅是View對象設(shè)置動(dòng)畫。屬性動(dòng)畫通過在特定的時(shí)間段內(nèi)改變對象的某個(gè)屬性(需要為所添加動(dòng)畫的對象的該屬性提供setter方法,getter方法則是可選),可以設(shè)置動(dòng)畫的持續(xù)時(shí)間(Duration)、動(dòng)畫在持續(xù)時(shí)間段內(nèi)的屬性改變規(guī)則(Time interpolation時(shí)間插值)、動(dòng)畫的重復(fù)次數(shù)、動(dòng)畫結(jié)束之后是否倒放、動(dòng)畫組合(順序播放一組動(dòng)畫或者同時(shí)播放一組動(dòng)畫)、幀刷新的頻率(默認(rèn)每隔10ms刷新一次幀,設(shè)置刷新頻率不一定就會按照所指定的頻率來,取決于當(dāng)前系統(tǒng)負(fù)載的任務(wù)數(shù)目、底層計(jì)時(shí)器有多快等)

2.1、屬性動(dòng)畫的工作原理

屬性動(dòng)畫根據(jù)動(dòng)畫流逝時(shí)間的百分比來計(jì)算出當(dāng)前的屬性值的改變的百分比,然后根據(jù)計(jì)算出來的屬性值改變百分比、初值和終值來計(jì)算當(dāng)前的當(dāng)前時(shí)間點(diǎn)的屬性值(調(diào)用所添加的對象的某個(gè)屬性的set方法)。

如何計(jì)算動(dòng)畫

如上圖所示,ValueAnimator通過封裝定義動(dòng)畫插值的TimeInterpolator以及定義如何計(jì)算添加了動(dòng)畫的屬性的值TypeEvaluator

線性動(dòng)畫示例圖

要想啟動(dòng)一個(gè)動(dòng)畫,需要?jiǎng)?chuàng)建ValueAnimator,并設(shè)置初值、終值、動(dòng)畫的持續(xù)時(shí)間。調(diào)用start()方法啟動(dòng)。在整個(gè)動(dòng)畫的執(zhí)行過程中,ValueAnimator會計(jì)算時(shí)間流逝率( elapsed fraction ),也就是動(dòng)畫已經(jīng)消耗的時(shí)間所占總持續(xù)時(shí)間的百分比,0表示0%,1表示100%。以上面的線性動(dòng)畫示例圖為例,當(dāng) t = 10 ms時(shí),因?yàn)榭倳r(shí)間為40ms,流逝率為fraction=(10-40)/40=0.25。當(dāng)其計(jì)算出時(shí)間流逝率fraction后,會將該它傳入當(dāng)前的ValueAnimator所設(shè)置的TimeInterpolator(默認(rèn)是LinearInterpolator)的getInterpolation方法,該方法只需一個(gè)參數(shù),對傳入的fraction通過某種算法生成一個(gè)插值率(interpolated fraction),不同的TimeInterpolator會有不同的實(shí)現(xiàn),因此得到的插值率會不一樣,對于LinearInterpolator,得到的插值率就是傳入的流逝率,所以所以屬性的變化會隨著時(shí)間的變化是線性關(guān)系而,對于下面的圖,采用的是AccelerateDecelerateInterpolator,也就是先加速后減速的時(shí)間插值,在 t = 10 ms時(shí)雖然時(shí)間流失率為 0.25,但是AccelerateDecelerateInterpolatorgetInterpolation方法實(shí)現(xiàn)如下所示:

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

當(dāng)fraction = 0.25,返回的值約為0.1464,顯然小于傳入的0.25,最后將getInterpolation方法的返回值、初值、終值帶入相應(yīng)的TypeEvaluator的[evaluate](http://developer.android.com/reference/android/animation/TypeEvaluator.html#evaluate(float, T, T))方法得到此時(shí)的屬性值,例如IntEvaluator的[evaluate](http://developer.android.com/reference/android/animation/TypeEvaluator.html#evaluate(float, T, T))的實(shí)現(xiàn)為:

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

因此當(dāng) t = 10 ms時(shí),對于線性動(dòng)畫,X=(int)(0+0.25x(40-0))=10,對于非線性動(dòng)畫X=(int)(0+0.1464x(40-0))=6,如果是ObjectAnimator,還會在添加動(dòng)畫的對象上通過反射調(diào)用修改屬性的set方法。

非線性動(dòng)畫示例圖

2.2、Property Animation與View Animation的不同之處

Property Animation不僅僅可以對view對象添加動(dòng)畫,還能對非view對象添加動(dòng)畫,而View Animation如果要對非View對象添加動(dòng)畫,需要自己實(shí)現(xiàn)比較麻煩,并且view Animation所能操縱的屬性也很少只能對view的縮放、旋轉(zhuǎn)、平移設(shè)置動(dòng)畫,比如不能修改背景色。view animation只是修改了view所繪制的地方,但并沒有修改view本身,比如對一個(gè)按鈕添加平移動(dòng)畫,按鈕仍然處在原來的地方,Property Animation則消除了這些限制。可以對view和非View對象添加動(dòng)畫,并且在動(dòng)畫播放過程中修改自身。提供了插值(參見2.9)、多動(dòng)畫功能(參見2.5)。

2.3、使用ValueAnimator設(shè)置動(dòng)畫

ValueAnimator提供了ofInt()ofFloat()和 [ofObject()](https://developer.android.com/reference/android/animation/ValueAnimator.html#ofObject(android.animation.TypeEvaluator, java.lang.Object...))三中工廠方法來創(chuàng)建對象,ofInt()ofFloat()方法類似,前者傳入整數(shù)序列,后者傳入浮點(diǎn)數(shù)序列,而 [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需要實(shí)現(xiàn) TypeEvaluator,參見2.8:

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

由于上面兩處示例代碼并沒有作用到某個(gè)對象上,因此該動(dòng)畫并沒有效果,要想動(dòng)畫作用到某對象上上,需要添加動(dòng)畫監(jiān)聽器,調(diào)用getAnimatedValue()方法獲取刷新到某個(gè)特定幀時(shí)計(jì)算所得屬性值,然后做相應(yīng)的邏輯處理。

2.4、使用ObjectAnimator設(shè)置動(dòng)畫

ObjectAnimatorValueAnimator的子類,它內(nèi)部封裝了計(jì)時(shí)引擎和ValueAnimator的值計(jì)算,可以自動(dòng)更新設(shè)置動(dòng)畫的某個(gè)屬性而無需實(shí)現(xiàn)ValueAnimator.AnimatorUpdateListeneronAnimationUpdate()方法獲取每一幀的屬性值來手動(dòng)刷新,參見2.6節(jié),從而讓代碼更加簡潔,如下所示:

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

注意

  • ObjectAnimator能夠自動(dòng)更新屬性值,實(shí)現(xiàn)原理最終是通過反射調(diào)用在某個(gè)屬性上的setter方法。因此當(dāng)對此某個(gè)對象某個(gè)屬性使用ObjectAnimator設(shè)置動(dòng)畫的時(shí)候一定要確保該屬性有setter(setXxx()格式)方法。如果沒有對應(yīng)的setter方法,如果有權(quán)限,添加相關(guān)屬性的setter方法,如果沒有添加該屬性setter方法的權(quán)限,則使用一個(gè)包裝類,或者改用ValueAnimator
  • 如果在調(diào)用ObjectAnimator的工廠方法(比如ofFloat())方法在參數(shù)values...上只傳入了一個(gè)值,則只會將該值作為終值,因此在設(shè)置動(dòng)畫的某個(gè)屬性需要有g(shù)etter方法(getXxx()格式),因?yàn)樾枰ㄟ^getter方法獲取初值。
  • 屬性的setter和getter方法操作的屬性值必須是同一數(shù)據(jù)類型作為傳入 ObjectAnimatorvalues...參數(shù)的初值和終值,比如當(dāng)采用的是
ObjectAnimator.ofFloat(targetObject, "propName", 1f)

則屬性的setter和getter參數(shù)必須是float類型.

  • 有些屬性需要手動(dòng)調(diào)用invalidate()刷新值。比如Drawable對象的顏色值。參見2.6

2.5、使用AnimatorSet同時(shí)設(shè)置多種動(dòng)畫

使用AnimatorSet類可以實(shí)現(xiàn)同時(shí)播放、順序播放、延時(shí)播放多個(gè)動(dòng)畫。下面的代碼是Android SDK中模擬小球落體壓扁后有反彈起來一段距離,然后消失的動(dòng)畫,先播放bounceAnim,然后同時(shí)播放squashAnim1squashAnim2stretchAnim1stretchAnim2。接著播放bounceBackAnim,最后播放fadeAnim。所播放的動(dòng)畫均為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、設(shè)置動(dòng)畫監(jiān)聽器

Android提供了監(jiān)聽動(dòng)畫播放事件的接口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);

    }
   ......
}

需要說明的是,對于某些設(shè)置動(dòng)畫的屬性,需要手動(dòng)調(diào)用invalidate()方法才能確保將屏幕區(qū)域用動(dòng)畫得到的新的屬性值重新繪制自己。比如一個(gè)Drawable對象的顏色屬性,然而對于setAlpha()setTranslationX()則不需要手動(dòng)調(diào)用invalidate()
監(jiān)聽示例代碼:

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設(shè)置布局變換時(shí)動(dòng)畫

拼命寫代碼
拼命寫代碼

Android屬性動(dòng)畫提供了給ViewGroups布局改變時(shí)添加動(dòng)畫的功能。所謂布局改變動(dòng)畫是指在ViewGroup里的View,當(dāng)你添加或者刪除它、或者調(diào)用它的setVisibility()方法將對應(yīng)的值設(shè)為VISIBLEGONEINVISIBLE時(shí)候所呈現(xiàn)出的出場動(dòng)畫、退場動(dòng)畫效果,當(dāng)對某個(gè)ViewGroup內(nèi)部的view做添加或者刪除的時(shí)候,還可以給在ViewGroup中的其他view從原來的位置到新的位置添加動(dòng)畫。
用法:創(chuàng)建一個(gè)LayoutTransition對象,然后調(diào)用該對象的[setAnimator](https://developer.android.com/reference/android/animation/LayoutTransition.html#setAnimator(int, android.animation.Animator))(int transitionType, Animator animator),然后在需要添加ViewGroup布局改變動(dòng)畫的某個(gè)ViewGroup對象上調(diào)用setLayoutTransition(LayoutTransition transition)方法。
其中[setAnimator](https://developer.android.com/reference/android/animation/LayoutTransition.html#setAnimator(int, android.animation.Animator))(int transitionType, Animator animator)方法的第一個(gè)參數(shù)只能為LayoutTransition類中下面四個(gè)靜態(tài)常量。

  • APPEARING 某個(gè)View在ViewGroup中出現(xiàn)時(shí)的設(shè)置動(dòng)畫
  • CHANGE_APPEARING 當(dāng)新的View出現(xiàn)在ViewGroup中出現(xiàn)時(shí)給已經(jīng)存在于ViewGroup中的其他View設(shè)置動(dòng)畫
  • DISAPPEARING 某個(gè)View在ViewGroup中消失時(shí)的設(shè)置動(dòng)畫
  • CHANGE_DISAPPEARING 當(dāng)某個(gè)View從ViewGroup中消失時(shí)給ViewGroup中的其他View設(shè)置動(dòng)畫。

代碼示例如下:


/**
 * 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

通過實(shí)現(xiàn)TypeEvaluator接口可以創(chuàng)建Android系統(tǒng)沒有實(shí)現(xiàn)的類型。Android系統(tǒng)已經(jīng)實(shí)現(xiàn)的TypeEvaluator的有IntEvaluator, FloatEvaluatorArgbEvaluator。只需要 [evaluate()](http://developer.android.com/reference/android/animation/TypeEvaluator.html#evaluate(float, T, T))方法就可以計(jì)算出設(shè)置動(dòng)畫的屬性當(dāng)前時(shí)間點(diǎn)的相應(yīng)的值。 FloatEvaluator類的實(shí)現(xiàn)如下所示:

public class FloatEvaluator implements TypeEvaluator {
     /**
      * @param fraction   實(shí)現(xiàn)TimeInterpolator接口的某個(gè)特定插值器的getInterpolation(float fraction)的返回值
      * @param startValue 添加動(dòng)畫效果的屬性的起始值
      * @param endValue   添加動(dòng)畫效果的屬性的結(jié)束值
      * @return 添加動(dòng)畫效果的屬性在當(dāng)前時(shí)間點(diǎn)的值
      */
    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節(jié)有關(guān)屬性動(dòng)畫的原理中已經(jīng)說到TimeInterpolator的作用是根據(jù)時(shí)間流逝率來計(jì)算當(dāng)前屬性的插值率。Android系統(tǒng)內(nèi)置許多插值器如LinearInterpolator(勻速)、AccelerateInterpolator(一直加速)AccelerateDecelerateInterpolator(先加速后減速)、BounceInterpolator(回彈效果)等多種類型,如果要實(shí)現(xiàn)自定義類型,需要實(shí)現(xiàn)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 并沒有實(shí)現(xiàn)TimeInterpolator接口的getInterpolation方法,實(shí)際上BaseInterpolator是一個(gè)抽象類,它實(shí)現(xiàn)了Interpolator 接口但沒有給出實(shí)現(xiàn),而Interpolator 接口又是繼承自TimeInterpolator的。

如果嫌自定義布局改變動(dòng)畫比較麻煩,可以使用默認(rèn)的效果,方法是在某個(gè)XML布局中設(shè)置android:animateLayoutchanges屬性為true。例如

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

現(xiàn)在對上面idverticalContainerLinearLayout內(nèi)部的子view做添加、刪除、顯示和隱藏操作等就有布局發(fā)生改變的動(dòng)畫效果。

2.10、使用Keyframe設(shè)置動(dòng)畫

Keyframe的作用通過傳入一對值(時(shí)間流失率與屬性值)申明動(dòng)畫在特定時(shí)間點(diǎn)的特定狀態(tài)。每一個(gè)keyframe 可以有自己的插值器來控制從前一幀到當(dāng)前幀的動(dòng)畫表現(xiàn)形式。
可以使用KeyframeofInt()ofFloat()或者ofObject()工廠方法來實(shí)例化特定的Keyframe對象。然后通過PropertyValuesHolder 的工廠方法 [ofKeyframe()](http://developer.android.com/reference/android/animation/PropertyValuesHolder.html#ofKeyframe(android.util.Property, android.animation.Keyframe...))獲取一個(gè)PropertyValuesHolder對象,然和將獲取到的PropertyValuesHolder對象和需要添加動(dòng)畫的對象傳入ObjectAnimator的工廠方法[ofPropertyValuesHolder](http://developer.android.com/reference/android/animation/ObjectAnimator.html#ofPropertyValuesHolder(java.lang.Object, android.animation.PropertyValuesHolder...))從而獲取一個(gè)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設(shè)置動(dòng)畫

使用ViewPropertyAnimator的好處是只需要一個(gè)底層的Animator對象就可以同時(shí)給各多個(gè)屬性添加動(dòng)畫,效果跟于ObjectAnimator一樣,都是通過實(shí)際通過修改view的屬性,并且采用流式風(fēng)格的代碼,通過鏈?zhǔn)秸{(diào)用,代碼更加精簡易讀。下面是同時(shí)給一個(gè)View的xy屬性添加動(dòng)畫的三種不同的寫法比較:

  • 多個(gè)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();
  • 一個(gè)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方式更加精簡易讀。更多有關(guān)ViewPropertyAnimator的內(nèi)容需要參考Android開發(fā)官方博客Introducing ViewPropertyAnimator

2.12、XML聲明方式設(shè)置動(dòng)畫

為了能夠區(qū)分出View AnimationProperty Animation,配置View Animation的XML文件存放在Android項(xiàng)目的res/anim/目錄下面,而配置Property Animation的XML文件存放在res/animator/目錄下面。

類 名 稱 標(biāo) 簽
ValueAnimator <animator>
ObjectAnimator <objectAnimator>
AnimatorSet <set>

XML配置動(dòng)畫集:

<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>

加載動(dòng)畫資源:

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

3、Drawable Animation

Drawable Animation(幀動(dòng)畫)通過加載一個(gè)接一個(gè)Drawable資源文件來生成動(dòng)畫,類似于老式電影通過連續(xù)旋轉(zhuǎn)已經(jīng)拍好的膠片放映電影。雖然可以使用AnimationDrawable來在代碼中創(chuàng)建動(dòng)畫,但最常用的方式是在你的Android項(xiàng)目的res/drawable/目錄下的XML文件定義動(dòng)畫,這個(gè)XML文件包含了構(gòu)成該動(dòng)畫的所有幀以及每一幀的持續(xù)時(shí)間。如下所示:

<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的根節(jié)點(diǎn),android:oneshot屬性設(shè)為true表示該動(dòng)畫只播放一次,并且當(dāng)動(dòng)畫結(jié)束之后停留在最后一幀,如果為false則表示動(dòng)畫在播放完以后會循環(huán)播放。先假設(shè)該上面的XML文件在res/drawable/目錄下的保存的文件名為rocket_thrust.xml,在一個(gè)Activity的ImageView中添加動(dòng)畫的實(shí)例代碼如下:

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()方法中調(diào)用AnimationDrawable 的start()方法,因?yàn)锳nimationDrawable 并沒有完全加載到窗口里面來,如果想在沒有交互的情況下(比如點(diǎn)擊某按鈕觸發(fā)動(dòng)畫播放事件)就播放動(dòng)畫,在Activity的onWindowFocusChanged()方法中調(diào)用該方法。該方法會在窗口獲取到焦點(diǎn)時(shí)被調(diào)用。

4、注意事項(xiàng)

  • OOM問題
    主要出現(xiàn)在Drawable Animation中,應(yīng)盡量避免加載數(shù)量較多且較大的圖片,盡量避免使用Drawable Animation
  • 內(nèi)存泄漏
    屬性動(dòng)畫中的無限循環(huán)動(dòng)畫需要在Activity退出的時(shí)候及時(shí)停止,否則將導(dǎo)致Activity無法釋放而造成內(nèi)存泄露。View Animation不存在這個(gè)問題。
  • 兼容問題
    某些動(dòng)畫在3.0以下系統(tǒng)上有兼容性問題,主要是Property Animation在Android 3.0以下不能使用。
  • View Animation的問題
    View Animation是對View的影像做動(dòng)畫,并不是真正的改變View的狀態(tài),因此有時(shí)候動(dòng)畫完成之后view無法隱藏,即setVisibility(View.GONE)失效了,此時(shí)需要調(diào)用view.clearAnimation()清除view動(dòng)畫才行。
  • 不要使用px
    盡量使用dp,px在不同的分辨率的手機(jī)下面會有不同的效果。
  • 動(dòng)畫交互問題
    在android3.0以前的系統(tǒng)上,view動(dòng)畫和屬性動(dòng)畫,新位置均無法觸發(fā)點(diǎn)擊事件,同時(shí),老位置仍然可以觸發(fā)單擊事件。從3.0開始,屬性動(dòng)畫的單擊事件觸發(fā)位置為移動(dòng)后的位置,view動(dòng)畫仍然在原位置。
  • 動(dòng)畫交互問題
    使用動(dòng)畫的過程中,建議開啟硬件加速,這樣會提高動(dòng)畫的流暢性。

5、參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容