Android屬性動畫解析,讓天下沒有難畫的動畫

ggghhh 8年前發布 | 10K 次閱讀 安卓開發 Android開發 移動開發

在屬性動畫出現之前,Android的動畫包括逐幀動畫、補間動畫兩種。逐幀動畫類似于gif圖片,一幀一幀的播放準備好的圖片。補間動畫作用于一個View,可以實現平移、縮放、旋轉、透明度這四種效果,也可以將多種動畫同時執行,并控制執行的速度。這兩種動畫的使用非常簡單,可以很輕松的實現一些簡單的動畫效果,關于具體的使用方法,不是本文的重點,不了解的同學可以自行百度~~

然而,這兩種動畫在使用簡單的同時,也有很大的局限性。逐幀動畫只能在準備好每一幀圖片的情況下播放。補間動畫只能實現上面提到的幾種效果,如果希望能實現更復雜的自定義動畫,那它就完全無能為力。另外,補間動畫只是對View的視圖進行改變,而沒有真正的改變View的屬性。例如使用補間動畫將一個Button從A平移到B,這時你嘗試點擊這個Button,你會發現它的點擊事件依然只能在A處響應。在移動端對UI的要求越來越高的今天,缺陷那么多的動畫框架是肯定沒法滿足需求的,于是,屬性動畫應運而生了。

屬性動畫有多強大呢?可以這么說,屬性動畫幾乎可以完全替代上面提到的兩種動畫,另外,如果你真正理解了屬性動畫的原理和用法,你會發現曾經感到難以實現、望而卻步的自定義View / 自定義動畫效果在屬性動畫面前原來是如此簡單。

好了,吹了這么多,下面進入本文的重點:屬性動畫到底是什么?我對屬性動畫的理解可以概括為以下一句話:

屬性動畫的本質是對象屬性的變化,即值的變化,而動畫效果只不過是值變化的一種表現形式。

這么說可能有點難以理解,舉個簡單的例子:對于下圖的這個動畫效果,你想怎么去實現它?你可能會說這個簡單,我問UI要一張圖,然后不停地旋轉它就實現了。但是如果有一天產品經理告訴你需求改了,現在要讓小球的大小可以配置,并且在旋轉的同時改變顏色,同時圓環中間要加個不停變化的文字……好吧,那現你只能帶著手撕產品經理的想法重新實現一遍了,那么如果要用動畫實現這個效果,應該怎么做呢?

Ring_Rotate.gif

如果你想的是趕緊查一查動畫的API,看看有沒有這種View繞圓旋轉的方法的話,那只能說你對動畫的理解還停留在Android3.0之前的時代。那么,如果使用屬性動畫的話,應該怎么實現呢?不妨對這個動畫進行分解,看看這個動畫做了什么:首先畫了個圓環,然后在圓環上有一個紅色的小球繞圓心旋轉。在這個過程中,唯一改變的是小球的坐標(x, y)。如果我們拋開視圖上的動畫,那么可以把這個動畫看做是無數坐標點的集合,所謂的動畫,只是在每個坐標點處重新繪制了小球。因此,如果我們得到小球坐標隨時間變化的規律,這個動畫自然就繪制出來了。帶著這個思想,我們開始屬性動畫的學習。

ValueAnimator

ValueAnimator是屬性動畫的核心類。它實現的就是為動畫的初始值和結束值在動畫運行時間內提供一個平滑的過度。舉個栗子:

ValueAnimatoranimator =ValueAnimator.ofInt(200,400);
animator.setDuration(3000);
animator.start();

ofInt方法返回一個ValueAnimator對象,ValueAnimator運行之后的效果是在3秒的時間內將一個值從200平滑過度到400。ofInt方法傳的參數沒有限制,比如:

ValueAnimatoranimator =ValueAnimator.ofInt(0,100,0,50);

就表示將一個值從1過度到100再到0再到50。

類似的方法還有ofFloat()、ofArgb()、ofObject()。ofFloat()和ofArgb()看著名字就應該知道是干啥的了,用法也是類似的~ofObject()等會再講。

看到這里可能有人就著急了:這坑爹呢,說好的動畫呢?連影子都沒見著啊!

別急,還記得前面說的嗎,屬性動畫的本質是值的變化,動畫只是值變化的表現形式。制定了值變化的規則,在這基礎上繪制動畫,那簡直是信手拈來。例如我們想讓一個小球從x軸的200px處平移到400px處,只需要在以上代碼中增加一個監聽:

animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                intcurrentValue=(int)animation.getAnimatedValue();
                invalidate();//具體繪制邏輯需要在onDraw中實現
            }
        });

監聽值從200變化到400的過程,在變化過程中可以通過getAnimatedValue()方法獲取到當前的值。取到這個值以后,就可以在相應的位置調用invalidate() / postInvalidate()方法重繪小球,一個簡單的平移動畫就實現了~

當然,只滿足于這么簡單的動畫當然是不夠的,一個較為復雜的動畫可能有不止一個變化的值,并且值的變化也不一定是線性的。這時就需要用到ValueAnimator更靈活的方法:ofObject()

public static ValueAnimator ofObject(TypeEvaluator evaluator, Object... values) {
        ValueAnimator anim = new ValueAnimator();
        anim.setObjectValues(values);
        anim.setEvaluator(evaluator);
        return anim;
    }

可以看到,ofObject方法除了傳遞初始值和結束值之外,還需要傳遞一個TypeEvaluator類型的參數,這個TypeEvaluator是做什么的呢?由于ofObject方法的初始值和結束值都是自定義的對象,而ValueAnimator并不知道如何從初始的對象過度到結束的對象,所以需要調用者制定一個過度的規則,舉個栗子:

Android屬性動畫解析,讓天下沒有難畫的動畫

Ring_Rotate.gif

還是看這個小球繞圓環旋轉的動畫,現在定義一個對象來管理這個動畫里的變量:

private class CirclePoint{
        float angle;

        public CirclePoint(float angle){
            this.angle = angle;
        }
    }

小球坐標的變化其實就是小球在圓上角度的變化。暫時我們只放角度這一個變量。然后實現一下ofObject方法:

ValueAnimator animator = ValueAnimator.ofObject(new TypeEvaluator() {
            @Override
            public Object evaluate(float fraction, Object startValue, Object endValue) {
                CirclePoint start = (CirclePoint) startValue;
                CirclePoint end = (CirclePoint) endValue;
                float startAngle = start.angle;
                float endAngle = end.angle;
                float currentAngle = startAngle + (endAngle - startAngle) * fraction;
                return new CirclePoint(currentAngle);
            }
        }, new CirclePoint(0), new CirclePoint(360));

可以看到,我們傳入了TypeEvaluator的實現類并重寫了evaluate()方法。evaluate()方法中的第一個參數fraction就表示了動畫的當前進度,范圍是0到1,我們就根據這個進度值來計算對象當前應該是什么樣的。后面兩個參數就表示了動畫的初始值和結束值。evaluate()方法里的邏輯還是非常簡單的,之后我們讓角度從0變化到360度。

好,以上我們就制定了從初始對象過度到結束對象時的規則,接下來就是監聽動畫的進度并實現動畫了。全部代碼:

public class RingRotateView extends View {

    private Context context;
    private Paint ringPaint, circlePaint;
    private int width, height;//外圓弧寬高
    private int outRadius, inRadius;//外圓弧和小圓半徑
    private float outX, outY, inX, inY;//外圓弧和小圓中心點
    private float distance;//大圓中心點到小圓中心點的距離
    private int PADDING = 50;
    private int STROKEN_WIDTH = 4;//圓環寬度
    private CirclePoint currentPoint;

    public RingRotateView(Context context) {
        super(context);
        this.context = context;
        init();
    }

    public RingRotateView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
        init();
    }

    public RingRotateView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context;
        init();
    }

    private void init(){
        ringPaint = new Paint();
        ringPaint.setStrokeWidth(STROKEN_WIDTH);
        ringPaint.setStyle(Paint.Style.STROKE);
        ringPaint.setColor(Color.WHITE);
        ringPaint.setAntiAlias(true);

        circlePaint = new Paint();
        circlePaint.setStyle(Paint.Style.FILL);
        circlePaint.setColor(Color.RED);
        circlePaint.setAntiAlias(true);

        currentPoint = new CirclePoint(0);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        width = MeasureSpec.getSize(widthMeasureSpec);
        height = MeasureSpec.getSize(heightMeasureSpec);
        outRadius = width > height ? height / 2 - PADDING : width / 2 - PADDING;
        inRadius = outRadius / 16;
        outX = width / 2;
        outY = height / 2;
        distance = outRadius - inRadius -STROKEN_WIDTH / 2;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawCircle(outX, outY, outRadius, ringPaint);
        inX = outX + distance * (float)Math.sin(currentPoint.angle / 360 * 2 * Math.PI);
        inY = outY - distance * (float)Math.cos(currentPoint.angle / 360 * 2 * Math.PI);
        canvas.drawCircle(inX, inY, inRadius, circlePaint);
    }

    public void startAnimation(){
        ValueAnimator animator = ValueAnimator.ofObject(new TypeEvaluator() {
            @Override
            public Object evaluate(float fraction, Object startValue, Object endValue) {
                CirclePoint start = (CirclePoint) startValue;
                CirclePoint end = (CirclePoint) endValue;
                float startAngle = start.angle;
                float endAngle = end.angle;
                float currentAngle = startAngle + (endAngle - startAngle) * fraction;
                return new CirclePoint(currentAngle);
            }
        }, new CirclePoint(0), new CirclePoint(360));

        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                currentPoint = (CirclePoint)valueAnimator.getAnimatedValue();
                invalidate();
            }
        });

        animator.setRepeatCount(-1);//無限重復
        animator.setDuration(1000);
        animator.start();
    }

    private class CirclePoint{
        float angle;

        public CirclePoint(float angle){
            this.angle = angle;
        }
    }
}

如果現在我們需要讓小球的顏色和大小不斷變化,只需要修改一下對象:

private class CirclePoint{
        float angle;
        float radius;
        int color;

        public CirclePoint(float angle, float radius, int color){
            this.angle = angle;
            this.radius = radius;
            this.color = color;
        }
    }

然后在TypeEvaluator中制定一下radius和color的改變規則,并在onDraw中繪制出來就可以了~

Interpolator

屬性動畫的Interpolator和補間動畫的Interpolator意義相同,都是用來控制動畫速率的。例如我們想讓動畫先加速播放,再減速播放,這時就可以用到Interpolator。Android為我們提供了幾種Interpolator,比如LinearInterpolator可以讓動畫勻速播放,AccelerateDecelerateInterpolator是默認的Interpolator,可以讓動畫先加速后減速播放。在屬性動畫中,Interpolator直接控制了fraction的改變速率。我們也可以自己定義一個Interpolator,來讓動畫按照我們希望的速率變化。

animator.setInterpolator(new Interpolator() {
            @Override
            public float getInterpolation(float input) {
                return 1 - (1 - input) * (1 - input) * (1 - input);
            }
        });

以上就實現了一個速率快速降低的Interpolator。

ObjectAnimator

其實ObjectAnimator才是屬性動畫中最常用的類,把它放到最后講是因為它的原理和ValueAnimator完全相同,它也是繼承于ValueAnimator的。ObjectAnimator的特點是可以操作對象擁有get/set方法的任意屬性,相對于ValueAnimator,它使用起來更簡潔。例如上文提到的將一個View從x軸200px移到400px處,只需要這么寫:

ObjectAnimator animator = ObjectAnimator.ofFloat(view, "translationX", 200f, 400f);  
animator.setDuration(3000);  
animator.start();

由于ObjectAnimator是繼承于ValueAnimator的,它也可以用到ValueAnimator中的所有方法,所以對于復雜一些的動畫,用ObjectAnimator一樣可以輕松搞定了~

總結

相對于視圖動畫,屬性動畫的核心在于它改變了對象的屬性,而不僅僅是視圖的變化。動畫只是對象屬性改變后在視圖層面的表現形式。掌握了屬性動畫的原理,相信一切復雜的動畫在你面前都是紙老虎!

 

來自:http://www.jianshu.com/p/7c0a8ae737e5

 

 本文由用戶 ggghhh 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!