一個絢麗的downloading動效分析與實現!

GAStudio發表於2016-12-18

閒逛之餘,看到一個不錯的downloading動效,這個動效用CJJ的話說難度還好,但本人覺得還比較靈動、帶感、俏皮、有新意,好了話不多說,我們們先來擼一張高清無碼gif圖:

一個絢麗的downloading動效分析與實現!

擼完,我們可以將整個動效簡單劃分為以下流程:
1.BeforeProgress(顯示進度前);
2.InProgress(顯示進度中);
3.Failed(失敗動畫);
4.Done(完成動畫);

下面我們們一起對以上流程進行分析與實現;


1.BeforeProgress(顯示進度前):

同樣,我們們一起擼一下第一部分高清無碼gif圖:

一個絢麗的downloading動效分析與實現!

通過觀察,我們可以將以上動畫分割為以下幾個內容:
1.圓形背景和下載剪頭整體縮放;
2.圓形背景逐步鏤空(縮放到一定階段,內部鏤空圓不斷擴大);
3.圓形背景變為一條直線,並伴隨箭頭些許上移;
4.直線上下震盪及下載箭頭(Arrow)變承載進度文字的線框形態;

1.1. 圓形背景和下載剪頭整體縮放:

這裡面,圓形背景和整體的縮放好說,稍顯麻煩的是下載箭頭,由於後面箭頭還需要形變為承載進度文字的線框,所以丟掉你使用圖片的小想法,我們們一起用path勾一個活潑的小箭頭:

// move to bottom center
mArrowPath.moveTo(halfArrowWidth, 0);
// rect bottom left edge
mArrowPath.lineTo(rectPaddingLeft, 0);
// rect left edge
mArrowPath.lineTo(rectPaddingLeft, rectHeight);
// tri bottom left edge
mArrowPath.lineTo(triPaddingLeft, rectHeight);
// tri left edge
mArrowPath.lineTo(halfArrowWidth, arrowHeight);
// tri right edge
mArrowPath.lineTo(arrowWidth - triPaddingLeft, rectHeight);
// tri bottom right edge
mArrowPath.lineTo(arrowWidth - rectPaddingLeft, rectHeight);
// rect right edge
mArrowPath.lineTo(arrowWidth - rectPaddingLeft, 0);
// rect right bottom edge
mArrowPath.lineTo(halfArrowWidth, 0);複製程式碼

箭頭OK了,圓形背景和整體的縮放就不再細說,只需要canvas.drawCircle()和使用ValueAnimator動態改變canvas縮放比例即可,so easy!

後面箭頭需要形變為承載進度文字的線框,通過觀察,可以看到線框的4個角是圓角。由於使用path勾勒,實現圓角線框大致有以下幾種方案:

1.使用path的quadTo()以二次貝塞爾曲線連線;
2.使用path的arcTo()以圓弧形式連線;
3.使用path中addArc()新增一段圓;
4.使用paint的setPathEffect設定PathEffect為ConnerPathEffect;

本人最終採用第四種方式進行實現;

一個絢麗的downloading動效分析與實現!

1.2.圓形背景逐步鏤空(縮放到一定階段,內部鏤空圓不斷擴大):

一個絢麗的downloading動效分析與實現!

擼完上圖,我們可看到,圓形背景由實心圓變換至一個圓環,最終消失,此處我們可以想到如下方案:

1.直接採用背景的顏色,在裡面畫實心圓(需要提前知道背景顏色並且背景只能為純色);
2.外面深色的圓直接是圓環,然後通過調整圓的半徑及paint的strokeWidth實現;
3.直接採用混合模式(Xfermode),圓形背景中混合掉內圓部分;

第一種方案太挫,帥氣逼人的GAStudio哥肯定不會考慮,本文采用混合模式方案,關鍵程式碼如下:

int layoutCont = canvas.saveLayer(mCircleRectF, mDefaultPaint, Canvas.ALL_SAVE_FLAG);
mDefaultPaint.setColor(mLoadingCircleBackColor);
canvas.drawCircle(mCircleRectF.centerX(), mCircleRectF.centerY(), mCircleRadius, mDefaultPaint);

mDefaultPaint.setXfermode(mXfermode);
// draw bg circle 2
int innerCircleRadius = (int) (mCircleRadius * innerCircleScalingFactor);
canvas.drawCircle(mCircleRectF.centerX(), mCircleRectF.centerY(), innerCircleRadius, mDefaultPaint);
mDefaultPaint.setXfermode(null);
canvas.restoreToCount(layoutCont);複製程式碼

1.3.圓形背景變為一條直線,並伴隨箭頭些許上移:

一個絢麗的downloading動效分析與實現!

這個部分相比前面兩步稍顯複雜,需要將圓環如絲般順滑的變換成直線,並隨之上線震盪,該過程拆解圖如下:
一個絢麗的downloading動效分析與實現!

對於這個過程,GAStudio哥採用兩條三階貝塞爾曲線對初期的圓環、中間部分的曲線、最終的直線進行模擬;

為了便於理解,抽象出四個核心狀態,過程圖解如下:

1.完整圓形狀態:

一個絢麗的downloading動效分析與實現!

2.延展開來狀態:
一個絢麗的downloading動效分析與實現!

3.橫向鋪開狀態:
一個絢麗的downloading動效分析與實現!

4.直線狀態:
一個絢麗的downloading動效分析與實現!

更新path核心邏輯如下:

private void updateCircleToLinePath(Path linePath, int circleDiameter, float normalizedTime) {
    if (linePath == null) {
        return;
    }
    int index = 0;
    float adjustNormalizedTime = 0;
    if (normalizedTime <= CIRCLE_TO_LINE_SEASONS[1]) {
        adjustNormalizedTime = normalizedTime / CIRCLE_TO_LINE_SEASONS[1];
    } else if (normalizedTime < CIRCLE_TO_LINE_SEASONS[2]) {
        index = 1;
        adjustNormalizedTime = (normalizedTime - CIRCLE_TO_LINE_SEASONS[1])
                / (CIRCLE_TO_LINE_SEASONS[2] - CIRCLE_TO_LINE_SEASONS[1]);
    } else {
        index = 2;
        adjustNormalizedTime = (normalizedTime - CIRCLE_TO_LINE_SEASONS[2])
                / (CIRCLE_TO_LINE_SEASONS[3] - CIRCLE_TO_LINE_SEASONS[2]);
    }

    // the path bounds width
    int boundWidth = (int) (((CIRCLE_TO_LINE_WIDTH_FACTOR[index + 1]
            - CIRCLE_TO_LINE_WIDTH_FACTOR[index])
            * adjustNormalizedTime + CIRCLE_TO_LINE_WIDTH_FACTOR[index]) * circleDiameter);

    // the distance of cubic line1' x1 to cubic line2's x2
    int adjustBoundWidth = boundWidth;
    if (normalizedTime <= CIRCLE_TO_LINE_SEASONS[1]) {
        adjustBoundWidth = (int) (boundWidth * adjustNormalizedTime);
    }

    // the path bounds height
    int boundHeight = (int) (((CIRCLE_TO_LINE_HEIGHT_FACTOR[index + 1]
            - CIRCLE_TO_LINE_HEIGHT_FACTOR[index])
            * adjustNormalizedTime + CIRCLE_TO_LINE_HEIGHT_FACTOR[index]) * circleDiameter);

    // calculate the four points
    float firstControlXFactor = (CIRCLE_TO_LINE_FST_CON_X_FACTOR[index + 1]
            - CIRCLE_TO_LINE_FST_CON_X_FACTOR[index])
            * adjustNormalizedTime + CIRCLE_TO_LINE_FST_CON_X_FACTOR[index];
    float firstControlYFactor = (CIRCLE_TO_LINE_FST_CON_Y_FACTOR[index + 1]
            - CIRCLE_TO_LINE_FST_CON_Y_FACTOR[index])
            * adjustNormalizedTime + CIRCLE_TO_LINE_FST_CON_Y_FACTOR[index];
    float secondControlXFactor = (CIRCLE_TO_LINE_SEC_CON_X_FACTOR[index + 1]
            - CIRCLE_TO_LINE_SEC_CON_X_FACTOR[index])
            * adjustNormalizedTime + CIRCLE_TO_LINE_SEC_CON_X_FACTOR[index];
    float secondControlYFactor = (CIRCLE_TO_LINE_SEC_CON_Y_FACTOR[index + 1]
            - CIRCLE_TO_LINE_SEC_CON_Y_FACTOR[index])
            * adjustNormalizedTime + CIRCLE_TO_LINE_SEC_CON_Y_FACTOR[index];
    int firstControlX = (int) (circleDiameter * firstControlXFactor);
    int firstControlY = (int) (circleDiameter * firstControlYFactor);
    int secondControlX = (int) (circleDiameter * secondControlXFactor);
    int secondControlY = (int) (circleDiameter * secondControlYFactor);

    linePath.reset();
    // left line
    linePath.cubicTo(firstControlX, firstControlY,
            secondControlX, secondControlY, adjustBoundWidth / 2, boundHeight);
    // left right line
    linePath.cubicTo(adjustBoundWidth - secondControlX,
            secondControlY, adjustBoundWidth - firstControlX, firstControlY, adjustBoundWidth, 0);

    // translate path to move the origin to the center
    int offsetX = (circleDiameter - adjustBoundWidth) / 2;
    int offsetY = (circleDiameter - boundHeight) / 2;
    linePath.addCircle(firstControlX, firstControlY,3, Path.Direction.CW);
    linePath.addCircle(secondControlX, secondControlY,3, Path.Direction.CW);
    linePath.addCircle(adjustBoundWidth - secondControlX,
            secondControlY,3, Path.Direction.CW);
    linePath.addCircle(adjustBoundWidth - firstControlX, firstControlY,3, Path.Direction.CW);
    linePath.offset(offsetX, offsetY);
}複製程式碼

整個過程路徑及控制點變化如下:

一個絢麗的downloading動效分析與實現!

至此,箭頭上移的效果,只需根據繩子中心點的位置,平移下載箭頭位置即可;

1.4.直線上下震盪及下載箭頭(Arrow)變承載進度文字的線框形態:

一個絢麗的downloading動效分析與實現!

這個過程有以下三點需要考慮:

1.4.1.直線震盪:
該效果僅需持續上下移動二階貝塞爾曲線的控制點即可,不再多言;

1.4.2.箭頭沿曲線移動:
移動的路線可以採用一個三階貝塞爾曲線進行模擬,再使用PathMeasure獲取過程中的實時位置(x、y),關鍵程式碼如下:

if (mArrowMovePath.isEmpty()) {
    mArrowMovePath.moveTo(mArrowMovePathRect.left, mArrowMovePathRect.bottom);
    mArrowMovePath.cubicTo(mArrowMovePathRect.left + mArrowMovePathRect.width() / 4,
            mArrowMovePathRect.top,
            mArrowMovePathRect.right,
            mArrowMovePathRect.top,
            mArrowMovePathRect.right, mArrowMovePathRect.bottom);
    mArrowPathMeasure.setPath(mArrowMovePath, false);
    mArrowMovePathLength = mArrowPathMeasure.getLength();
}

mArrowPathMeasure.getPosTan(mArrowMovePathLength * normalizedTime , mArrowMovePoint, null);複製程式碼

1.4.3.移動過程中的下載箭頭形態變換:

我們們用rectWidth、rectHeight分別指代下載箭頭底部的矩形部分的寬高,triWidth、triHeight分別指代Arrow頭部的三角形部分的寬高,angle指代下載箭頭的旋轉角度;

只需用ValueAnimator建立一個過程將以上數值進行如下變換:

rectWidth 到 2rectWidth;
rectHeight 到 1.4rectHeight 再到 rectHeight;
triWidth 到 0.65triWidth;
triHeight 到 0.65*triHeight;
angle 由 0 -> -30 -> 20 -> -10 -> 0度;

OK,到這裡,第一部分就可以告一段落,我們們繼續看後面的部分;

2.InProgress(顯示進度中) :

一個絢麗的downloading動效分析與實現!

GAStudio哥本次在實現過程中,沒有實現在移動的過程中的線框的搖擺,有興趣的同學可以自己修改實現,剩餘部分主要講下拉繩的變動:

2.1. 拉繩的變動:

一個絢麗的downloading動效分析與實現!

觀察上圖,可以將拉繩下拉的頂點移動的軌跡近似看成一條折線,先計算出頂點的位置,再分別繪製左、右兩邊的直線,關鍵程式碼如下:

private void drawProgressRopePath(
        Canvas canvas, float normalizeProgress, int baselineLen,
        int baseLineX, int baseLineY, int highestPointHeight, int leftLineColor) {
    int halfLen = baselineLen / 2;
    int middlePointX = (int) (baseLineX + baselineLen * normalizeProgress);
    int middlePointY;

    float k = (float) highestPointHeight / halfLen;
    if (normalizeProgress < HALF_NORMALIZED_PROGRESS) {
        middlePointY = (int) (halfLen * k
                * normalizeProgress / HALF_NORMALIZED_PROGRESS) + baseLineY;
    } else {
        middlePointY = (int) (halfLen * k
                * (1 - normalizeProgress) / HALF_NORMALIZED_PROGRESS) + baseLineY;
    }
    // draw right part first
    mBaseLinePaint.setColor(DEFAULT_LOADING_LINE_COLOR);
    canvas.drawLine(middlePointX, middlePointY, baseLineX + baselineLen,
            baseLineY, mBaseLinePaint);

    // draw left part
    mBaseLinePaint.setColor(leftLineColor);
    canvas.drawLine(baseLineX, baseLineY, middlePointX, middlePointY, mBaseLinePaint);
    if (mProgressRopePathRectF == null) {
        mProgressRopePathRectF = new RectF();
    }
    mProgressRopePathRectF.set(baseLineX, baseLineY, baseLineX + baselineLen, middlePointY);
}複製程式碼

3.Failed(失敗動畫):

一個絢麗的downloading動效分析與實現!

擼完以上gif,我們可以把這部分效果分為如下幾點:
1.線框內的文字變為Failed並且晃動;
2.繩子上下抖動;
3.繩子左側的白色部分爆炸消失;
4.線框回到最初位置,變且變為下載箭頭;
5.圓形背景逐漸放大出現;
6.圓形背景和下載箭頭整體縮放;

在這裡,我們一起看下爆炸效果的實現,其他部分相對簡單,不再贅述;

關於爆炸效果,我們可以很逼真的模擬,繪製出各式各樣的圓點來模擬,但是由於點的個數多,大小不一,採用該方式費事費力,並且由於效果速度快,轉瞬即逝,我們可以採用一種簡單而效果看起來差不多的方式,就是隻畫幾個形狀,然後平鋪到整個繩子;

該處主要使用paint的setPathEffect方法將PathEffect設定為PathDashPathEffect,關鍵程式碼如下:

Path cycle = new Path();
// generate bomb point shape
cycle.addCircle(0, 0, mBaseLineStrokeWidth / 2, Path.Direction.CCW);
cycle.addCircle(mBaseLineStrokeWidth, 0, mBaseLineStrokeWidth / 3, Path.Direction.CCW);
cycle.addCircle(mBaseLineStrokeWidth * 2, 0, mBaseLineStrokeWidth / 4, Path.Direction.CCW);
cycle.addCircle(mBaseLineStrokeWidth * 3, 0, mBaseLineStrokeWidth / 5, Path.Direction.CCW);
mFailedBombPaint = new Paint();
mFailedBombPaint.setStrokeWidth(mBaseLineStrokeWidth);
mFailedBombPaint.setAntiAlias(true);
mFailedBombPaint.setColor(DEFAULT_PROGRESS_LINE_LEFT_COLOR);
mFailedBombPaint.setStyle(Paint.Style.STROKE);

mFailedBombPaint.setPathEffect(new PathDashPathEffect(cycle,
        mBaseLineStrokeWidth * 3, 0, PathDashPathEffect.Style.TRANSLATE));
mFailedBombBellowPaint = new Paint(mFailedBombPaint);
mFailedBombBellowPaint.setPathEffect(new PathDashPathEffect(cycle,
        mBaseLineStrokeWidth * 3, HALF_FULL_ANGLE, PathDashPathEffect.Style.TRANSLATE));複製程式碼

4.Done(完成動畫):

一個絢麗的downloading動效分析與實現!

擼完以上gif, 我們可以將該部分概括為以下部分:

1.線框繞Y軸旋轉,並由100%變換為done;
2.線框隨進度條收縮到最中心;
3.線框在中心點晃動;
4.線框變換為下載箭頭,圓形背景復出;
5.圓形背景和下載箭頭整體縮放,伴隨下載箭頭上下晃動;

該部分我們們一起看下第一條的實現,即Canvas裡如何實現偽三維變換;
Canvas中只有rotate函式,也就是在二維平面內進行旋轉,不能實現如上的繞Y軸旋轉,類似效果需要藉助Camera來實現,關鍵程式碼如下:

float angle;
String str;
if (normalizedTime <= HALF_NORMALIZED_PROGRESS) {
    str = FULL_PROGRESS_STR;
    angle = HALF_FULL_ANGLE * normalizedTime;
    mProgressTextPaint.setColor(DEFAULT_PROGRESS_TEXT_COLOR);
} else {
    str = FULL_PROGRESS_DONE_STR;
    angle = HALF_FULL_ANGLE * normalizedTime + HALF_FULL_ANGLE;
    mProgressTextPaint.setColor(DEFAULT_DONE_PROGRESS_TEXT_COLOR);
}
if (mCamera == null) {
    mCamera = new Camera();
}
mCamera.save();
mCamera.rotateY(angle);
mCamera.getMatrix(mArrowRotateMatrix);
mCamera.restore();
// 保證繞Arrow的中心進行旋轉
mArrowRotateMatrix.preTranslate(-mArrowRectF.centerX(), -mArrowRectF.centerY());
mArrowRotateMatrix.postTranslate(mArrowRectF.centerX(), mArrowRectF.centerY());
mLastArrowOffsetX = (int) (mBaseLineX + mBaseLineLen - mArrowRectF.width() / 2);
mLastArrowOffsetY = (int) (mBaseLineY - mArrowRectF.height());
canvas.save();
canvas.translate(mLastArrowOffsetX, mLastArrowOffsetY);
// 應用上述Camera變換的結果
canvas.concat(mArrowRotateMatrix);
mDefaultPaint.setColor(DEFAULT_ARROW_COLOR);
// 繪製Arrow
canvas.drawPath(mArrowPath, mDefaultPaint);
mProgressTextPaint.getTextBounds(str,
        0, str.length(), mProgressTextRect);
// 文字
canvas.drawText(str,
        mArrowRectF.left + (mArrowRectF.width() - mProgressTextRect.width()) / 2,
        mArrowRectF.bottom - mArrowRectF.height() / 2, mProgressTextPaint);
canvas.restore();複製程式碼

至此,該效果的核心邏輯我們們已經分析完畢,實現效果如下:

成功部分:

一個絢麗的downloading動效分析與實現!

失敗部分:
一個絢麗的downloading動效分析與實現!

你以為到這裡就結束了嗎?No-No-No,作為一個負責任的開發者,最後我們們加上合理的自定義屬性,以方便使用者自行定義:

<declare-styleable name="GADownloadingView">
    <attr name="arrow_color" format="color" />
    <attr name="loading_circle_back_color" format="color" />
    <attr name="loading_line_color" format="color" />
    <attr name="progress_line_color" format="color" />
    <attr name="progress_text_color" format="color" />
    <attr name="done_text_color" format="color" />
</declare-styleable>複製程式碼

最後附上QQ技術交流群和 github 地址,喜歡的同學歡迎follow和star:

如果你想看 GAStudio Github主頁,請戳這裡
如果你想看 GAStudio更多技術文章,請戳這裡
QQ技術交流群:277582728;
github地址: github.com/Ajian-studi…

一個絢麗的downloading動效分析與實現!

相關文章