10分钟带你实现一个Android自定义View:带动画的等级经验条

先展示一下静态效果图

介绍一下我们的实现流程:

  1. 首先整个经验条有一个圆角边框的背景打底;
  2. 然后给经验条绘制一条轨道,让用户比较直观地看到总进度的长度;
  3. 在轨道的上层绘制我们的渐变色经验条;
  4. 在经验条的上层绘制等级分割点,已达成的等级大圆点,未达成就是小圆点;
  5. 最终再给View加上经验变化的动画效果就实现了。

动态与静态效果图:

img_v2_8330ee62-9efd-4cb8-b174-9ce54780db1g.gif
image.png

按流程实现效果

0.准备工作

定义需要用到的变量

重要的变量都加上注释了,没有注释的就是我觉得没必要注释🚀

//整个View的宽度
private var mViewWidth = 0F
//整个View的高度
private var mViewHeight = 0F
//内部经验条的宽度
private var mLineWidth = 0F
//内部经验条的高度
private var mLineHeight = 0F
//内部经验条的左边距
private var mLineLeft = 0F
//内部经验条的上边距
private var mLineTop = 0F
//经验条的圆角
private var mRadius = 0F
//等级圆点的间隔
private var mPointInterval = 0F
//当前经验值
private var mExperience = 0
//每一等级占总长的百分比
private var mLevelPercent = 1F
//经验条百分比(相对于总进度)
private var mExperiencePercent = 1F
//当前等级
private var mCurrentLevel = 0
//升级所需要的经验列表
private val mLevelList = mutableListOf<Int>()//各种颜色值
private val mPointColor = Color.parseColor("#E1E1E1")
private val mLineColor = Color.parseColor("#666666")
private val mShaderStartColor = Color.parseColor("#18EFE2")
private val mShaderEndColor = Color.parseColor("#0CF191")
private val mStrokeColor = Color.parseColor("#323232")
//各种颜色值//各种画笔
private val mStrokePaint by lazy {Paint().apply {color = mStrokeColor}
}
private val mShaderPaint by lazy {Paint().apply {color = mShaderStartColor}
}
private val mLinePaint by lazy {Paint().apply {color = mLineColor}
}
private val mLevelAchievedPaint by lazy {Paint().apply {color = mShaderEndColor}
}
private val mLevelNotAchievedPaint by lazy {Paint().apply {color = mPointColor}
}
//各种画笔

重写onMeasure方法,计算View的宽高与各种参数

这里的参数含义在上面大多都已经有注释了,这里就不再多解释,主要说一下做了什么:
通过MeasureSpec去拿到最终的宽度,最终宽度减去我们的左右内边距就是我们要绘制的实际宽度,我们要绘制的经验条宽高比为20:1,所以View的最终高度就是宽度的1/20加上上下的内边距;
经验条内部轨道的高度为边框高度的1/3,由此算出内部轨道对于边框的四个边距是多少(mLineTop、mLineLeft);
计算完前面的参数就根据轨道的宽度去设置渐变画笔的shader属性,也计算每个等级点之间的间隔距离;
最终将实际的View宽高传给setMeasuredDimension,完成测量工作。

/*** 测量各种尺寸*/
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {val width = MeasureSpec.getSize(widthMeasureSpec)mViewWidth = (width - paddingStart - paddingEnd).toFloat()val height = mViewWidth / 20 + paddingTop + paddingBottommViewHeight = mViewWidth / 20mRadius = mViewHeightmLineHeight = mViewHeight / 3mLineTop = (mViewHeight - mLineHeight) / 2mLineWidth = mViewWidth - mLineTop * 2mLineLeft = mLineTopsetShaderColor()computerPointInterval()setMeasuredDimension(width, height.toInt())
}/*** 设置经验条的渐变色*/
private fun setShaderColor() {mShaderPaint.shader = LinearGradient(0F, 0F, mLineWidth, 0F,mShaderStartColor, mShaderEndColor, Shader.TileMode.CLAMP)
}/*** 计算各个等级点之间的间隔*/
private fun computerPointInterval() {if (mLineWidth > 0F || mLevelList.isNotEmpty()) {mPointInterval = mLineWidth / mLevelList.size}
}

1.绘制经验条的打底圆角背景框

重写onDraw方法,将画布canvas进行偏移,移除内边距对我们绘制的影响。
saverestore这两个API是成对使用的,save会保存画布的当前状态,然后我们就可以对画布进行偏移、旋转和缩放等操作,等我们绘制完之后再调用restore,就可以使用画布回到之前的状态了。
translate方法可以对画布进行偏移,偏移之后我们的所有绘制操作就都基于偏移后的坐标了。
drawBackground实现了底部边框的绘制,只有一行代码,实现非常简单。
drawRoundRect这个API的效果就是绘制一个圆角矩形,传入四个边角坐标、圆角和画笔即可。

效果图:

image.png

/*** 绘制View*/
override fun onDraw(canvas: Canvas) {canvas.save()canvas.translate(paddingStart.toFloat(), paddingTop.toFloat())drawBackground(canvas)canvas.restore()
}/*** 绘制背景边框*/
private fun drawBackground(canvas: Canvas) {canvas.drawRoundRect(0F, 0F, mViewWidth, mViewHeight, mRadius, mRadius, mStrokePaint)
}

2.给经验条绘制一条轨道

修改onDraw方法,添加drawExperienceBar方法。
drawExperienceBar里面也做了一个画布的偏移,然后绘制一个圆角矩形,跟上面的很相似,相信大家都能看懂。
效果图:
image.png

override fun onDraw(canvas: Canvas) {canvas.save()canvas.translate(paddingStart.toFloat(), paddingTop.toFloat())drawBackground(canvas)drawExperienceBar(canvas)canvas.restore()
}/*** 绘制经验条*/
private fun drawExperienceBar(canvas: Canvas) {val save = canvas.saveCountcanvas.save()canvas.translate(mLineLeft, mLineTop)//绘制经验条底部背景canvas.drawRoundRect(0F, 0F, mLineWidth, mLineHeight, mRadius, mRadius, mLinePaint)canvas.restoreToCount(save)
}

3.在轨道的基础上再画一个渐变色的经验条

还是drawExperienceBar这个方法,在绘制轨道之后,再调用一次drawRoundRect去绘制经验条,这里看起来没什么大的区别,但是需要注意的点有两个:

  1. 绘制轨道的第一个参数x0,从一开始的0F开始修改成了从经验条的右侧开始,这样做是为了尽可能减少过度绘制,也就是下面效果图的图1,轨道实际上只绘制了一部分,如果这里看不懂的话,可以直接忽略;
  2. 绘制渐变进度条的宽度是从0F开始,到mLineWidth * mExperiencePercent结束,mExperiencePercent是前面computerLevelInfo计算好的百分比,至于它为什么有渐变色,已经在setShaderColor设置了shader属性,支持了线性渐变。

效果图:
image.png
image.png

private fun drawExperienceBar(canvas: Canvas) {val save = canvas.saveCountcanvas.save()canvas.translate(mLineLeft, mLineTop)//绘制经验条底部背景canvas.drawRoundRect((mLineWidth * mExperiencePercent - mLineHeight).coerceAtLeast(0F),0F, mLineWidth, mLineHeight, mRadius, mRadius, mLinePaint)//绘制渐变的经验条canvas.drawRoundRect(0F, 0F, mLineWidth * mExperiencePercent, mLineHeight,mRadius, mRadius, mShaderPaint)canvas.restoreToCount(save)
}

4.在经验条的上层绘制等级分割点,已达成的等级大圆点,未达成就是小圆点

修改onDraw方法,添加drawLevelPoint方法。
drawLevelPoint方法循环去绘制每个等级的分割点,等级点的间距在上面的computerPointInterval已经计算好了,在循环里面会通过当前的经验进度是否大于等于当前等级的进度,如果是的话就是已达成的等级,否则就是未达成。
这里的drawCircleAPI可以让我们在画布上绘制一个圆,只要传入圆心坐标、半径和画笔即可。
效果图:
image.png

override fun onDraw(canvas: Canvas) {canvas.save()canvas.translate(paddingStart.toFloat(), paddingTop.toFloat())drawBackground(canvas)drawExperienceBar(canvas)drawLevelPoint(canvas)canvas.restore()
}/*** 绘制等级分割点*/
private fun drawLevelPoint(canvas: Canvas) {if (mLevelList.size > 1) {val save = canvas.saveCountcanvas.save()canvas.translate(mLineLeft, 0F)//等级圆点的圆心Y轴坐标(由于经验条是水平的,所以所有Y轴坐标都一样)val cy = mViewHeight / 2//总共有n - 1个等级圆点,所以从1开始画,已达成的等级大圆点,未达成就是小圆点for (level in 1 until mLevelList.size) {//当前等级是否已达成val achieved = mExperiencePercent >= mLevelPercent * levelcanvas.drawCircle(mPointInterval * level, cy,if (achieved) mLineHeight else mLineHeight / 2,if (achieved) mLevelAchievedPaint else mLevelNotAchievedPaint)}canvas.restoreToCount(save)}
}

加上经验变化的动画效果

动画这一块再定义一些变量,当我们设置经验条的经验数据时,内部就会调用startAnimator方法,通过ValueAnimator的回调,不断地去更新mExperiencePercent,然后刷新View,就可以实现经验条增加经验的动画了。

//动画相关
private var mAnimator : ValueAnimator? = null
//动画时长
private val mAnimatorDuration = 500L
//插值器
private val mInterpolator by lazy { DecelerateInterpolator() }
//动画值回调
private val mAnimatorListener by lazy {ValueAnimator.AnimatorUpdateListener {mExperiencePercent = it.animatedValue as Floatinvalidate()}
}/*** 开始经验条动画*/
private fun startAnimator(start : Float, end : Float) {mAnimator?.cancel()mAnimator = ValueAnimator.ofFloat(start, end).apply {duration = mAnimatorDurationinterpolator = mInterpolatoraddUpdateListener(mAnimatorListener)start()}
}
//动画相关

开放两个API给外部设置等级和经验信息

不管是哪种更新方式,都会调用startAnimator方法去启动动画修改。

/*** 外界更新经验*/
fun updateExperience(experience : Int) {if (mLevelList.isEmpty() || experience == mExperience) returnmExperience = experiencestartAnimator(mExperiencePercent, computerLevelInfo())
}/*** 外界设置等级信息*/
fun setLevelInfo(experience : Int, list : List<Int>) {mExperience = experiencemLevelList.clear()mLevelList.addAll(list)computerPointInterval()startAnimator(0F, computerLevelInfo())
}

自定义View的工作到这里就完成了!

外部的使用代码

XML:

<com.hbh.customview.view.ExperienceBarandroid:id="@+id/experience_bar"android:layout_width="match_parent"android:layout_height="wrap_content"android:paddingHorizontal="20dp"app:layout_constraintTop_toTopOf="parent"app:layout_constraintBottom_toBottomOf="parent"/><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"android:layout_marginVertical="16dp"app:layout_constraintTop_toBottomOf="@id/experience_bar"><Buttonandroid:id="@+id/btn_test1"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="test1"android:layout_gravity="center"/><Buttonandroid:id="@+id/btn_test2"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="test2"android:layout_gravity="center"/></LinearLayout>

Activity:

val experience_bar = findViewById<ExperienceBar>(R.id.experience_bar)val a = 5
val b = listOf(10,50,100,250,500,1000)
val btn_test1 = findViewById<Button>(R.id.btn_test1).apply {setOnClickListener {experience_bar.setLevelInfo(a, b)}
}
val btn_test2 = findViewById<Button>(R.id.btn_test2).apply {var index = 0val c = listOf(888, 188)setOnClickListener {experience_bar.updateExperience(c[(index++) % c.size])}
}

总结

10分钟过去了,这个简单的自定义View你拿下没有?
觉得不错的话,就不要吝啬你的点赞!
需要整份代码的话,下面链接自提。
代码链接 : github.MyCustomView

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

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

相关文章

用html+javascript打造公文一键排版系统8:附件及标题排版

最近工作有点忙&#xff0c;所 以没能及时完善公文一键排版系统&#xff0c;现在只好熬夜更新一下。 有时公文有包括附件&#xff0c;招照公文排版规范&#xff1a; 附件应当另面编排&#xff0c;并在版记之前&#xff0c;与公文正文一起装订。“附件”二字及附件顺序号用3号黑…

Python(四十六)列表

❤️ 专栏简介&#xff1a;本专栏记录了我个人从零开始学习Python编程的过程。在这个专栏中&#xff0c;我将分享我在学习Python的过程中的学习笔记、学习路线以及各个知识点。 ☀️ 专栏适用人群 &#xff1a;本专栏适用于希望学习Python编程的初学者和有一定编程基础的人。无…

【Java基础教程】(四十八)集合体系篇 · 上:全面解析 Collection、List、Set常用子接口及集合元素迭代遍历方式~【文末送书】

Java基础教程之集合体系 上 &#x1f539;本章学习目标1️⃣ 类集框架介绍2️⃣ 单列集合顶层接口&#xff1a;Collection3️⃣ List 子接口3.1 ArrayList 类&#x1f50d; 数组&#xff08;Array&#xff09;与列表&#xff08;ArrayList&#xff09;有什么区别?3.2 LinkedL…

在 ArcGIS Pro 中使用 H3 创建蜂窝六边形

H3是Uber开发的分层索引系统,它使用六边形来平铺地球表面。H3在二十面体(一个具有20个三角形面和12个顶点的形状)上构建其六边形网格。由于仅用六边形不可能平铺二十面体,因此每个分辨率需要12个五边形来完成网格。分层索引网格意味着每个六边形都可以细分为子单元六边形。…

给jupter设置新环境

文章目录 给jupternotebook设置新环境遇到的报错添加路径的方法 给jupternotebook设置新环境 # 先在anaconda界面新建环境 conda env list # 查看conda prompt下的有的环境变量 带星号的是当前活跃的 activate XXXX pip install ipykernel ipython ipython kernel install --u…

如何安装mmcv?官网解答

pip install -U openmim mim install mmcv

【高分论文密码】大尺度空间模拟预测与数字制图教程

详情点击链接&#xff1a;【高分论文密码】大尺度空间模拟预测与数字制图 一&#xff0c;R语言空间数据及数据挖掘关键技术 1、R语言空间数据及应用特点 1)R语言基础与数据科学 2)R空间矢量数据 3)R栅格数据 2、R语言空间数据挖掘关键技术 二&#xff0c;R语言空间数据高…

素描基础知识

素描基础入门 1.基础线条 1.1 握笔姿势及长线条 2.排线 2.1 不同姿势画排线 2.1.1 姿势画排线 2.1.2 用手腕画排线 2.1.3 小拇指画排线 2.1.4 叠加排线 2.1.5交叉排线 2.2 纸张擦法 2.3 排线学习榜样 2.4 四种常见的排线 3、定向连线 4、一点透视 4.1 透视的规律 4.2 焦点透视…

SpringCloudAlibaba:服务网关之Gateway的cors跨域问题

目录 一&#xff1a;解决问题 二&#xff1a;什么是跨域 三&#xff1a;cors跨域是什么&#xff1f; 一&#xff1a;解决问题 遇到错误&#xff1a; 前端请求时报错 解决&#xff1a; 网关中添加配置文件&#xff0c;注意springboot版本&#xff0c;添加配置。 springboo…

Hive 调优集锦(1)

一、前言 1.1 概念 Hive 依赖于 HDFS 存储数据&#xff0c;Hive 将 HQL 转换成 MapReduce 执行&#xff0c;所以说 Hive 是基于Hadoop 的一个数据仓库工具&#xff0c;实质就是一款基于 HDFS 的 MapReduce 计算框架&#xff0c;对存储在HDFS 中的数据进行分析和管理。 1.2 架…

删除每行中的最大值

给你一个 m x n 大小的矩阵 grid &#xff0c;由若干正整数组成。 执行下述操作&#xff0c;直到 grid 变为空矩阵&#xff1a; 从每一行删除值最大的元素。如果存在多个这样的值&#xff0c;删除其中任何一个。 将删除元素中的最大值与答案相加。 注意 每执行一次操作&…

HBase有写入数据,页面端显示无数据量

写了一个测试类&#xff0c;插入几条数据&#xff0c;测试HBase的数据量。很简单的功能&#xff0c;这就出现问题了。。网页端可以看到&#xff0c;能够看到读写请求&#xff0c;但是不管是内存、还是磁盘&#xff0c;都没有数据。 于是就想到去HDFS查看&#xff0c;也是有数据…

windows命令行

参考:https://blog.csdn.net/u014419722/article/details/130427423 1、 创建文件夹&#xff08;mkdir或md&#xff09; 创建单个文件&#xff1a;mkdir cmd_test 创建二级文件&#xff1a;mkdir cmd_test\456\123 创建多个文件&#xff1a;mkdir cmd_test\000 cmd_test\111 2…

idea快速运行vue项目

目录 一、前提 二、步骤 安装vue.js插件 添加脚本 进行如下配置 一、前提 安装好node.js环境并初始化完成和安装好依赖 二、步骤 安装vue.js插件 打开idea,然后在File–Settings–Plugins–Makerplace下找到vue.js插件,安装并重启idea 添加脚本 进行如下配置 在Sctipts中根…

Linux复习——基础知识

作者简介:一名云计算网络运维人员、每天分享网络与运维的技术与干货。 座右铭:低头赶路,敬事如仪 个人主页:网络豆的主页​​​​​ 1. 有关早期linux系统中 sysvin的init的7个级别描述正确的是( )[选择1项] A. init 1 关机状态 B. init 2 字符界面多用户模式 …

【MySQL进阶(三)】 InnoDB体系架构之内存池(buffer pool)

InnoDB体系架构之内存池 一、InnoDB 体系结构二、缓冲池 buffer pool内部结构free 链&#xff08;管理空闲缓冲页&#xff09;怎么知道数据页是否被缓存&#xff1f; flush 链表&#xff08;管理脏页&#xff09;1. 脏页2. 链表结构3. 刷盘时机 LRU 链表&#xff08;控制数据热…

影视行业案例 | 燕千云助力大地影院集团搭建智能一体化IT服务管理平台

影视行业过去三年受新冠肺炎疫情影响&#xff0c;经历了一定程度的冲击和调整&#xff0c;但也展现出了强大的韧性和潜力。2023年中国影视产业规模可能达到2600亿元左右&#xff0c;同比增长11%左右。影视行业的发展趋势主要表现在内容创新、模式创新和产业融合三个方面&#x…

ICC2如何计算Gate Count?

我正在「拾陆楼」和朋友们讨论有趣的话题&#xff0c;你⼀起来吧&#xff1f;知识星球入口 我们认为gate count等于standard cell(非physical only)总面积 / 最小驱动二输入与非门面积。 ICC2没有专门的命令去报告gate count&#xff0c;只能自己计算&#xff0c;使用report_d…

面试题:什么是闭包?

一、怎么理解闭包&#xff1f; 简单理解&#xff1a;闭包 内层函数 外层函数的变量 下面是一组简单的闭包代码&#xff1a; function outer() {let count 1function inner() {console.log(count)}inner() } outer()闭包有两个注意点&#xff1a; 闭包一定有return吗&#x…

如何利用设备数字化平台推动精益制造?

人工智能驱动技术的不断发展&#xff0c;尤其是基于机器学习的预测分析工具的使用&#xff0c;为制造业带来了全新的效率和价值水平。一直以来&#xff0c;精益生产&#xff08;也叫精益制造&#xff09;在制造业中扮演着重要角色&#xff0c;而现在通过与工业 4.0的融合&#…