当前位置: 首页 > news >正文

一个绚丽的downloading动效分析与实现!

闲逛之余,看到一个不错的downloading动效,这个动效用CJJ的话说难度还好,但本人觉得还比较灵动、带感、俏皮、有新意,好了话不多说,咱们先来撸一张高清无码gif图:

撸完,咱可以将整个动效简单划分为以下流程:
1.BeforeProgress(显示进度前);
2.InProgress(显示进度中);
3.Failed(失败动画);
4.Done(完成动画);

下面咱们一起对以上流程进行分析与实现;


1.BeforeProgress(显示进度前):

同样,咱们一起撸一下第一部分高清无码gif图:


通过观察,我们可以将以上动画分割为以下几个内容:
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;

本人最终采用第四种方式进行实现;

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.圆形背景变为一条直线,并伴随箭头些许上移:


这个部分相比前面两步稍显复杂,需要将圆环如丝般顺滑的变换成直线,并随之上线震荡,该过程拆解图如下:

对于这个过程,GAStudio哥采用两条三阶贝塞尔曲线对初期的圆环、中间部分的曲线、最终的直线进行模拟;

为了便于理解,抽象出四个核心状态,过程图解如下:

1.完整圆形状态:


2.延展开来状态:

3.横向铺开状态:

4.直线状态:

更新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);
}复制代码

整个过程路径及控制点变化如下:


至此,箭头上移的效果,只需根据绳子中心点的位置,平移下载箭头位置即可;

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(显示进度中) :


GAStudio哥本次在实现过程中,没有实现在移动的过程中的线框的摇摆,有兴趣的同学可以自己修改实现,剩余部分主要讲下拉绳的变动:

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(完成动画):


撸完以上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();复制代码

至此,该效果的核心逻辑咱们已经分析完毕,实现效果如下:

成功部分:


失败部分:

你以为到这里就结束了吗?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…

相关文章:

  • 判断2的幂次方(多种算法)
  • VMware中装Win2012并配置Hyper-v
  • MySQL运维之神奇的参数
  • IOS技能点
  • 远离DoS攻击 Windows Server 2016发布DNS政策
  • Linux学习总结(22)——CentOS7.2安装Nginx
  • JS根据经纬度获取地址信息
  • 源码解读之工具--Source Insight
  • 如果就
  • Hydra用户手册
  • ABP学习日记1
  • 小博老师解析Java核心技术 ——JSwing高级菜单制作
  • Java JDBC中,MySQL字段类型到JAVA类型的转换
  • bzoj3894: 文理分科
  • javascript的对象
  • [nginx文档翻译系列] 控制nginx
  • [数据结构]链表的实现在PHP中
  • Android Studio:GIT提交项目到远程仓库
  •  D - 粉碎叛乱F - 其他起义
  • ECS应用管理最佳实践
  • ES6系列(二)变量的解构赋值
  • JavaScript异步流程控制的前世今生
  • Joomla 2.x, 3.x useful code cheatsheet
  • Linux快速配置 VIM 实现语法高亮 补全 缩进等功能
  • Sequelize 中文文档 v4 - Getting started - 入门
  • Shell编程
  • Spark VS Hadoop:两大大数据分析系统深度解读
  • Spring Boot MyBatis配置多种数据库
  • Spring声明式事务管理之一:五大属性分析
  • vue 个人积累(使用工具,组件)
  • vue中实现单选
  • Vue组件定义
  • WordPress 获取当前文章下的所有附件/获取指定ID文章的附件(图片、文件、视频)...
  • 将 Measurements 和 Units 应用到物理学
  • 深入浅出Node.js
  • 什么是Javascript函数节流?
  • 完善智慧办公建设,小熊U租获京东数千万元A+轮融资 ...
  • # 20155222 2016-2017-2 《Java程序设计》第5周学习总结
  • (JSP)EL——优化登录界面,获取对象,获取数据
  • (过滤器)Filter和(监听器)listener
  • (区间dp) (经典例题) 石子合并
  • (十二)devops持续集成开发——jenkins的全局工具配置之sonar qube环境安装及配置
  • (五) 一起学 Unix 环境高级编程 (APUE) 之 进程环境
  • (一)硬件制作--从零开始自制linux掌上电脑(F1C200S) <嵌入式项目>
  • (原创) cocos2dx使用Curl连接网络(客户端)
  • (原創) 如何使用ISO C++讀寫BMP圖檔? (C/C++) (Image Processing)
  • ***微信公众号支付+微信H5支付+微信扫码支付+小程序支付+APP微信支付解决方案总结...
  • **PHP分步表单提交思路(分页表单提交)
  • .NET 6 在已知拓扑路径的情况下使用 Dijkstra,A*算法搜索最短路径
  • .net framwork4.6操作MySQL报错Character set ‘utf8mb3‘ is not supported 解决方法
  • .NET Micro Framework初体验
  • .NET 服务 ServiceController
  • .net 无限分类
  • .NET 中的轻量级线程安全
  • .net 逐行读取大文本文件_如何使用 Java 灵活读取 Excel 内容 ?