Android動畫:一個等待動畫的製做過程

看到一個很好玩的gif等待動畫,記錄一下製做過程。php

先上圖,展現一下這gif。java

圖中四個空心圓,一個實心園,依次做規則雙星運動。android

三個晚上,目前已經已經實現了。又學到了很多東西,這幾天把博客寫完。算法

放個視頻看下效果canvas

 

先說一下思路,目前想到三種,一是自定義viewgroup,而後把小圓圈寫成自定義的view,用animator屬性動畫來控制小圓圈的移動;二是自定義view,用canvas不斷重繪來實現動畫效果。我選擇了第一種,第二種有空選另外一個動畫來實現,應該也不難,加油吧。數組

 

1、CircleView—小圓圈的製做app

在gif圖中,有四個空心圓,一個實心圓,由於沒有太多的東西,因此直接用canvas繪製便可。ide

CircleView有五個參數,Context,是不是空心的,空內心面的顏色(gif中的紅色),邊框的顏色(gif中的白色),邊框的寬度(單位是px);函數

PS:這裏能夠把strokeSize和circleSize設置成同樣的大小,效果就是全部的CircleView都是實心的了。動畫

CircleView的大小在onDraw方法裏獲取,由viewGroup來肯定,這一點在第二部分說。

package org.out.naruto.view;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.view.View;

import org.out.naruto.utils.MyPoint;

/**
 * Created by Hao_S on 2016/6/1.
 */

public class CircleView extends View {

    private static final String TAG = "CircleView";

    private boolean isHollow = true; // 是不是空心圓
    private int circleColor; // 顏色
    private int strokeColor; // 邊框顏色
    private int mSize = 0; // view大小
    private int strokeSize; // 邊框寬度,單位 px


    public CircleView(Context context) {
        super(context);
    }

    public CircleView(Context context, Boolean isHollow, int circleColor, int strokeColor, int strokeSize) {
        super(context);
        this.isHollow = isHollow;
        this.circleColor = circleColor;
        this.strokeColor = strokeColor;
        this.strokeSize = strokeSize;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mSize = this.getHeight();
        Paint paint = new Paint(); // 畫筆
        paint.setAntiAlias(true); // 抗鋸齒
        paint.setColor(strokeColor);
        canvas.drawCircle(mSize / 2, mSize / 2, mSize / 2, paint); // 四個參數,分別是x座標 y座標 半徑?? 畫筆
        if (isHollow) { // 若是是空心的,在裏面再繪製一個圓
            paint.setColor(this.circleColor);
            canvas.drawCircle(mSize / 2, mSize / 2, (mSize - mSize / (strokeSize * 2)) / 2, paint);
        }
    }

    /**
     * @param myPoint 包含xy座標的對象
     *                這就是具體讓小圓圈動起來的函數
     *                view.animate()函數是Android 3.1 提供的,返回的是ViewPropertyAnimator,簡單來講就是對animator的封裝。
     */

    public void setPoint(MyPoint myPoint) {

        this.animate().y(myPoint.getY()).x(myPoint.getX()).setDuration(0);

    }

}

canvas裏面的繪製函數我就不詳細解釋了,就是畫個圓 = = 

setPoint和後面的一塊兒解釋。

2、ViewGroup的製做

這裏我選擇繼承了FrameLayout,緣由很簡單:感受(認真臉)。PS,抽空去試試其餘的ViewGroup,應該會存在效率和資源上的差距。

這裏先列舉一下要肯定的屬性:ViewGroup的大小、CircleView的大小、CircleView之間的間距、CircleView的邊框顏色、CircleView的數量(未實現,由於數量不一樣動畫規律也不一樣)。

private Context context;

    private int viewHeight, viewWidth;

    private int viewColor = Color.RED; // ViewGroup裏面的背景色,也是空心CircleView裏面的顏色,默認紅色。
    private int circleSize = 100; // CircleView的大小,默認100像素。
    private int spacing = 50; // CircleView之間的間隔,默認50像素。
    private int strokeColor = Color.WHITE; // CircleView的圓形邊框顏色,默認白色。
    private boolean autoStart = false; // 是否自動執行動畫

    private int circleNum = 5; // CircleView的數量,默認5個。
    private CircleView[] circleViews; // 全部的CircleView
    private MyPoint[] myPoints; // 全部的座標點
    private CircleView targetView; // 那個實心的CircleView

首先在values文件夾下建立attrs.xml,規定好本身的屬性

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="WaitingView">
        <attr name="viewColor" format="color" />
        <attr name="strokeColor" format="color" />
        <attr name="viewSpacing" format="integer" />
        <attr name="circleNum" format="integer" />
        <attr name="circleSize" format="integer"/>
        <attr name="AutoStart" format="boolean" />
    </declare-styleable>

</resources>

而後在構造方法裏獲取這些值(算是初級自定義view要掌握的):

public WaitingView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.WaitingView, defStyleAttr, 0); // 搞清楚這些參數

        int num = a.getIndexCount();

        for (int i = 0; i < num; i++) {
            int attr = a.getIndex(i);
            switch (attr) {
                case R.styleable.WaitingView_viewColor:
                    this.viewColor = a.getColor(attr, viewColor);
                    break;
                case R.styleable.WaitingView_strokeColor:
                    this.strokeColor = a.getColor(attr, strokeColor);
                    break;
                case R.styleable.WaitingView_viewSpacing:
                    this.spacing = a.getInteger(attr, spacing);
                    break;
                case R.styleable.WaitingView_circleNum:
                    this.circleNum = a.getInteger(attr, circleNum);
                    break;
                case R.styleable.WaitingView_circleSize:
                    this.circleSize = a.getInt(attr, circleSize);
                    break;
                case R.styleable.WaitingView_AutoStart:
                    this.autoStart = a.getBoolean(attr, autoStart);
                    if (autoStart) {
                        Log.i(TAG, "autoStart is true");
                    }
                    break;
                case R.styleable.WaitingView_strokeSize:
                    int tempInt = a.getInteger(attr, strokeSize);
                    if (tempInt * 2 <= circleSize) {
                        strokeSize = tempInt;
                    }
                    break;
            }

        }

        a.recycle(); // 釋放資源

        circleViews = new CircleView[circleNum];
        myPoints = new MyPoint[circleNum];

        setWillNotDraw(false); // 聲明要調用onDraw方法。


    }

這裏要特別提一下構造方法中最後一個方法setWillNotDraw(),以前還在這裏卡了一下,由於背景要繪製顏色,因此在onDraw裏直接canvas.drawColor,結果發現不起做用(遞歸矇蔽ing)。後來查資料發現,原來是由於這是個ViewGroup,若是不在xml文件裏寫android:background = "color"的話,系統是不會調用onDraw方法的,由於ViewGroup背景默認透明啊。因此就要把WillNotDraw設置爲false。

自定義view屬性還有一種方法,不用配置attrs.xml,無心中發現的,由於我沒有使用這個方法,因此放個連接:

http://terryblog.blog.51cto.com/1764499/414884/

 

我是在onDraw方法裏獲取view的大小而後再添加CircleView,目前還不知道有什麼弊端,可是這樣就不用在以前的方法(執行順序:onMesure onLayout onDraw)用很複雜的方式判斷了,算是投機取巧?

private boolean first = true; // 用於標識只添加一次CircleView

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(viewColor);
        if (first) {
            viewHeight = this.getHeight();
            viewWidth = this.getWidth();
            creatCircle();
            first = false;
            if (autoStart)
                startAnim();
        }
    }

 

3、小圓圈添加到ViewGroup

gif圖中五個圓在一條水平線上,水平居中。

直接上代碼:

private void creatCircle() {
        int top = (viewHeight - circleSize) / 2; // view的上邊界距父View上邊界的距離,單位是px(下同)。ViewGroup的高與CircleView的高之差的一半。
        int left = (int) (viewWidth / 2 - ((circleNum / 2f) * circleSize + (circleNum - 1) / 2f * spacing));
        // int left = view左邊界距父view左邊界的距離,這裏先算出了最左邊view的數值,看着這麼長,實在不想看。
        // 總之就是,ViewGroup的寬的一半,減去一半數量的CircleView的寬和一半數量的CircleView間距,能理解級理解,不能理解我也沒辦法了。
        int increats = circleSize + spacing; // left的增長量,每次增長一個CircleView的寬度和一個間距。

        for (int i = 0; i < circleNum; i++) {
            CircleView circleView = new CircleView(context, i != 0, viewColor, strokeColor); // new出來,除了第一個是實心圓,其餘都是空心的。
            circleViews[i] = circleView; // 添加到數組中,動畫執行的時候要用。
            FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(circleSize, circleSize); // 這裏就是肯定CircleView大小的地方。
            int realLeft = left + i * increats; // 實際的left值
            layoutParams.setMargins(realLeft, top, 0, 0); // 設置座標
            MyPoint myPoint = new MyPoint(realLeft, top); // 把該座標保存起來,動畫執行的時候會用到。
            myPoints[i] = myPoint;
            circleView.setLayoutParams(layoutParams);
            addView(circleView); // 添加
        }

        this.targetView = circleViews[0]; // 那個白色的實心圓

    }

2016/6/3 17:45 先寫到這裏,有時間繼續更。

4、小圓圈的運動

大部分說明都寫在註釋裏了 = = 這裏就再也不重複了

/**
     * 先說一下動畫規律吧,實心白色圓不斷依次和剩下的空心圓作半個雙星運動。
     * 每次一輪運動結束後,最早在前面的空心圓到了最後,就像一個循環隊列同樣。
     * 可是這裏我沒有使用隊列來實現,而是使用了數組,利用模除運算來計算出運動規律,這一點多是這動畫的短板,改進以後估計會解決自適應CircleView數量問題。
     * 2016/6/4 1:00 解決了動畫自適應CircleView的數量問題,是我以前的寫法有點死板。
     */

    private int position = 0; // CircleView動畫執行次數
    private int duration = 500; // 一次動畫的持續時間
    private AnimatorSet animatorSet; // AnimatorSet,使動畫同時進行
    private ObjectAnimator targetAnim, otherAnim; // 兩個位移屬性動畫

    public void startAnim() {

        animatorSet = new AnimatorSet();
        // 添加一個監聽,一小段動畫結束以後當即開啓下一小段動畫
        // 這裏
        animatorSet.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                startAnim();
            }
        });

        int targetPosition = position % circleNum; // 這是實心白色CircleView所在次序,變化規律 0..(circleNum-1)

        int otherPosition = (position + 1) % circleNum; // 即將和實心白色CircleView做圓周運動的空心圓所在次序,變化規律 1..(circleNum-1)0

        int tempInt = (position + 1) % (circleNum - 1); // 這是除掉實心白色圓以後,剩下空心圓的次序,變化規律 1..(circleNum-1)

        CircleView circleView = circleViews[tempInt == 0 ? (circleNum - 1) : tempInt]; // 獲取即將和實心白色圓做圓周運動的CircleView對象

        MyPoint targetPoint = myPoints[targetPosition]; // 實心白色圓實際的座標點

        MyPoint otherPoint = myPoints[otherPosition]; // 將要執行動畫的空心圓座標點

        PointEvaluator targetPointEvaluator, otherPointEvaluator; // 座標計算對象

        // 這裏有三種狀況,第一種就是實心圓運動到了最後,和第一個空心圓交換
        // 第二種就是實心圓在上面,空心圓在下面的交換動畫
        // 第三種是實心圓在下面,空心圓在上面的交換動畫,除了第一種以外,其餘都是實心圓往右移動,空心圓往左移動。
        if (targetPosition == circleNum - 1) {
            targetPointEvaluator = new PointEvaluator(MoveType.Left, MoveType.Down);
            otherPointEvaluator = new PointEvaluator(MoveType.Right, MoveType.Up);
        } else if ((targetPosition % 2) == 0) {
            targetPointEvaluator = new PointEvaluator(MoveType.Right, MoveType.Up);
            otherPointEvaluator = new PointEvaluator(MoveType.Left, MoveType.Down);
        } else {
            targetPointEvaluator = new PointEvaluator(MoveType.Right, MoveType.Down);
            otherPointEvaluator = new PointEvaluator(MoveType.Left, MoveType.Up);
        }

        // 建立ObjectAnimator對象
        // 第一個參數就是要作運動的view
        // 第二個是要調用的方法,能夠看看CircleView裏面會有一個setPoint方法,這裏會根據你填入的參數去尋找同名的set方法。
        // 第三個是自定義的數值計算器,會根據運動狀態的程度計算相應的結果
        // 第四個和第五個參數是運動初始座標和運動結束座標。
        targetAnim = ObjectAnimator.ofObject(this.targetView, "Point", targetPointEvaluator, targetPoint, otherPoint);

        otherAnim = ObjectAnimator.ofObject(circleView, "Point", otherPointEvaluator, targetPoint, otherPoint);

        animatorSet.playTogether(targetAnim, otherAnim); // 動畫同時運行
        animatorSet.setDuration(duration); // 設置持續時間
        animatorSet.start(); // 執行動畫
        position++;
    }

明天更新詳細說明自定義動畫值計算對象的寫法,先放代碼,這裏是高中圓周運動知識,具體動畫座標是由運動角度和正弦餘弦計算得出。

/**
     * 枚舉型標識動畫運動類型
     */
    public enum MoveType {
        Left, Right, Up, Down
    }

    /**
     * 運動算法:
     * 根據作雙星運動的兩個CircleView的座標,首先求出兩座標的中心點做爲運動圓心。
     * 根據運動的角度,結合cos與sin分別算出x軸與y軸的數值變化,而後返回當前運動座標。
     * x = (運動中心x座標 ± Cos(運動角度)X 運動半徑);
     * y = (運動中心y座標 ± Sin(運動角度)X 運動半徑);
     */
    private class PointEvaluator implements TypeEvaluator {
        private MoveType LeftOrRight, UpOrDown;

        public PointEvaluator(MoveType LeftOrRight, MoveType UpOrDown) {
            this.LeftOrRight = LeftOrRight;
            this.UpOrDown = UpOrDown;
        }

        @Override
        public Object evaluate(float fraction, Object startValue, Object endValue) {

            MyPoint startPoint = (MyPoint) startValue; // 運動開始時的座標
            MyPoint endPoint = (MyPoint) endValue; // 運動結束時的座標
            int R = (int) (Math.abs(startPoint.getX() - endPoint.getX()) / 2); // 運動圓周的半徑
            double r = Math.PI * fraction; // 當前運動角度
            int circleX = (int) ((startPoint.getX() + endPoint.getX()) / 2); // 運動圓心座標X
            int circleY = (int) endPoint.getY();// 運動圓心座標Y
            float x = 0, y = 0; // 當前運動座標

            switch (LeftOrRight) {
                case Left:
                    x = (float) (circleX + Math.cos(r) * R);
                    break;
                case Right:
                    x = (float) (circleX - Math.cos(r) * R);
                    break;
            }

            switch (UpOrDown) {
                case Up:
                    y = (float) (circleY - Math.sin(r) * R);
                    break;
                case Down:
                    y = (float) (circleY + Math.sin(r) * R);
                    break;
            }

            MyPoint myPoint = new MyPoint(x, y);

            return myPoint;
        }
    }

輔助類MyPoint

package org.out.naruto.utils;

/**
 * Created by Hao_S on 2016/6/2.
 */

public class MyPoint {
    private float x, y;


    public MyPoint(float x, float y) {
        this.x = x;
        this.y = y;
    }


    public float getY() {
        return y;
    }

    public float getX() {
        return x;
    }
}

 

最後感謝GQ、ZSJ學長和我一塊兒找bug,衷心祝畢業愉快。

 

參考博客:

http://blog.csdn.net/lmj623565791/article/details/24555655

http://blog.csdn.net/guolin_blog/article/details/43816093

http://blog.csdn.net/leehong2005/article/details/7299471

相關文章
相關標籤/搜索