Android自定義動畫系列六,今天來分享第六個(gè)自定義Loading動畫(ElasticBallBuilder),我給他起了一個(gè)逼格很高的名字,叫顫抖吧!球球,這個(gè)動畫讓我絞盡腦汁,算數(shù)算的暈乎乎,不過結(jié)果還是很滿意的,還是老規(guī)矩先介紹,效果圖在最后面。
實(shí)現(xiàn)效果圖在最后,GIF有點(diǎn)大,手機(jī)流量請三思。
??這里我想問大家一個(gè)問題,這個(gè)最終效果圖,是放在文章的 開頭 好呢?還是放在 結(jié)尾 好呢?
大家評論里面給個(gè)建議吧。謝謝了。
介紹
首先依舊是聲明,做這些動畫的初衷是為了學(xué)習(xí)和分享,所以希望大家可以指點(diǎn)錯誤,讓我更好的進(jìn)步。(系列加載動畫的截止時(shí)間:我放棄的時(shí)候)。
上一個(gè)動畫鏈接:Android自定義加載動畫-PacMan
正文
參數(shù)變量初始化,用處我都在代碼里寫了注釋了,不懂得大家也可以問,有什么不對的,也希望大家?guī)兔χ赋?,謝謝了。如下:
//動畫間隔時(shí)間
private static final long DURATION_TIME = 333;
//最終階段
private static final int FINAL_STATE = 2;
//小球共5個(gè)位置
private static final int SUM_POINT_POS = 5;
//貝塞爾曲線常量
private static final float PROP_VALUE = 0.551915024494f;
//小球點(diǎn)集合
private final LinkedList<CirclePoint> mBallPoints = new LinkedList<>();
//背景圓集合
private final LinkedList<CirclePoint> mBGCircles = new LinkedList<>();
private Paint mPaint;
private float mBallR;
private Path mPath;
//當(dāng)前動畫階段
private int mCurrAnimatorState = 0;
//每個(gè)小球的偏移量
private float mCanvasTranslateOffset;
//當(dāng)前狀態(tài)是否翻轉(zhuǎn)
private boolean mIsReverse = false;
//當(dāng)前小球的位置
private int mCurrPointPos = 0;
首先初始化參數(shù),mBallR 為小球半徑,mCanvasTranslateOffset 為小球之間的間距,mPath 路徑,其它如注釋:
@Override
protected void initParams(Context context)
{
mBallR = getAllSize() / SUM_POINT_POS;
mCanvasTranslateOffset = getIntrinsicWidth() / SUM_POINT_POS;
mPath = new Path();
initPaint(5);
initPoints();
initBGPoints();
}
/**
* 背景圓點(diǎn)初始化
*/
private void initBGPoints()
{
float centerX = getViewCenterX();
float centerY = getViewCenterY();
CirclePoint p_0 = new CirclePoint(centerX - mCanvasTranslateOffset * 2, centerY);
CirclePoint p_1 = new CirclePoint(centerX - mCanvasTranslateOffset, centerY);
CirclePoint p_2 = new CirclePoint(centerX, centerY);
CirclePoint p_3 = new CirclePoint(centerX + mCanvasTranslateOffset, centerY);
CirclePoint p_4 = new CirclePoint(centerX + mCanvasTranslateOffset * 2, centerY);
p_0.setEnabled(false);//默認(rèn)第一個(gè)圓不顯示
mBGCircles.add(p_0);
mBGCircles.add(p_1);
mBGCircles.add(p_2);
mBGCircles.add(p_3);
mBGCircles.add(p_4);
}
這里很重要,很重要,重要。
這里初始化的是小球的各個(gè)點(diǎn)坐標(biāo),這里的小球是通過貝塞爾曲線繪制了,為了后面方便動畫操作,所以球的繪制就會相對的繁瑣了。
貝塞爾曲線畫球的原理,如下:

具體標(biāo)注點(diǎn),請對照我注釋中的P0~P11點(diǎn),如下:
/**
* p10 p9 p8
* ------ ------
* p11 p7
* | |
* | |
* p0 | (0,0) | p6
* | |
* | |
* p1 p5
* ------ ------
* p2 p3 p4
*/
private void initPoints()
{
float centerX = getViewCenterX();
float centerY = getViewCenterY();
CirclePoint p_0 = new CirclePoint(centerX - mBallR, centerY);
mBallPoints.add(p_0);
CirclePoint p_1 = new CirclePoint(centerX - mBallR, centerY + mBallR * PROP_VALUE);
mBallPoints.add(p_1);
CirclePoint p_2 = new CirclePoint(centerX - mBallR * PROP_VALUE, centerY + mBallR);
mBallPoints.add(p_2);
CirclePoint p_3 = new CirclePoint(centerX, centerY + mBallR);
mBallPoints.add(p_3);
CirclePoint p_4 = new CirclePoint(centerX + mBallR * PROP_VALUE, centerY + mBallR);
mBallPoints.add(p_4);
CirclePoint p_5 = new CirclePoint(centerX + mBallR, centerY + mBallR * PROP_VALUE);
mBallPoints.add(p_5);
CirclePoint p_6 = new CirclePoint(centerX + mBallR, centerY);
mBallPoints.add(p_6);
CirclePoint p_7 = new CirclePoint(centerX + mBallR, centerY - mBallR * PROP_VALUE);
mBallPoints.add(p_7);
CirclePoint p_8 = new CirclePoint(centerX + mBallR * PROP_VALUE, centerY - mBallR);
mBallPoints.add(p_8);
CirclePoint p_9 = new CirclePoint(centerX, centerY - mBallR);
mBallPoints.add(p_9);
CirclePoint p_10 = new CirclePoint(centerX - mBallR * PROP_VALUE, centerY - mBallR);
mBallPoints.add(p_10);
CirclePoint p_11 = new CirclePoint(centerX - mBallR, centerY - mBallR * PROP_VALUE);
mBallPoints.add(p_11);
}
/**
* 初始化畫筆
*/
private void initPaint(float lineWidth)
{
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mPaint.setStrokeWidth(lineWidth);
mPaint.setColor(Color.BLACK);
mPaint.setDither(true);
mPaint.setFilterBitmap(true);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setStrokeJoin(Paint.Join.ROUND);
}
以下,開始了繪制工作了,drawBG() 繪制背景,在這里就不多介紹了;我們看下 drawBall() 方法,通過路徑貝塞爾曲線繪制并連接一開始初始化的12個(gè)點(diǎn)坐標(biāo)形成路徑,最終繪制到畫布上。
@Override
protected void onDraw(Canvas canvas)
{
drawBG(canvas);
drawBall(canvas);
}
/**
* 繪制小球
*
* @param canvas
*/
private void drawBall(Canvas canvas)
{
canvas.save();
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
float offsetX = mBGCircles.size() / 2 * mCanvasTranslateOffset;
canvas.translate(-offsetX + mCanvasTranslateOffset * mCurrPointPos, 0);
mPath.reset();
mPath.moveTo(mBallPoints.get(0).getX(), mBallPoints.get(0).getY());
mPath.cubicTo(mBallPoints.get(1).getX(), mBallPoints.get(1).getY(), mBallPoints.get(2).getX(), mBallPoints.get(2).getY(), mBallPoints.get(3).getX(), mBallPoints.get(3).getY());
mPath.cubicTo(mBallPoints.get(4).getX(), mBallPoints.get(4).getY(), mBallPoints.get(5).getX(), mBallPoints.get(5).getY(), mBallPoints.get(6).getX(), mBallPoints.get(6).getY());
mPath.cubicTo(mBallPoints.get(7).getX(), mBallPoints.get(7).getY(), mBallPoints.get(8).getX(), mBallPoints.get(8).getY(), mBallPoints.get(9).getX(), mBallPoints.get(9).getY());
mPath.cubicTo(mBallPoints.get(10).getX(), mBallPoints.get(10).getY(), mBallPoints.get(11).getX(), mBallPoints.get(11).getY(), mBallPoints.get(0).getX(), mBallPoints.get(0).getY());
canvas.drawPath(mPath, mPaint);
canvas.restore();
}
/**
* 繪制背景圓
*
* @param canvas
*/
private void drawBG(Canvas canvas)
{
canvas.save();
mPaint.setStyle(Paint.Style.STROKE);
for (CirclePoint point : mBGCircles)
{
point.draw(canvas, mBallR, mPaint);
}
canvas.restore();
}
這里是不同階段對應(yīng)的不同偏移量賦值,前三個(gè)階段是針對順序移動的操作,后三個(gè)階段是對應(yīng)的逆序時(shí)所需要的賦值操作,所有位移操作都是對小球的12點(diǎn)進(jìn)行X軸方向的處理。
@Override
protected void computeUpdateValue(ValueAnimator animation, @FloatRange(from = 0.0, to = 1.0) float animatedValue)
{
float offset = mCanvasTranslateOffset;
int currState = mIsReverse ? mCurrAnimatorState + 3 : mCurrAnimatorState;
switch (currState)
{
case 0:
animation.setDuration(DURATION_TIME);
animation.setInterpolator(new AccelerateInterpolator());
mBallPoints.get(5).setOffsetX(animatedValue * offset);
mBallPoints.get(6).setOffsetX(animatedValue * offset);
mBallPoints.get(7).setOffsetX(animatedValue * offset);
break;
case 1:
animation.setDuration(DURATION_TIME + 111);
animation.setInterpolator(new DecelerateInterpolator());
mBallPoints.get(2).setOffsetX(animatedValue * offset);
mBallPoints.get(3).setOffsetX(animatedValue * offset);
mBallPoints.get(4).setOffsetX(animatedValue * offset);
mBallPoints.get(8).setOffsetX(animatedValue * offset);
mBallPoints.get(9).setOffsetX(animatedValue * offset);
mBallPoints.get(10).setOffsetX(animatedValue * offset);
break;
case 2:
animation.setDuration(DURATION_TIME + 333);
animation.setInterpolator(new BounceInterpolator());
mBallPoints.get(0).setOffsetX(animatedValue * offset);
mBallPoints.get(1).setOffsetX(animatedValue * offset);
mBallPoints.get(11).setOffsetX(animatedValue * offset);
break;
case 3:
animation.setDuration(DURATION_TIME);
animation.setInterpolator(new AccelerateInterpolator());
mBallPoints.get(0).setOffsetX((1 - animatedValue) * offset);
mBallPoints.get(1).setOffsetX((1 - animatedValue) * offset);
mBallPoints.get(11).setOffsetX((1 - animatedValue) * offset);
break;
case 4:
animation.setDuration(DURATION_TIME + 111);
animation.setInterpolator(new DecelerateInterpolator());
mBallPoints.get(2).setOffsetX((1 - animatedValue) * offset);
mBallPoints.get(3).setOffsetX((1 - animatedValue) * offset);
mBallPoints.get(4).setOffsetX((1 - animatedValue) * offset);
mBallPoints.get(8).setOffsetX((1 - animatedValue) * offset);
mBallPoints.get(9).setOffsetX((1 - animatedValue) * offset);
mBallPoints.get(10).setOffsetX((1 - animatedValue) * offset);
break;
case 5:
animation.setDuration(DURATION_TIME + 333);
animation.setInterpolator(new BounceInterpolator());
mBallPoints.get(5).setOffsetX((1 - animatedValue) * offset);
mBallPoints.get(6).setOffsetX((1 - animatedValue) * offset);
mBallPoints.get(7).setOffsetX((1 - animatedValue) * offset);
break;
}
}
在下面的方法中,對小球動畫的各個(gè)階段進(jìn)行分布,各個(gè)點(diǎn)的偏移量進(jìn)行重置,以及順序倒序移動的切換邏輯進(jìn)行判斷,并且對背景圓也做了關(guān)聯(lián)處理。
@Override
public void onAnimationRepeat(Animator animation)
{
if (++mCurrAnimatorState > FINAL_STATE)
{//還原到第一階段
mCurrAnimatorState = 0;
/* 小球位置改變 */
if (mIsReverse)
{//倒序
mCurrPointPos--;
}
else
{//順序
mCurrPointPos++;
}
/* 重置并翻轉(zhuǎn)動畫過程 */
if (mCurrPointPos >= SUM_POINT_POS - 1)
{//倒序
mIsReverse = true;
mCurrPointPos = SUM_POINT_POS - 2;//I Don't Know
for (int i = 0; i < mBGCircles.size(); i++)
{
CirclePoint point = mBGCircles.get(i);
if (i == mBGCircles.size() - 1)
{
point.setEnabled(true);
}
else
{
point.setEnabled(false);
}
}
}
else if (mCurrPointPos < 0)
{//順序
mIsReverse = false;
mCurrPointPos = 0;
for (int i = 0; i < mBGCircles.size(); i++)
{
CirclePoint point = mBGCircles.get(i);
if (i == 0)
{
point.setEnabled(false);
}
else
{
point.setEnabled(true);
}
}
}
//每個(gè)階段恢復(fù)狀態(tài),以及對背景圓的控制
if (mIsReverse)
{//倒序
//恢復(fù)狀態(tài)
for (CirclePoint point : mBallPoints)
{
point.setOffsetX(mCanvasTranslateOffset);
}
mBGCircles.get(mCurrPointPos + 1).setEnabled(true);
}
else
{//順序
//恢復(fù)狀態(tài)
for (CirclePoint point : mBallPoints)
{
point.setOffsetX(0);
}
mBGCircles.get(mCurrPointPos).setEnabled(false);
}
}
}
圓點(diǎn)的內(nèi)部類,封裝了此動畫中所涉及到的點(diǎn)和球的參數(shù)信息(小球的點(diǎn)與背景球都是復(fù)用此類的)
/**
* 圓點(diǎn)內(nèi)部類
*/
private class CirclePoint
{
private final float mX;
private final float mY;
private float mOffsetX = 0;
private float mOffsetY = 0;
private boolean mEnabled = true;
CirclePoint(float x, float y)
{
mX = x;
mY = y;
}
float getX()
{
return mX + mOffsetX;
}
float getY()
{
return mY + mOffsetY;
}
void setOffsetX(float offsetX)
{
mOffsetX = offsetX;
}
void setOffsetY(float offsetY)
{
mOffsetY = offsetY;
}
public void setEnabled(boolean enabled)
{
mEnabled = enabled;
}
void draw(Canvas canvas, float r, Paint paint)
{
if (this.mEnabled)
{
canvas.drawCircle(this.getX(), this.getY(), r, paint);
}
}
}
總結(jié)
小伙伴們,介紹就到這里了,要是想看更多細(xì)節(jié),可以前往文章最下面的Github鏈接,如果大家覺得ok的話,希望能給個(gè)喜歡,最渴望的是在Github上給個(gè)star。謝謝了。
如果大家有什么更好的方案,或者想要實(shí)現(xiàn)的加載效果,可以給我留言或者私信我,我會想辦法實(shí)現(xiàn)出來給大家。謝謝支持。
演示

Github:zyao89/ZCustomView
作者:Zyao89;轉(zhuǎn)載請保留此行,謝謝;
個(gè)人博客:https://zyao89.cn