看到一個很好玩的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