1. 程式人生 > >使用animator實現粒子動畫效果

使用animator實現粒子動畫效果

1、前言

本文圍繞著實現粒子放大效果,著重講解android中涉及到動畫縮放以及動畫集的使用,並且會將講解一些插值器相關的知識。閱讀本文需要讀者有一定的自定義View的基礎知識,本文將不再講解自定義View的相關知識,讀者需要可以自行去學習,也可以閱讀筆者的文章,自定義View的基本知識

2、著色器

為了讓效果的色彩比較的絢麗,需要讓粒子(這裡其實就是用小圓點代替)有一個色彩的過渡,所以需要用到著色器。比較常用到的主要有線性漸變的著色器,它可以讓粒子整體上看上去具有一個按照線性排列的色彩過渡。瞭解線性漸變的著色器之前,先了解shader。在paint裡面有一個setshader方法,用於設定著色器,著色器的作用就是用於給顏色實現一個跨度的平衡感,如果paint裡面包含了shader物件,那麼用此paint繪畫的線,圓圈,影象之類的圖案,都會從shader裡面獲取顏色,因此,它是實現絢麗色彩的基礎。

2.1LinearGradient

筆者更傾向與叫它線性畫筆,它是shader的子類,他實現的顏色平衡感是通過線性漸變來實現的。使用它,很簡單,只需要知道它的建構函式就行。
public LinearGradient(float x0, float y0, float x1, float y1, int color0, int color1,
            TileMode tile)

其中x0表示起始點x座標,x1表示終止點x座標,color0是起始顏色,color1是終止顏色,tile是平鋪模式,它主要有三個值分別是TileMode.CLAMP,TileMode.REPEAT,TileMode.MIRROR。分表表示覆制模式,重複模式,映象模式。假如有如下程式碼:
 LinearGradient linearGradient=new LinearGradient(0,0,canvas.getWidth(),canvas.getHeight(), Color.RED,Color.BLUE, Shader.TileMode.REPEAT);
        Paint paint=new Paint();
        paint.setShader(linearGradient);
        canvas.drawCircle(canvas.getWidth()/2,canvas.getHeight()/2,canvas.getWidth()/2,paint);


那麼它的效果如下:
可以看到線性漸變畫筆可以讓顏色有一個過渡,這種感覺是很美妙的。如果我們再加一些動畫,一些透明度變化,就可以做出很絢麗的色彩效果了。

3.animator

animator是android3.0之後提供的特性,在這之前使用的是animation,區別在於,前者是可以真正改變屬性值的,後者只是改變了檢視的位置,但是檢視的屬性並沒有得到任何改變,型別障眼法。animator是一個抽象類,是valueAnimator,objectAnimator等的父類,它主要鑑定了一些屬性動畫的基本操作,比如啟動,暫停設定動畫監聽器等等。 啟動動畫:
public void start()

 public void end()
上述方法分別用於啟動動畫和終止動畫,start方法由哪個執行緒啟動則執行在哪個執行緒,end方法則用於終止動畫的執行,它會讓動畫停止並且迅速到結尾的值(屬性動畫一般會有插值器,預設的是先加速後減速的插值器,用於從一個初始值到結尾制的過渡)。 暫停,繼續動畫:
public void pause()
public void resume() 

pause用於暫停動畫,如果動畫尚未start,或者已經end,則此呼叫會被忽略。注意此方法必須和start同一執行緒被呼叫。如果要繼續動畫,則呼叫resume方法。 設定動畫時長:
 public abstract Animator setDuration(long duration)

用於給動畫從開始到結束設定一個時間長度,單位是毫秒。 設定時間插值器:
public abstract void setInterpolator(TimeInterpolator value);

此方法可以設定時間插值器,它的作用是,可以讓動畫不處於線性變化的效果,預設的話是先加速後減速的插值器。即動畫效果從起始值到終止值得過渡,會經歷一個加速減速的過程。 設定監聽器:
public void addListener(AnimatorListener listener) 

如果希望監聽動畫是否完成結束或者重複的動作,就可以設定一個動畫監聽器。animatorListener的原型如下:

3.1 animatorListener

  public static interface AnimatorListener {
      
        void onAnimationStart(Animator animation);

    
        void onAnimationEnd(Animator animation);

    
        void onAnimationCancel(Animator animation);

      
        void onAnimationRepeat(Animator animation);
    }


當呼叫start時候,onAnimationStart就會被回撥,當end被呼叫的時候,onAnimationEnd被回撥。當呼叫cancel停止動畫的時候,onAnimatonCancel被回撥。 以上是animator的基本要點。有了這些基本知識,現在就可以學習ValueAnimator了。

4.valueAnimator

valueAnimator是一個值動畫,怎麼理解呢?它的作用在於,能夠把你設定的初始值,和終止值作為起始點,然後通過插值器,在一段時間內,按照插值器的的計算來設定當前動畫的值,因為這些值一般都會影響動畫的效果,所以叫做值動畫。對於插值器,可能大家比較模糊,插值器主要是用來加速度來改變值的,比如:
AccelerateDecelerateInterpolator


 public float getInterpolation(float input) {
        return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f;
    }
這是一個先加速後減速的插值器,它的計算過程是通過getInterpolation來計算返回值的。再細心一點,大家可以研究一下cos函式的數值變化,就知道為什麼是先加速後減速了。

4.1常用的初始化valueAnimator的方法

既然valueAnimator是值動畫,那麼自然包含可以設定值的方法,valueAnimator對一些常用的數值型別提供了支援。比如:
public static ValueAnimator ofInt(int... values) 

public static ValueAnimator ofArgb(int... values)

public static ValueAnimator ofFloat(float... values)
它們都可用於設定多個值,注意,千萬不要只設置一個值,最少都要兩個值,一個代表起始值,一個代表終止值。這三個方法分表表示int資料型別的變化值動畫,float,argb顏色的值動畫。到這裡大家可能迷惑,這些值是除了作為起始點和終止點之外,有什麼用處。在valueAnimator中,有一個方法是用於獲取屬性值的,如下:
 public Object getAnimatedValue()
這個方法用於獲取動畫當前處於的值,這個值必定是介於起始值和終止值之間的。比如我們用ofInt獲得了一個valueAnimator,然後監聽動畫,在每次動畫更新的時候,就可以呼叫getAnimatedValue方法獲取當前的int值,這個值可以用來重新設定畫筆的粗細,或者圓的半徑,矩形的寬和高...... 除了這些資料型別之外,也允許自定義資料型別,但是因為是自定義的型別,valueAnimator並不知道你所需要的變化規則是什麼,所以你需要給他們提供變化規則。使用自定義資料型別獲取valueAnimator的方法如下:
 public static ValueAnimator ofObject(TypeEvaluator evaluator, Object... values)

在講述這個方法之前,先了解一下TypeEvaluator,從字面值上看,它是用來評估值的。它確實也是這樣的。

4.2TypeEvaluator


TypeEvaluator是一個介面,用於開發者自定義值變化的規則,可通過ValueAnimator.setEvaluator方法給值動畫設定自定義的值變化規則。在這個介面中,最重要也是為一個的一個方法是:
 public T evaluate(float fraction, T startValue, T endValue);

其中fraction是一個插值器提供的值,就是我們前面講的插值器,通常我們實現這個方法,用result=x0+t*(x1-x0)這個規則就好,x0表示startValue,x1表示endValue,t表示fraction。這樣就可以通過getAnimatedValue方法獲取到計算結果。


4.3 oFObject方法

現在回到我們剛剛那個方法來。說到,這個方法可以獲取一個自定義值變化規則的valueAnimator動畫。那麼我們需要怎麼使用它呢。給出一個例子: 首先需要一個TypeEvaluator提供變化規則。假設現在有一個Circle實體,裡面含有x,y,radius分別表示中心點和半徑。我們定義一個circle規則的TypeEvaluator
public class CircleEvaluate implements TypeEvaluator<Circle> {
    /**
     * 估算值的表示式 result = x0 + t * (x1 - x0)
     * x0是startValue,x1是endValue,t是fraction
     *
     * @param fraction
     * @param startValue
     * @param endValue
     * @return
     */
    @Override
    public Circle evaluate(float fraction, Circle startValue, Circle endValue) {
        float circleX = startValue.getCenterX() + fraction * (endValue.getCenterX() - startValue.getCenterX());
        float circleY = startValue.getCenterY() + fraction * (endValue.getCenterY() - startValue.getCenterY());
        float radius = startValue.getRadius() + fraction * (endValue.getRadius() - startValue.getRadius());
        return new Circle(circleX, circleY, radius);
    }


接著,就可以使用它定義我們的valueAnimator了。如下:
 ValueAnimator animator = ValueAnimator.ofObject(evaluate, circleGroup[i][j],minCircleGroup[i][j]);
後面兩個其實是circle物件。對於valueAnimator,因為是animator的子類,所以包含有基本的start,end等方法,它也可以設定插值器setInterpolator,設定Evaluator.......


5、監聽valueAnimator的變更

在valueAnimator裡面,我們必須得知道當前的值隨著插值器和typeEvaluator的估值,處於什麼養的一個狀態,所以我們必須監聽值得變化,因為值得變化一定伴隨著動畫的變化,不然怎麼叫值動畫呢?所以我們需要監聽動畫的變化來獲取估值,然後重新整理View重新根據新的值來繪製介面。
 public static interface AnimatorUpdateListener {
        void onAnimationUpdate(ValueAnimator animation);

    }


只要我們監聽了這個物件,那麼就可以從animation.getAnimatedValue獲取當前的估值。 大部分時候,僅僅對一個物件進行動畫,並不能滿足我們的需求,我們很經常需要對很多物件進行動畫的變化,這就涉及到集體的變換了。在android裡面,提供了AnimatorSet,來解決這個問題。


6、animatorSet

animatorSet是一個動畫集合,裡面的動畫都儲存在一個列表裡面,它們可以一起播放,也可以按次序播放,或者指定一定的時間間隔播放。總的來說,對它的操作就相當於對集合中所有的動畫的操作。比如呼叫animatorSet的start方法,等同於呼叫動畫集合所有的動畫的start方法。其中呼叫的次序是可以設定的。知道了這些,下面我們詳細瞭解一下這個類帶給了我們什麼。

6.1設定動畫集合的播放規則

這個播放規則不是動畫的播放規則(TypeEvaluator和interpolarter決定),而是動畫間的播放規則。主要有
public void playTogether(Animator... items)
public void playTogether(Collection<Animator> items)
public void playSequentially(Animator... items)
public void playSequentially(List<Animator> items) 
總共提供了兩種規則,一種是所有的動畫一起播放,另外一種是等前面一個播放完畢再接著播放下一個。 前面提到AnimatorSet只是一個動畫的集合,因此,Animator有的它都有,這裡就不再說了。


7、使用Animator實現粒子縮放效果

接下來,通過例項講解animator的使用。例子實現了一個同時對圓點集合進行縮放的效果,在縮放過程中,通過改變圓點的中心座標和半徑,來改變圓點的位置和大小,從而達到粒子縮放效果。 首先定義一個attrs.xml檔案。
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="ParticleView">
        <attr name="circleStartColor" format="color"></attr>
        <attr name="circleEndColor" format="color"></attr>
        <attr name="row" format="integer"></attr>
        <attr name="col" format="integer"></attr>
    </declare-styleable>
</resources>


然後定義小圓點的實體:
package cn.com.chinaweal.mypartical;

/**
 * Created by Myy on 2016/8/31.
 */
public class Circle {

    private float centerX;
    private float centerY;
    private float radius;

    public Circle(float centerX, float centerY, float radius) {
        this.centerX = centerX;
        this.centerY = centerY;
        this.radius = radius;
    }

    public float getCenterX() {
        return centerX;
    }

    public void setCenterX(float centerX) {
        this.centerX = centerX;
    }

    public float getCenterY() {
        return centerY;
    }

    public void setCenterY(float centerY) {
        this.centerY = centerY;
    }

    public float getRadius() {
        return radius;
    }

    public void setRadius(float radius) {
        this.radius = radius;
    }
}

設定圓點的變化規律:
package cn.com.chinaweal.mypartical;

import android.animation.TypeEvaluator;

/**
 * 圓點的屬性變化評估值,用於動畫過程獲取值
 * Created by Myy on 2016/8/31.
 */
public class CircleEvaluate implements TypeEvaluator<Circle> {
    /**
     * 估算值的表示式 result = x0 + t * (x1 - x0)
     * x0是startValue,x1是endValue,t是fraction
     *
     * @param fraction
     * @param startValue
     * @param endValue
     * @return
     */
    @Override
    public Circle evaluate(float fraction, Circle startValue, Circle endValue) {
        float circleX = startValue.getCenterX() + fraction * (endValue.getCenterX() - startValue.getCenterX());
        float circleY = startValue.getCenterY() + fraction * (endValue.getCenterY() - startValue.getCenterY());
        float radius = startValue.getRadius() + fraction * (endValue.getRadius() - startValue.getRadius());
        return new Circle(circleX, circleY, radius);
    }
}


接著自定義View實現可縮放的粒子效果:
package cn.com.chinaweal.mypartical;

import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.WindowManager;
import java.util.ArrayList;
import java.util.List;

/**
 * Created by Myy on 2016/8/31.
 */
public class ParticleView extends View {

    private Paint circlePaint;//小圓點畫筆
    private Context context;
    private int circleStartColor, circleEndColor;//小圓點起始顏色,終止顏色
    private int row = 10, col = 10;
    private Circle circleGroup[][],minCircleGroup[][];//圓點陣列,最小圓點陣列,這個可以用來在圓點動畫過程中指定最終值
    private float startWidth, startHeight, circleWidth, circleHeight,minCircleBound;
    private float circlePadding;//圓點的間距
    private float density;//畫素密度
    private float radius;//圓點半徑
    private float firstCircleX, firstCircleY;//第一個小圓點的中心座標
    private final static int START=0,ANIMATION_CIRCLE=1;//初始窗臺,圓點動畫狀態
    private int status=START;

    public ParticleView(Context context) {
        super(context);
        init(context);
    }

    public ParticleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray typeArray = context.obtainStyledAttributes(attrs, R.styleable.ParticleView);
        circleStartColor = typeArray.getColor(R.styleable.ParticleView_circleStartColor, Color.CYAN);
        circleEndColor = typeArray.getColor(R.styleable.ParticleView_circleEndColor, Color.RED);
        row = typeArray.getInt(R.styleable.ParticleView_row, 10);
        col = typeArray.getInt(R.styleable.ParticleView_col, 10);

        init(context);
    }

    private void init(Context context) {
        this.context=context;
        circlePaint = new Paint();
        circlePaint.setColor(circleStartColor);
        circleGroup = new Circle[row][col];
        minCircleGroup=new Circle[row][col];
        getDensity();

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if(status==START) {
            initCanvas(canvas);
        }
        if(status==ANIMATION_CIRCLE)
        {
            for (int i = 0; i < row; i++) {
                for (int j = 0; j < col; j++) {
                    canvas.drawCircle(circleGroup[i][j].getCenterX(), circleGroup[i][j].getCenterY(), circleGroup[i][j].getRadius(), circlePaint);
                }
            }
        }
    }

    /**
     * 初始化佈局
     * @param canvas
     */
    private void initCanvas(Canvas canvas) {
        startWidth = canvas.getWidth() - getPaddingLeft() - getPaddingRight();
        startHeight = canvas.getHeight() - getPaddingTop() - getPaddingBottom();
        circlePadding = 5 * density;
        circleWidth = (startWidth - circlePadding * (col - 1)) / col;
        circleHeight = (startHeight - circlePadding * (row - 1)) / row;
        minCircleBound=Math.min(circleWidth,circleHeight);//以最小的值為直徑,這樣才可以完全顯示所有的圓點
        radius = minCircleBound / 2;//獲取半徑
        firstCircleX = minCircleBound / 2;
        firstCircleY = circleHeight / 2;
        //設定漸變畫筆效果
        LinearGradient linearGradient = new LinearGradient(0, 0, startWidth, startHeight, circleStartColor, circleEndColor, Shader.TileMode.MIRROR);
        circlePaint.setShader(linearGradient);
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                circleGroup[i][j] = new Circle(firstCircleX + (minCircleBound + circlePadding) * i, firstCircleY + (minCircleBound + circlePadding) * j, radius);
                minCircleGroup[i][j] = new Circle(firstCircleX + (minCircleBound + circlePadding) * i+circlePadding*3, firstCircleY + (minCircleBound + circlePadding) * j+circlePadding*3, circlePadding);
                canvas.drawCircle(circleGroup[i][j].getCenterX(), circleGroup[i][j].getCenterY(), radius, circlePaint);
            }
        }
    }

    /**
     * 啟動動畫
     */

    public void startAnimation() {
        //false表示每個動畫都是用自己的插值器否則就是用共有的插值器
        status=ANIMATION_CIRCLE;
        final AnimatorSet animatorSet = new AnimatorSet();
        List<Animator> animatorList = new ArrayList<>();
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                //valueAnimator可以設定動畫屬性的值在某個區間內以某種方式進行加速或者減速
                final int tempRow=i;
                final int tempCol=j;
                final CircleEvaluate evaluate=new CircleEvaluate();
                ValueAnimator animator = ValueAnimator.ofObject(evaluate, circleGroup[i][j],minCircleGroup[i][j]);
                animator.setDuration(3000);
                animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation) {
                        Circle circle=(Circle)animation.getAnimatedValue();
                        circleGroup[tempRow][tempCol]=circle;
                        invalidate();
                    }
                });
                animatorList.add(animator);
            }
        }

        animatorSet.playTogether(animatorList);
        animatorSet.start();
        animatorSet.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {

            }

            @Override
            public void onAnimationEnd(Animator animation) {
                
            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });

    }

    /**
     * 獲取螢幕密度
     */
    private void getDensity() {
        DisplayMetrics displayMetrics = new DisplayMetrics();
        WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        windowManager.getDefaultDisplay().getMetrics(displayMetrics);
        density = displayMetrics.density;
    }

}

讀者在看這個例子的時候,關注點不要在哪些半徑直徑的東西,而是關注valueAnimator和circle之間的變化,以及這些變化如何引起view渲染的。

佈局檔案:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:weightSum="2"
    android:orientation="vertical"
    tools:context="cn.com.chinaweal.mypartical.MainActivity">

    <cn.com.chinaweal.mypartical.ParticleView
        android:id="@+id/particleView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="1"
        app:circleStartColor="#efefaa"
        app:circleEndColor="#aa11aa"
        android:background="#efefef"
        app:col="8"
        app:row="8" />
    <cn.com.chinaweal.mypartical.ParticleView
        android:id="@+id/particleView1"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="1"
        app:circleStartColor="#eaaefa"
        app:circleEndColor="#1133aa"
        app:col="10"
        app:row="10" />
</LinearLayout>

acitivtiy:
package cn.com.chinaweal.mypartical;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

    private ParticleView particleView;
    private ParticleView particleView1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.shader_layout);
        particleView = (ParticleView) findViewById(R.id.particleView);
        particleView1=(ParticleView)findViewById(R.id.particleView1);
    }

    @Override
    protected void onStart() {
        super.onStart();
        particleView.postDelayed(new Runnable() {
            @Override
            public void run() {
                particleView.startAnimation();
                particleView1.startAnimation();
            }
        }, 1000);
    }
}
效果圖: 開始:
進過一段時間:


如果讀者瞭解了,可以繼續研究文字的拖拽,矩形的變化,圖片的旋轉.......各種效果,綜合起來就可以達到很炫的動畫,可以用作起始頁或者引導頁作為歡迎動畫......

---------文章寫自:HyHarden---------