Android自定義動畫系列六,今天來分享第六個自定義Loading動畫(ElasticBallBuilder),我給他起了一個逼格很高的名字,叫顫抖吧!球球
,這個動畫讓我絞盡腦汁,算數算的暈乎乎,不過結果還是很滿意的,還是老規矩先介紹,效果圖在最后面。
實現效果圖在最后,GIF有點大,手機流量請三思。
??這里我想問大家一個問題,這個最終效果圖,是放在文章的 開頭 好呢?還是放在 結尾 好呢?
大家評論里面給個建議吧。謝謝了。
介紹
首先依舊是聲明,做這些動畫的初衷是為了學習和分享,所以希望大家可以指點錯誤,讓我更好的進步。(系列加載動畫的截止時間:我放棄的時候)。
上一個動畫鏈接:Android自定義加載動畫-PacMan
正文
參數變量初始化,用處我都在代碼里寫了注釋了,不懂得大家也可以問,有什么不對的,也希望大家幫忙指出,謝謝了。如下:
//動畫間隔時間
private static final long DURATION_TIME = 333;
//最終階段
private static final int FINAL_STATE = 2;
//小球共5個位置
private static final int SUM_POINT_POS = 5;
//貝塞爾曲線常量
private static final float PROP_VALUE = 0.551915024494f;
//小球點集合
private final LinkedList<CirclePoint> mBallPoints = new LinkedList<>();
//背景圓集合
private final LinkedList<CirclePoint> mBGCircles = new LinkedList<>();
private Paint mPaint;
private float mBallR;
private Path mPath;
//當前動畫階段
private int mCurrAnimatorState = 0;
//每個小球的偏移量
private float mCanvasTranslateOffset;
//當前狀態是否翻轉
private boolean mIsReverse = false;
//當前小球的位置
private int mCurrPointPos = 0;
首先初始化參數,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();
}
/**
* 背景圓點初始化
*/
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);//默認第一個圓不顯示
mBGCircles.add(p_0);
mBGCircles.add(p_1);
mBGCircles.add(p_2);
mBGCircles.add(p_3);
mBGCircles.add(p_4);
}
這里很重要,很重要,重要。
這里初始化的是小球的各個點坐標,這里的小球是通過貝塞爾曲線繪制了,為了后面方便動畫操作,所以球的繪制就會相對的繁瑣了。
貝塞爾曲線畫球的原理,如下:
具體標注點,請對照我注釋中的P0~P11點,如下:
/**
* 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個點坐標形成路徑,最終繪制到畫布上。
@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();
}
這里是不同階段對應的不同偏移量賦值,前三個階段是針對順序移動的操作,后三個階段是對應的逆序時所需要的賦值操作,所有位移操作都是對小球的12點進行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;
}
}
在下面的方法中,對小球動畫的各個階段進行分布,各個點的偏移量進行重置,以及順序倒序移動的切換邏輯進行判斷,并且對背景圓也做了關聯處理。
@Override
public void onAnimationRepeat(Animator animation)
{
if (++mCurrAnimatorState > FINAL_STATE)
{//還原到第一階段
mCurrAnimatorState = 0;
/* 小球位置改變 */
if (mIsReverse)
{//倒序
mCurrPointPos--;
}
else
{//順序
mCurrPointPos++;
}
/* 重置并翻轉動畫過程 */
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);
}
}
}
//每個階段恢復狀態,以及對背景圓的控制
if (mIsReverse)
{//倒序
//恢復狀態
for (CirclePoint point : mBallPoints)
{
point.setOffsetX(mCanvasTranslateOffset);
}
mBGCircles.get(mCurrPointPos + 1).setEnabled(true);
}
else
{//順序
//恢復狀態
for (CirclePoint point : mBallPoints)
{
point.setOffsetX(0);
}
mBGCircles.get(mCurrPointPos).setEnabled(false);
}
}
}
圓點的內部類,封裝了此動畫中所涉及到的點和球的參數信息(小球的點與背景球都是復用此類的)
/**
* 圓點內部類
*/
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);
}
}
}
總結
小伙伴們,介紹就到這里了,要是想看更多細節,可以前往文章最下面的Github鏈接,如果大家覺得ok的話,希望能給個喜歡,最渴望的是在Github上給個star。謝謝了。
如果大家有什么更好的方案,或者想要實現的加載效果,可以給我留言或者私信我,我會想辦法實現出來給大家。謝謝支持。
演示
Github:zyao89/ZCustomView
作者:Zyao89;轉載請保留此行,謝謝;
個人博客:https://zyao89.cn