闲逛之余,看到一个不错的downloading动效,这个动效用CJJ的话说难度还好,但本人以为还比较灵动、带感、俏皮、有新意,好了话很少说,我们先来撸一张高清无码gif图: javascript
撸完,咱能够将整个动效简单划分为如下流程:
1.BeforeProgress(显示进度前);
2.InProgress(显示进度中);
3.Failed(失败动画);
4.Done(完成动画);java
下面我们一块儿对以上流程进行分析与实现;git
1.BeforeProgress(显示进度前):github
一样,我们一块儿撸一下第一部分高清无码gif图:
canvas
1.1. 圆形背景和下载剪头总体缩放:函数
这里面,圆形背景和总体的缩放好说,稍显麻烦的是下载箭头,因为后面箭头还须要形变为承载进度文字的线框,因此丢掉你使用图片的小想法,我们一块儿用path勾一个活泼的小箭头:post
// 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勾勒,实现圆角线框大体有如下几种方案:ui
1.使用path的quadTo()以二次贝塞尔曲线链接;
2.使用path的arcTo()以圆弧形式链接;
3.使用path中addArc()添加一段圆;
4.使用paint的setPathEffect设置PathEffect为ConnerPathEffect;spa
本人最终采用第四种方式进行实现;
1.2.圆形背景逐步镂空(缩放到必定阶段,内部镂空圆不断扩大):
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.圆形背景变为一条直线,并伴随箭头些许上移:
为了便于理解,抽象出四个核心状态,过程图解以下:
1.完整圆形状态:
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);
}复制代码
整个过程路径及控制点变化以下:
1.4.直线上下震荡及下载箭头(Arrow)变承载进度文字的线框形态:
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(显示进度中) :
2.1. 拉绳的变更:
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(失败动画):
撸完以上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(完成动画):
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();复制代码
至此,该效果的核心逻辑我们已经分析完毕,实现效果以下:
成功部分:
<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…