Android動畫詳解(下)

本篇文章主要介紹屬性動畫,需要了解補間動畫和幀動畫相關知識的,建議閱讀Android動畫詳解(上)。屬性動畫非常強大,運用也非常靈活,爲了便於理解,本文首先從類的角度介紹了屬性動畫的繼承關係,然後針對一些重點類介紹了其內的主要方法,最後通過demo的方式對屬性的動畫的常見用法進行了演示。

類繼承關係

屬性動畫存放在android.animation包下,主要的類繼承關係如下:
在這裏插入圖片描述

主要方法

Animator是一個抽象類,其內提供了一些公共方法。需要注意的是,一些方法是需要子類去重寫的,比如getInterpolator()這個方法目前直接返回null,子類需要根據實際情況返回Interpolator。
在這裏插入圖片描述
ValueAnimator是屬性動畫中非常常用的一個類,除了實現或重寫了父類的上述方法外,其還增加了一些新的功能,如常用的估值器的設置。
在這裏插入圖片描述
AnimatorSet主要用來控制動畫組合的播放順序及方式,特別是其的Builder,功能強大,使用方便。
在這裏插入圖片描述

常見用法

屬性動畫的用法非常靈活,首先從簡單的平移動畫開始入手,效果如如下:
在這裏插入圖片描述

代碼比較簡單,老規矩,介紹xml和Java兩種實現方式:
xml實現方式:

<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:propertyName="translationX"
android:duration="2000"
android:valueFrom="0"
android:valueTo="500"
android:repeatCount="1"
android:repeatMode= "reverse"
 />

Java中調用代碼:

        ObjectAnimator objectAnimator= (ObjectAnimator)AnimatorInflater.loadAnimator(this,R.animator.propertyanimation);
        objectAnimator.setTarget(mAnimationBtn0);
        objectAnimator.start();

需要注意的是xml代碼是放在res的animator目錄下,另外注意repeatCount的值和repeatMode選項。

Java實現方式:

 ObjectAnimator translationXAnimation = ObjectAnimator.ofFloat(mAnimationBtn0, "translationX", 0, 500);
                translationXAnimation.setDuration(2000);
                translationXAnimation.setRepeatCount(1);
                translationXAnimation.setRepeatMode(ValueAnimator.REVERSE);
                translationXAnimation.start();

上述Java代碼之所以能夠實現平移效果是因爲mAnimationBtn0這個Target屬於View包含translationX對應的get/set方法。至於爲啥是ofFloat而不是ofInt,是因爲其get/set方法對應的方法參數爲float類型,它們是相互關聯的。
旋轉、縮放等動畫與平移動畫類似,就不再介紹了。
假如我們需要改變一個View的寬度,但是View裏面卻沒有對應的get/set該如何實現呢?方案很多,爲了對比學習,挑選了三種典型的方案,效果圖如下:
在這裏插入圖片描述

從圖中可以看到紅、藍、綠三個View的變化效果一致,但是正如其顯示的內容一樣,使用了不同的方式,分別爲ObjectAnimator.ofInt、ValueAnimator.ofInt 、ValueAnimator.ofObject。

ObjectAnimator.ofInt 方式

該方式是處理這種沒有對應set/get方法卻想使用屬性動畫情況的常用的方式,主要思路是利用裝飾者模式,對現有target進行裝飾,在裝飾類裏提供set/get操作,完成相關屬性的設置。實現步驟如下:
步驟一:創建裝飾類

public class WidthWrapper {
    private View mTargetView;

    WidthWrapper(View view) {
        mTargetView = view;
    }

    public int getWidth(){
        return mTargetView.getLayoutParams().width;
    }
    public void setWidth(int width) {
        mTargetView.getLayoutParams().width = width;
        mTargetView.requestLayout();
    }
}

步驟二:配置屬性動畫

 ObjectAnimator widthAnimation = ObjectAnimator.ofInt(new WidthWrapper(mAnimationBtn1), "Width", 0, 600);
                widthAnimation.setDuration(2000);
                widthAnimation.start();

ValueAnimator.ofInt 方式

該方式是屬性動畫運用的通用方式,重要性較高,對一些效果複雜或多個動畫關聯調用的情況處理比較有優勢,其核心是動畫回調的運用。核心代碼如下:

 if (null != widthAnimation2 && widthAnimation2.isRunning()) {
                    return;
                }
                widthAnimation2 = ValueAnimator.ofInt(0, 600);
                widthAnimation2.setDuration(2000);
                widthAnimation2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation) {
                        mAnimationBtn2.getLayoutParams().width = (int) animation.getAnimatedValue();
                        mAnimationBtn2.requestLayout();

                    }
                });
                widthAnimation2.addListener(new Animator.AnimatorListener() {
                    @Override
                    public void onAnimationStart(Animator animation) {
                        mAnimationBtn2.getLayoutParams().width = 0;
                        mAnimationBtn2.requestLayout();
                    }

                    @Override
                    public void onAnimationEnd(Animator animation) {
                        mAnimationBtn2.getLayoutParams().width = 600;
                        mAnimationBtn2.requestLayout();
                    }

                    @Override
                    public void onAnimationCancel(Animator animation) {

                    }

                    @Override
                    public void onAnimationRepeat(Animator animation) {

                    }
                });
                widthAnimation2.start();

首先,如果動畫存在且正在執行則返回。如果動畫不存在則創建動畫,並設置監聽,在監聽中動態改變佈局參數並刷新佈局。最後啓動動畫。

ValueAnimator.ofObject方式

該方式主要是爲了演示TypeEvaluator的運用,針對沒有get/set這種場景並不常見,主要運用於一些類的特殊變化過程,實現過程有點像第一種方式和第二種方式的結合。實現步驟如下:
步驟一:自定義WidthObject

public class WidthObject {
    private int mWidth;

    WidthObject(int width) {
        mWidth = width;
    }

    public int getWidth() {
        return mWidth;
    }
}

步驟二:自定義TypeEvaluator

public class WidthTypeEvaluator implements TypeEvaluator <WidthObject>{
    @Override
    public WidthObject evaluate(float fraction, WidthObject startValue, WidthObject endValue) {
        return new WidthObject((int)(startValue.getWidth()+fraction*(endValue.getWidth()-startValue.getWidth())));
    }
}

自定義TypeEvaluator需要實現TypeEvaluator接口,根據傳入參數返回對應值。計算方式爲
startValue+fraction*(endValue-startValue)。
步驟三:配置屬性動畫

 if (null != widthAnimation3 && widthAnimation3.isRunning()) {
                    return;
                }
                widthAnimation3 = ValueAnimator.ofObject(new WidthTypeEvaluator(),new WidthObject(0), new WidthObject(600));
                widthAnimation3.setDuration(2000);
                widthAnimation3.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation) {
                        mAnimationBtn3.getLayoutParams().width = ((WidthObject)animation.getAnimatedValue()).getWidth();
                        mAnimationBtn3.requestLayout();

                    }
                });
                widthAnimation3.addListener(new Animator.AnimatorListener() {
                    @Override
                    public void onAnimationStart(Animator animation) {
                        mAnimationBtn3.getLayoutParams().width = 0;
                        mAnimationBtn3.requestLayout();
                    }

                    @Override
                    public void onAnimationEnd(Animator animation) {
                        mAnimationBtn3.getLayoutParams().width = 600;
                        mAnimationBtn3.requestLayout();
                    }

                    @Override
                    public void onAnimationCancel(Animator animation) {

                    }

                    @Override
                    public void onAnimationRepeat(Animator animation) {

                    }
                });
                widthAnimation3.start();

上述代碼跟ValueAnimator.ofInt 方式大同小異,都是獲取動態佈局參數並請求屬性佈局。

AnimatorSet的使用

AnimatorSet用於控制動畫的播放,跟AnimationSet作用類似,但還是有一定的區別的,相對更加強大,可以控制多個動畫的播放順序。下面的代碼設置了四個不同效果的屬性動畫,動畫1爲平移動畫,動畫2爲透明度變化動畫,動畫3爲縮放動畫,動畫4爲旋轉動畫。

 ObjectAnimator animation1 = ObjectAnimator.ofFloat(mAnimationBtn4, "translationX", 0, 500);
                ObjectAnimator animation2 = ObjectAnimator.ofFloat(mAnimationBtn4, "alpha", 0, 1);
                ObjectAnimator animation3 = ObjectAnimator.ofFloat(mAnimationBtn4, "scaleX", 2);
                ObjectAnimator animation4 = ObjectAnimator.ofFloat(mAnimationBtn4,"rotationX",0,270,50);
                AnimatorSet  animatorSet=new AnimatorSet ();
                animatorSet.play(animation1).with(animation2).after(animation3).before(animation4);
                animatorSet.setDuration(2000);
                animatorSet.start();

AnimatorSet 對其進行了組合調用,將動畫1和動畫2一起播放,並且在動畫3之後,在動畫4之前。所以其播放順序爲:動畫3>(動畫1==動畫2)>動畫4;效果如下:
在這裏插入圖片描述

自定義控件switcher

除了上述知識點之外,屬性動畫還有一個非常重要的知識點那就是Interpolator,下面通過一個常用的自定義控件switcher來演示Interpolator的用法。先上一個慢放的效果圖:
在這裏插入圖片描述

先處理我們比較熟悉的繪製過程,很明顯這個switcher無論什麼狀態都是由兩個圖形組成,外圍的橢圓和內部的圖形(圓或橢圓),且外圍橢圓的背景是根據狀態變化的。
外圍的圖形所在矩形座標好確認,如果不考慮pading之類的參數,左上x、y的起始值都爲0,右下x、y值分別爲其寬度和高度。內部圖形是座標是變化的,我們定義了mInnerStartX、mInnerStartY、 mInnerEndX、 mInnerEndY 這4個值來代表其所在矩形的左上和右下的x、y值。另外,需要根據當前的背景繪製設置外圍橢圓的背景。繪製過程如下:

  /**
     * 根據當前背景和計算出的座標信息繪製switcher
     * @param canvas
     */
    private void drawSwitcher(Canvas canvas) {
        RectF rect = new RectF(0, 0, mWidth, mHeight);
        mPaint.setColor(mBgOnCurrent);
        canvas.drawRoundRect(rect, rect.height() / 2, rect.height() / 2, mPaint);//畫外部橢圓
        RectF innerRect = new RectF(mInnerStartX, mInnerStartY, mInnerEndX, mInnerEndY);
        mPaint.setColor(Color.WHITE);
        canvas.drawRoundRect(innerRect, innerRect.height() / 2, innerRect.height() / 2, mPaint);//畫內部橢圓
    }

  @Override
    protected void onDraw(Canvas canvas) {
        drawSwitcher(canvas);
        super.onDraw(canvas);
    }

寬度和高度的獲取是在onSizeChanged中獲得的,代碼如下:

  @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = w;
        mHeight = h;
        mInnerStartY = mInterval;
        mInnerEndY = mHeight - mInterval;
        if (isOpen) {
            buildOpenStatusParams();
        } else {
            buildCloseStatusParams();
        }
    }

isOpen標識當前switcher是否爲打開狀態, buildOpenStatusParams爲構建打開時的狀態, buildCloseStatusParams爲構建關閉時的狀態,實現過程後面會有介紹。

剩下的主要工作就是構建switcher每個變化過程的繪製參數,這也是難點所在。

首先將switcher變化動畫分解,通過觀察可以看到,動畫主要分爲4個狀態:起始狀態、移動狀態1、移動狀態2、結束狀態。移動狀態1和2內部爲一個橢圓,其中狀態1橢圓的起始X跟起始狀態X一致,狀態2橢圓的結束X與結束狀態X一致。所謂的起始狀態和結束狀態是相對的,也就是說,開關啓動前狀態爲起始狀態,結束時爲結束狀態,可以是內部圓在左側的時候,也可以是內部圓在右側的時候。
將上述分析轉換爲代碼形式:

if (null == mAnimator) {
            mAnimator = ValueAnimator.ofInt(0, 3);
            mAnimator.setDuration(DURATION_TIME);
            mAnimator.setInterpolator(new SwitcherInterpolator());
            //設置監聽,動畫結束改變按鈕的當前狀態值
            mAnimator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    super.onAnimationEnd(animation);
                    isOpen = !isOpen;
                    if (null != mCallBack) {
                        mCallBack.IsOpen(isOpen);
                    }
                }
            });
            //根據進度判斷處於何種狀態,計算對應的繪製參數,0-起始狀態,1-移動過程1,2-移動過程2,3-結束狀態
            mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    switch ((int) animation.getAnimatedValue()) {
                        case 0:
                            if (isOpen) {
                                buildOpenStatusParams();
                            } else {
                                buildCloseStatusParams();
                            }
                            break;
                        case 1:
                            if (isOpen) {
                                buildRightToCloseStatusParams();
                            } else {
                                buildLeftToOpenStatusParams();
                            }
                            break;
                        case 2:
                            if (isOpen) {
                                buildLeftToCloseStatusParams();
                            } else {
                                buildRightToOpenStatusParams();
                            }
                            break;
                        case 3:
                            if (isOpen) {
                                buildCloseStatusParams();
                            } else {
                                buildOpenStatusParams();
                            }
                            break;
                    }
                    invalidate();
                }
            });
        } else if (mAnimator.isRunning()) {
            return;
        }
        mAnimator.start();

如果動畫不存在則構建動畫,值的變化範圍爲0~3,分別代表不同的狀態:0-起始狀態,1-移動過程1,2-移動過程2,3-結束狀態。添加監聽,在動畫結束時改變按鈕的狀態。添加進度監聽,根據不同的AnimatedValue(實際爲不同的狀態),構建不同狀態的繪製參數。如果動畫存在且正在運行則不執行任何操作,否則啓動已有動畫。動畫通過 setInterpolator方法設置了一個自定義Interpolator——SwitcherInterpolator,其代碼如下:

public class SwitcherInterpolator implements Interpolator {
    @Override
    public float getInterpolation(float input) {
        if(input<0.1){
            return 0;
        }else if(input<0.5){
            return 0.34f;
        }else if(input<0.9){
            return 0.67f;
        }else{
            return 1.0f;
        }
    }
}

SwitcherInterpolator中根據傳入的input(動畫執行的比例,取值從0到1)計算出當前動畫的進行程度。這裏將動畫的前1/10時間返回值設爲0,1/10-1/2設置爲0.34f, 1/2-9/10設置爲0.67f,後1/10時間設置爲1.0f.

  • 當進行程度爲0時,AnimatedValue爲0,爲起始狀態
  • 當進行程度爲0.34f時,AnimatedValue爲1,爲移動過程1
  • 當進行程度爲0.67f時,AnimatedValue爲2,爲移動過程2
  • 當進行程度爲1.0f時,AnimatedValue爲3,爲結束狀態

AnimatedValue值需要結合上面所講的TypeEvaluator知識來計算,例如移動狀態1的AnimatedValue=0+0.34f*(3-0).

另外,我們也可以通過自定義TypeEvaluator的方式來達到這種效果,TypeEvaluator是用來計算其對應對象的屬性在某個插值下的值。我們完全可以在TypeEvaluator下判斷當前的插值,然後返回對應的值(0,1,2,3),跟SwitcherInterpolator中的邏輯類似。

哪Interpolator和TypeEvaluator能否相互取代呢?答案肯定是否定的,因爲它倆的職責本身就不同,Interpolator主要是改變插值,而TypeEvaluator是根據插值改變屬性的值。當然,如果在TypeEvaluator增加一定的邏輯也能取代Interpolator達到特定的效果,但是這與其設計初衷想背離的。

mAnimator返回值表示當前的動畫狀態,根據動畫狀態和switcher的開關狀態構建繪製參數,構建方法如下:

 /***
     * 構建switcher關閉狀態參數
     */
    private void buildCloseStatusParams() {
        mInnerStartX = mInnerStartY;
        mInnerEndX = mInnerStartX + (mHeight - 2 * mInterval);
        mBgOnCurrent = mBgOnClose;
    }

    /***
     * 構建switcher打開狀態參數
     */
    private void buildOpenStatusParams() {
        mBgOnCurrent = mBgOnOpen;
        mInnerEndX = mWidth - mInterval;
        mInnerStartX = mInnerEndX - (mHeight - mInterval * 2);
    }

    /****
     * 構建switcher打開過程1參數,啓動起始X位置爲間隔,
     * 結束X位置爲寬度的2/3,當前背景爲關閉
     */
    private void buildLeftToOpenStatusParams() {
        mInnerStartX = mInnerStartY;
        mInnerEndX = mWidth / 3 * 2;
        mBgOnCurrent = mBgOnClose;
    }

    /****
     * 構建switcher打開過程2參數,啓動起始X位置爲寬度的1/3,
     * 結束X位置爲寬度-間隔,當前背景爲打開
     */
    private void buildRightToOpenStatusParams() {
        mInnerStartX = mWidth / 3;
        mInnerEndX = mWidth - mInterval;
        mBgOnCurrent = mBgOnOpen;
    }

    /***
     * 構建switcher關閉過程1參數,啓動起始X位置爲寬度的1/3,
     * 結束X位置爲寬度-間隔,當前北京爲打開
     */
    private void buildRightToCloseStatusParams() {
        mInnerStartX = mWidth / 3;
        mInnerEndX = mWidth - mInterval;
        mBgOnCurrent = mBgOnOpen;
    }

    /****
     * 構建switcher關閉過程2參數,啓動起始X位置爲間隔,
     * 結束X位置爲寬度的2/3,當前背景爲關閉
     */
    private void buildLeftToCloseStatusParams() {
        mInnerStartX = mInnerStartY;
        mInnerEndX = mWidth / 3 * 2;
        mBgOnCurrent = mBgOnClose;
    }

打開狀態和關閉狀態可能爲起始狀態也可能是結束狀態,需要根據switcher的開關狀態來判斷,如果當前switcher爲關閉,則關閉狀態爲起始狀態,打開狀態爲結束狀態;如果當前switcher爲打開則打開狀態爲起始狀態,關閉狀態爲結束狀態。switcher的打開和關閉過程邏輯跟上面的邏輯類似,結合代碼非常容易理解,就不詳細講解了。

剩下的就是一些完善性工作了

設置開關狀態的監聽,獲取開關的狀態變化

/***
 * 設置狀態改變回調
 * @param callback
 */
public void SetCallBack(SwitcherCallback callback) {
    mCallBack = callback;
}

public interface SwitcherCallback {
    void IsOpen(boolean aIsOpen);
}

設置開關的狀態

  /***
     * 設置開關狀態,不回調狀態改變接口
     * @param isOpen 開關狀態
     */
    public void setOpen(boolean isOpen) {
       setOpen(isOpen,false);
    }

    /***
     *  設置開關狀態,根據需要回調狀態
     * @param isOpen 開關狀態
     * @param isCallBack 是否需要回調
     */
    public void setOpen(boolean isOpen, boolean isCallBack) {
        this.isOpen = isOpen;
        if (isOpen) {
            buildOpenStatusParams();
        } else {
            buildCloseStatusParams();
        }
        invalidate();
        if (isCallBack && null != mCallBack) {
            mCallBack.IsOpen(isOpen);
        }
    }

switcher的核心代碼就這些,主要是對屬性動畫的靈活運用,以及對Interpolator的理解。這兩篇文章對應的代碼都放在了一個項目裏,有需要的可以下載。項目地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章