如何仿一个抖音极速版领现金的进度条动画?

效果演示

20230617_064552_edit.gif

不仅仅是实现效果,要封装,就封装好

看完了演示的效果,你是否在思考,代码应该怎么实现?先不着急写代码,先想想哪些地方是要可以动态配置的。首先第一个,进度条的形状是不是要可以换?然后进度条的背景色和填充的颜色,以及动画的时长是不是也要可以配置?没错,起始位置是不是也要可以换?最好还要让速度可以一会快一会慢对吧,画笔的笔帽是不是还可以选择平的或圆的?带着这些问题,我们再开始写代码。

代码实现

我们写一个自定义View,把可以动态配置的地方想好后,就可以定义自定义属性了。

<?xml version="1.0" encoding="utf-8"?>
<resources><declare-styleable name="DoraProgressView"><attr name="dview_progressType"><enum name="line" value="0"/><enum name="semicircle" value="1"/><enum name="semicircleReverse" value="2"/><enum name="circle" value="3"/><enum name="circleReverse" value="4"/></attr><attr name="dview_progressOrigin"><enum name="left" value="0"/><enum name="top" value="1"/><enum name="right" value="2"/><enum name="bottom" value="3"/></attr><attr format="dimension|reference" name="dview_progressWidth"/><attr format="color|reference" name="dview_progressBgColor"/><attr format="color|reference" name="dview_progressHoverColor"/><attr format="integer" name="dview_animationTime"/><attr name="dview_paintCap"><enum name="flat" value="0"/><enum name="round" value="1"/></attr></declare-styleable>
</resources>

然后我们不管三七二十一,先把自定义属性解析出来。

private fun initAttrs(context: Context, attrs: AttributeSet?, defStyleAttr: Int) {val a = context.obtainStyledAttributes(attrs,R.styleable.DoraProgressView,defStyleAttr,0)when (a.getInt(R.styleable.DoraProgressView_dview_progressType, PROGRESS_TYPE_LINE)) {0 -> progressType = PROGRESS_TYPE_LINE1 -> progressType = PROGRESS_TYPE_SEMICIRCLE2 -> progressType = PROGRESS_TYPE_SEMICIRCLE_REVERSE3 -> progressType = PROGRESS_TYPE_CIRCLE4 -> progressType = PROGRESS_TYPE_CIRCLE_REVERSE}when (a.getInt(R.styleable.DoraProgressView_dview_progressOrigin, PROGRESS_ORIGIN_LEFT)) {0 -> progressOrigin = PROGRESS_ORIGIN_LEFT1 -> progressOrigin = PROGRESS_ORIGIN_TOP2 -> progressOrigin = PROGRESS_ORIGIN_RIGHT3 -> progressOrigin = PROGRESS_ORIGIN_BOTTOM}when(a.getInt(R.styleable.DoraProgressView_dview_paintCap, 0)) {0 -> paintCap = Paint.Cap.SQUARE1 -> paintCap = Paint.Cap.ROUND}progressWidth = a.getDimension(R.styleable.DoraProgressView_dview_progressWidth, 30f)progressBgColor =a.getColor(R.styleable.DoraProgressView_dview_progressBgColor, Color.GRAY)progressHoverColor =a.getColor(R.styleable.DoraProgressView_dview_progressHoverColor, Color.BLUE)animationTime = a.getInt(R.styleable.DoraProgressView_dview_animationTime, 1000)a.recycle()
}

解析完自定义属性,切勿忘了释放TypedArray。接下来我们考虑下一步,测量。半圆是不是不要那么大的画板对吧,我们在测量的时候就要充分考虑进去。

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {super.onMeasure(widthMeasureSpec, heightMeasureSpec)progressBgPaint.strokeWidth = progressWidthprogressHoverPaint.strokeWidth = progressWidthif (progressType == PROGRESS_TYPE_LINE) {// 线var left = 0fvar top = 0fvar right = measuredWidth.toFloat()var bottom = measuredHeight.toFloat()val isHorizontal = when(progressOrigin) {PROGRESS_ORIGIN_LEFT, PROGRESS_ORIGIN_RIGHT -> trueelse -> false}if (isHorizontal) {top = (measuredHeight - progressWidth) / 2bottom = (measuredHeight + progressWidth) / 2progressBgRect[left + progressWidth / 2, top, right - progressWidth / 2] = bottom} else {left = (measuredWidth - progressWidth) / 2right = (measuredWidth + progressWidth) / 2progressBgRect[left, top + progressWidth / 2, right] = bottom - progressWidth / 2}} else if (progressType == PROGRESS_TYPE_CIRCLE || progressType == PROGRESS_TYPE_CIRCLE_REVERSE) {// 圆var left = 0fval top = 0fvar right = measuredWidthvar bottom = measuredHeightprogressBgRect[left + progressWidth / 2, top + progressWidth / 2, right - progressWidth / 2] =bottom - progressWidth / 2} else {// 半圆val isHorizontal = when(progressOrigin) {PROGRESS_ORIGIN_LEFT, PROGRESS_ORIGIN_RIGHT -> trueelse -> false}val min = measuredWidth.coerceAtMost(measuredHeight)var left = 0fvar top = 0fvar right = 0fvar bottom = 0fif (isHorizontal) {if (measuredWidth >= min) {left = ((measuredWidth - min) / 2).toFloat()right = left + min}if (measuredHeight >= min) {bottom = top + min}progressBgRect[left + progressWidth / 2, top + progressWidth / 2, right - progressWidth / 2] =bottom - progressWidth / 2setMeasuredDimension(MeasureSpec.makeMeasureSpec((right - left).toInt(),MeasureSpec.EXACTLY),MeasureSpec.makeMeasureSpec((bottom - top + progressWidth).toInt() / 2,MeasureSpec.EXACTLY))} else {if (measuredWidth >= min) {right = left + min}if (measuredHeight >= min) {top = ((measuredHeight - min) / 2).toFloat()bottom = top + min}progressBgRect[left + progressWidth / 2, top + progressWidth / 2, right - progressWidth / 2] =bottom - progressWidth / 2setMeasuredDimension(MeasureSpec.makeMeasureSpec((right - left + progressWidth).toInt() / 2,MeasureSpec.EXACTLY),MeasureSpec.makeMeasureSpec((bottom - top).toInt(),MeasureSpec.EXACTLY))}}
}

View的onMeasure()方法是不是默认调用了一个

super.onMeasure(widthMeasureSpec, heightMeasureSpec)

它最终会调用setMeasuredDimension()方法来确定最终测量的结果吧。如果我们对默认的测量不满意,我们可以自己改,最后也调用setMeasuredDimension()方法把测量结果确认。半圆,如果是水平的情况下,我们的宽度就只要一半,相反如果是垂直的半圆,我们高度就只要一半。最后我们画还是照常画,只不过在最后把画到外面的部分移动到画板上显示出来。接下来就是我们最重要的绘图环节了。

override fun onDraw(canvas: Canvas) {if (progressType == PROGRESS_TYPE_LINE) {val isHorizontal = when(progressOrigin) {PROGRESS_ORIGIN_LEFT, PROGRESS_ORIGIN_RIGHT -> trueelse -> false}if (isHorizontal) {canvas.drawLine(progressBgRect.left,measuredHeight / 2f,progressBgRect.right,measuredHeight / 2f,progressBgPaint)} else {canvas.drawLine(measuredWidth / 2f,progressBgRect.top,measuredWidth / 2f,progressBgRect.bottom, progressBgPaint)}if (percentRate > 0) {when (progressOrigin) {PROGRESS_ORIGIN_LEFT -> {canvas.drawLine(progressBgRect.left,measuredHeight / 2f,(progressBgRect.right) * percentRate,measuredHeight / 2f,progressHoverPaint)}PROGRESS_ORIGIN_TOP -> {canvas.drawLine(measuredWidth / 2f,progressBgRect.top,measuredWidth / 2f,(progressBgRect.bottom) * percentRate,progressHoverPaint)}PROGRESS_ORIGIN_RIGHT -> {canvas.drawLine(progressWidth / 2 + (progressBgRect.right) * (1 - percentRate),measuredHeight / 2f,progressBgRect.right,measuredHeight / 2f,progressHoverPaint)}PROGRESS_ORIGIN_BOTTOM -> {canvas.drawLine(measuredWidth / 2f,progressWidth / 2 + (progressBgRect.bottom) * (1 - percentRate),measuredWidth / 2f,progressBgRect.bottom,progressHoverPaint)}}}} else if (progressType == PROGRESS_TYPE_SEMICIRCLE) {if (progressOrigin == PROGRESS_ORIGIN_LEFT) {// PI ~ 2PIcanvas.drawArc(progressBgRect, 180f, 180f, false, progressBgPaint)canvas.drawArc(progressBgRect,180f,angle.toFloat(),false,progressHoverPaint)} else if (progressOrigin == PROGRESS_ORIGIN_TOP) {canvas.translate(-progressBgRect.width() / 2, 0f)// 3/2PI ~ 2PI, 0 ~ PI/2canvas.drawArc(progressBgRect, 270f, 180f, false, progressBgPaint)canvas.drawArc(progressBgRect,270f,angle.toFloat(),false,progressHoverPaint)} else if (progressOrigin == PROGRESS_ORIGIN_RIGHT) {canvas.translate(0f, -progressBgRect.height() / 2)// 2PI ~ PIcanvas.drawArc(progressBgRect, 0f, 180f, false, progressBgPaint)canvas.drawArc(progressBgRect,0f,angle.toFloat(),false,progressHoverPaint)} else if (progressOrigin == PROGRESS_ORIGIN_BOTTOM) {// PI/2 ~ 3/2PIcanvas.drawArc(progressBgRect, 90f, 180f, false, progressBgPaint)canvas.drawArc(progressBgRect,90f,angle.toFloat(),false,progressHoverPaint)}} else if (progressType == PROGRESS_TYPE_SEMICIRCLE_REVERSE) {if (progressOrigin == PROGRESS_ORIGIN_LEFT) {canvas.translate(0f, -progressBgRect.height() / 2)// PI ~ 2PIcanvas.drawArc(progressBgRect, 180f, -180f, false, progressBgPaint)canvas.drawArc(progressBgRect,180f,-angle.toFloat(),false,progressHoverPaint)} else if (progressOrigin == PROGRESS_ORIGIN_TOP) {// 3/2PI ~ PI/2canvas.drawArc(progressBgRect, 270f, -180f, false, progressBgPaint)canvas.drawArc(progressBgRect,270f,-angle.toFloat(),false,progressHoverPaint)} else if (progressOrigin == PROGRESS_ORIGIN_RIGHT) {// 2PI ~ PIcanvas.drawArc(progressBgRect, 0f, -180f, false, progressBgPaint)canvas.drawArc(progressBgRect,0f,-angle.toFloat(),false,progressHoverPaint)} else if (progressOrigin == PROGRESS_ORIGIN_BOTTOM) {canvas.translate(-progressBgRect.width() / 2, 0f)// PI/2 ~ 2PI, 2PI ~ 3/2PIcanvas.drawArc(progressBgRect, 90f, -180f, false, progressBgPaint)canvas.drawArc(progressBgRect,90f,-angle.toFloat(),false,progressHoverPaint)}} else if (progressType == PROGRESS_TYPE_CIRCLE) {val deltaAngle = if (progressOrigin == PROGRESS_ORIGIN_TOP) {90f} else if (progressOrigin == PROGRESS_ORIGIN_RIGHT) {180f} else if (progressOrigin == PROGRESS_ORIGIN_BOTTOM) {270f} else {0f}canvas.drawArc(progressBgRect, 0f, 360f, false, progressBgPaint)canvas.drawArc(progressBgRect,180f + deltaAngle,angle.toFloat(),false,progressHoverPaint)} else if (progressType == PROGRESS_TYPE_CIRCLE_REVERSE) {val deltaAngle = if (progressOrigin == PROGRESS_ORIGIN_TOP) {90f} else if (progressOrigin == PROGRESS_ORIGIN_RIGHT) {180f} else if (progressOrigin == PROGRESS_ORIGIN_BOTTOM) {270f} else {0f}canvas.drawArc(progressBgRect, 0f, 360f, false, progressBgPaint)canvas.drawArc(progressBgRect,180f + deltaAngle,-angle.toFloat(),false,progressHoverPaint)}
}

绘图除了需要Android的基础绘图知识外,还需要一定的数学计算的功底,比如基本的几何图形的点的计算你要清楚。怎么让绘制的角度变化起来呢?这个问题问的好。这个就牵扯出我们动画的一个关键类,TypeEvaluator,这个接口可以让我们只需要指定边界值,就可以根据动画执行的时长,来动态计算出当前的渐变值。

private inner class AnimationEvaluator : TypeEvaluator<Float> {override fun evaluate(fraction: Float, startValue: Float, endValue: Float): Float {return if (endValue > startValue) {startValue + fraction * (endValue - startValue)} else {startValue - fraction * (startValue - endValue)}}
}

百分比渐变的固定写法,是不是应该记个笔记,方便以后CP?那么现在我们条件都成熟了,只需要将初始角度的百分比改变一下,我们写一个改变角度百分比的方法。

fun setPercentRate(rate: Float) {if (animator == null) {animator = ValueAnimator.ofObject(AnimationEvaluator(),percentRate,rate)}animator?.addUpdateListener { animation: ValueAnimator ->val value = animation.animatedValue as Floatangle =if (progressType == PROGRESS_TYPE_CIRCLE || progressType == PROGRESS_TYPE_CIRCLE_REVERSE) {(value * 360).toInt()} else if (progressType == PROGRESS_TYPE_SEMICIRCLE || progressType == PROGRESS_TYPE_SEMICIRCLE_REVERSE) {(value * 180).toInt()} else {0   // 线不需要求角度}percentRate = valueinvalidate()}animator?.interpolator = LinearInterpolator()animator?.setDuration(animationTime.toLong())?.start()animator?.addListener(object : Animator.AnimatorListener {override fun onAnimationStart(animation: Animator) {}override fun onAnimationEnd(animation: Animator) {percentRate = ratelistener?.onComplete()}override fun onAnimationCancel(animation: Animator) {}override fun onAnimationRepeat(animation: Animator) {}})
}

这里牵扯到了Animator。有start就一定不要忘了异常中断的情况,我们可以写一个reset的方法来中断动画执行,恢复到初始状态。

fun reset() {percentRate = 0fanimator?.cancel()
}

如果你不reset,想连续执行动画,则两次调用的时间间隔一定要大于动画时长,否则就应该先取消动画。

涉及到的Android绘图知识点

我们归纳一下完成这个自定义View需要具备的知识点。

  1. 基本图形的绘制,这里主要是扇形
  2. 测量和画板的平移变换
  3. 自定义属性的定义和解析
  4. Animator和动画估值器TypeEvaluator的使用

思路和灵感来自于系统化的基础知识

这个控件其实并不难,主要就是动态配置一些参数,然后在计算上稍微复杂一些,需要一些数学的功底。那么你为什么没有思路呢?你没有思路最可能的原因主要有以下几个可能。

  1. 自定义View的基础绘图API不熟悉
  2. 动画估值器使用不熟悉
  3. 对自定义View的基本流程不熟悉
  4. 看的自定义View的源码不够多
  5. 自定义View基础知识没有系统学习,导致是一些零零碎碎的知识片段
  6. 数学功底不扎实

我觉得往往不是你不会,这些基础知识点你可能都看到过很多次,但是一到自己写就没有思路了。思路和灵感来自于大量源码的阅读和大量的实践。大前提就是你得先把自定义View的这些知识点系统学习一下,先保证都见过,然后才是将它们融会贯通,用的时候信手拈来。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/19959.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

力扣257. 二叉树的所有路径

思路&#xff1a;题目需要记录从根节点开始走的路径&#xff0c;无疑选用前序遍历&#xff0c;用一个数组paths 记录走过的节点信息&#xff0c;遇到叶子节点就用另一个list记录下路径&#xff0c;回溯时删掉paths尾节点即可 class Solution {public List<String> binar…

JeecgBoot-Vue3:基于Vue3的低代码开发平台的新篇章

摘要 随着前端技术的不断发展&#xff0c;Vue3.0、TypeScript、Vite以及Ant Design Vue等新技术方案的涌现&#xff0c;为低代码开发平台带来了全新的可能性。JeecgBoot-Vue3作为JeecgBoot低代码平台的全新UI版本&#xff0c;采用Vue3技术栈&#xff0c;结合上述先进技术&#…

VBA代码解决方案第十四讲 如何利用VBA检查单元格中是否含有公式

《VBA代码解决方案》(版权10028096)这套教程是我最早推出的教程&#xff0c;目前已经是第三版修订了。这套教程定位于入门后的提高&#xff0c;在学习这套教程过程中&#xff0c;侧重点是要理解及掌握我的“积木编程”思想。要灵活运用教程中的实例像搭积木一样把自己喜欢的代码…

解决bind error: Address already in use

是端口复用问题 产生原因 程序突然退出系统但是没有释放端口 问题解决 首先通过 //显示进程信息 ps -la //杀死相关进程 kill -9 xxxx然后添加socket设置 int on1; if(setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0){perror("setsockopt")…

Qt QScript 之 C++/JavaScript相互调用

文章目录 Qt Script什么是ECMAScriptQt 中JavaScriptclass 详解Basic UsageQObject对脚本引擎可用使用信号槽connect 三种模式访问属性, 子对象使c++对象可用于用Qt Script编写的脚本C++ 类成员函数可用于脚本C++ 类属性可用于脚本对脚本中的c++对象信号的反应函数对象和本机函…

CRMEB多店版v3.0前端技术革新与实践

摘要 随着移动互联网技术的飞速发展&#xff0c;用户对移动应用的体验要求日益提高。CRMEB多店版v3.0作为一款针对多门店管理的电商系统&#xff0c;在前端技术层面进行了全面的革新与优化。本文将从移动端UI设计、页面功能更新、DIY设计功能升级、移动端平台与门店管理、营销…

Kubernetes 系统监控Metrics Server、HorizontalPodAutoscaler、Prometheus

Metrics Server Linux 系统命令 top 能够实时显示当前系统的 CPU 和内存利用率&#xff0c;它是性能分析和调优的基本工具。 Kubernetes 也提供了类似的命令&#xff0c;就是 kubectl top&#xff0c;不过默认情况下这个命令不会生效&#xff0c;必须要安装一个插件 Metrics …

halcon程序如何导出C#文件

1.打开halcon文件&#xff1b; 2.写好需要生成C#文件的算子或函数&#xff1b; 3.找到档案-输出&#xff0c;如下图&#xff1b; 4.点击输出&#xff0c;弹出如下窗口 &#xff08;1&#xff09;可以修改导出文件的存储路径 &#xff08;2&#xff09;选择C#-HALCON/.NET &…

centos7 openssh9.7p 制作rpm包

centos7 openssh9.7p 制作rpm包 下载源码包&#xff1a;通过git开源打包源码准备编译打包环境编译打包上传rpm包到需要更新的服务器,并更新 下载源码包&#xff1a; 一般只用ssh源码就可以了 cd /root wget https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-9.7p…

论文《Causal Inference for Recommender Systems》阅读

论文《Causal Inference for Recommender Systems》阅读 论文概况论文动机&#xff08;Introduction&#xff09;MethodologyPreliminariesClassical Causal Inference & Causal AdjustmentDeconfounded Recommender 总结 论文概况 今天给大家带来的是发表在推荐系统顶会 …

使用Spring Boot自定义注解 + AOP实现基于IP的接口限流和黑白名单

&#x1f604; 19年之后由于某些原因断更了三年&#xff0c;23年重新扬帆起航&#xff0c;推出更多优质博文&#xff0c;希望大家多多支持&#xff5e; &#x1f337; 古之立大事者&#xff0c;不惟有超世之才&#xff0c;亦必有坚忍不拔之志 &#x1f390; 个人CSND主页——Mi…

IDEA启动jsp项目

1、背景 有个老项目的前端需要修改&#xff0c;整来源码之后发现是比较古老的jsp项目&#xff0c;需要在idea中启动下试试 2、代码配置流程 常规的配置流程网上都有 2.1 首先找到Project Structure 2.2 配置web.xml 注意下方的 web resource directory, web.xml中的写的相对…

Markdown 使用技巧之利用 Mermaid 进行绘制流程图

文章目录 前言一、基础语法1.1 声明图像类型1.2 声明排列方向1.3 声明节点1.4 声明节点形状1.5 声明节点间的连接1.5.1 基本连接线1.5.2 调整链接的长度1.5.3 调整链接的样式二、流程图-进阶使用2.1 自定义节点样式2.2 自定义形状大小2.3 自定义链接样式2.4 视图分组三、使用场…

校园安保巡逻机器人

2023年8月5日&#xff0c;陕西西安一高校实验室起火冒烟&#xff0c;导致学校化学实验室发生火灾。2022年8月3日&#xff0c;一名歹徒持械闯入江西吉安安福县城的一家私立幼儿园&#xff0c;对着无辜的幼儿行凶伤人&#xff0c;造成3死6伤。 像这样的事故有不断地发生&#xf…

161.二叉树:在每个树中找最大值(力扣)

代码解决 /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}* Tre…

C语言王国——杨氏矩阵

目录 1. 引言 2. 了解杨氏矩阵 3. 思路分析 4. 代码 5. 总结 1. 引言 最近在做二维数组的训练的时候发现了一个很有意思的题&#xff1a; 一看这不是杨氏矩阵嘛&#xff0c;接下来就由姜糖我带大家了解一下这个著名的矩阵。 2. 了解杨氏矩阵 通过查阅百度得知&#xff1a; …

python数据分析——datetime数据类型1

参考资料&#xff1a;活用pandas库 1、python的datetime对象 # 导入datetime对象 from datetime import datetime# 获取当前日期和时间 nowdatetime.now() print(now)# 手动创建datetime t1datetime.now() t2datetime(1970,1,1) # 对datetime做数学运算 difft1-t2 print(diff…

儿童节快乐!探索图形化编程桌面的“童年”成长之路

在这个充满童真与快乐的儿童节&#xff0c;我要向在CSDN平台上努力拼搏的每一位朋友&#xff0c;送上我最热切、最深情的祝福&#xff01;愿你们心中那份孩童般的纯真与对世界无尽的好奇永不褪色&#xff0c;愿你们的人生道路如同这个美好的节日&#xff0c;流光溢彩、欢乐永恒…

lynis安全漏洞扫描工具

Lynis是一款Unix系统的安全审计以及加固工具&#xff0c;能够进行深层次的安全扫描&#xff0c;其目的是检测潜在的时间并对未来的系统加固提供建议。这款软件会扫描一般系统信息&#xff0c;脆弱软件包以及潜在的错误配置。 安装 方式1 git下载使用git clone https://github…

docker compose完成简单项目部署

1. 项目环境 centos7 docker mysql redis ruoyi项目 ruoyi项目链接&#xff1a;https://gitee.com/y_project/RuoYi-Vue.git 2. 进行项目前后端代码打包 后端打包&#xff1a; 修改mysql连接的相关配置文件 RuoYi-Vue/ruoyi-admin/src/main/resources/application-dru…