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

news2024/9/20 19:58:53

先展示一下静态效果图

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

  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 + paddingBottom
    mViewHeight = mViewWidth / 20
    mRadius = mViewHeight

    mLineHeight = mViewHeight / 3
    mLineTop = (mViewHeight - mLineHeight) / 2

    mLineWidth = mViewWidth - mLineTop * 2
    mLineLeft = mLineTop
    setShaderColor()
    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.saveCount
    canvas.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.saveCount
    canvas.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.saveCount
        canvas.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 * level
            canvas.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 Float
        invalidate()
    }
}

/**
 * 开始经验条动画
 */
private fun startAnimator(start : Float, end : Float) {
    mAnimator?.cancel()
    mAnimator = ValueAnimator.ofFloat(start, end).apply {
        duration = mAnimatorDuration
        interpolator = mInterpolator
        addUpdateListener(mAnimatorListener)
        start()
    }
}
//动画相关

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

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

/**
 * 外界更新经验
 */
fun updateExperience(experience : Int) {
    if (mLevelList.isEmpty() || experience == mExperience) return
    mExperience = experience
    startAnimator(mExperiencePercent, computerLevelInfo())
}

/**
 * 外界设置等级信息
 */
fun setLevelInfo(experience : Int, list : List<Int>) {
    mExperience = experience
    mLevelList.clear()
    mLevelList.addAll(list)
    computerPointInterval()
    startAnimator(0F, computerLevelInfo())
}

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

外部的使用代码

XML:

<com.hbh.customview.view.ExperienceBar
    android: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"/>

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:layout_marginVertical="16dp"
    app:layout_constraintTop_toBottomOf="@id/experience_bar">

    <Button
        android:id="@+id/btn_test1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="test1"
        android:layout_gravity="center"/>

    <Button
        android: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 = 0
    val c = listOf(888, 188)
    setOnClickListener {
        experience_bar.updateExperience(c[(index++) % c.size])
    }
}

总结

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

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

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

相关文章

vue新学习 02 vue命令v-model,数据代理(作用域和作用域链),事件,监听,渲染,计算属性(也就是把操作属性的语句放到vue实例中)

双向绑定用命令v-model&#xff1a; v-bind的命令是单项去绑定data中的相关属性&#xff0c;此时的data是真正的data&#xff0c;并没有用变量声明的方式去接收vue实例对象&#xff0c;也就是例如用const vm new Vue({})。而是直接就采用了new Vue&#xff08;{}&#xff09;这…

6、Kubernetes核心技术 - Pod

目录 一、概述 二、Pod机制 2.1、共享网络 2.2、共享存储 三、Pod资源清单 四、 Pod 的分类 五、Pod阶段 六、Pod 镜像拉取策略 ImagePullBackOff 七、Pod 资源限制 八、容器重启策略 一、概述 Pod 是可以在 Kubernetes 中创建和管理的、最小的可部署的计算单元。P…

oracle12c静默安装

目录 前言 安装配置步骤 关闭防火墙,禁止防火墙开机自启 关闭selinux yum安装必要安装包 内网环境下载依赖包的方式 创建用户和组 创建oinstall和dba组 创建oracle用户 设置oracle密码 查看创建结果 修改内核参数 使配置生效 修改用户及文件限制 改文件限制 修改用…

ol.Overlay+gif动画及隐藏问题

隐藏要使用css比较彻底&#xff0c;例如$("#markerLbs").hide()。 如果使用markerLbs.setVisible(false)&#xff0c;当地图刷新后再次显示&#xff0c;可能我的用法不对&#xff0c;欢迎指教。 我的demo是一个gif动画&#xff0c;当zoom变化时控制gif显隐&#xf…

防汛备汛 | EasyV数字孪生可视化防汛应急解决方案护平安!

一、方案背景 汛期是指季节性降雨增多&#xff0c;河流湖泊水位上涨的时期。当前&#xff0c;我国正式进入“七下八上”防汛关键期&#xff0c;多地进入主汛期。入汛以来&#xff0c;我国经历了18次强降雨过程&#xff0c;21个省份271条河流发生超警以上洪水。其中南方地区受台…

Linux文件和目录的777、755、644权限解释

1.文件权限 在linux系统中,文件或目录的权限可以分为3种: r:4 读 w:2 写 x:1 执行(运行) &#xff0d;&#xff1a;对应数值0 数字 4 、2 和 1表示读、写、执行权限 rwx 4 2 1 7 (可读写运行&#xff09; rw 4 2 6 &#xff08;可读写不可运行&#xff09; rx …

vue3+ts+vite项目中使用@时报错:找不到模块,两步教你快速解决

亲爱的小伙伴&#xff0c;如果你也在vue3tsvite项目中使用时报错&#xff1a;找不到模块的话&#xff0c;如下图情况&#xff0c;不要担心&#xff0c;不要害怕&#xff0c;两步教你解决&#xff01; 1.找到tsconfig.json文件 2.添加如下代码即可 代码如下&#xff1a; "…

SpringMVC中的返回值是什么?

1.谈谈你对SpringMVC的理解 SpringMVC是属于Spring Framework生态里面的一个模块&#xff0c;它是在Servlet的基础上构建并且使用了mvc模式设计的一个Web框架它的主要目的是为了去简化传统的ServletJSP模式下的Web开发方式&#xff0c;其次Spring mvc 的整个架构设计&#xff0…

建木使用进阶-创建密钥管理

阿丹&#xff1a; 第一次我们进入建木&#xff0c;第一件事情就是配置我们相关的密钥。 解读&#xff1a; 在建木中我们可以进行创建密钥来对我们服务器等密码进行方便的管理。 注意&#xff1a; 登录的时候账号为&#xff1a;admin 密码为&#xff1a;123456 这是初始…

【javaSE】 数组的定义与使用

目录 数组的基本概念 为什么要使用数组 什么是数组 数组的创建及初始化 数组的创建 数组的初始化 动态初始化 静态初始化 注意事项 数组的使用 数组中元素访问 注意事项 遍历数组 数组是引用类型 初识JVM的内存分布 基本类型变量与引用类型变量的区别 再谈引用…

java自定义导出excel

导出Excel时候&#xff0c;表头需要自定义&#xff0c;只能自己重新写导出代码了 1、引入包 <dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>2.1.6</version> </dependency>2、工具…

优化 SQL 体验:五款 AI 驱动工具助力 SQL 查询

SQL AI 工具能够极大提高跟数据和数据库打交道人员&#xff08;e.g. 数据分析师、数据科学家、数据库管理员、开发者&#xff09;的效率和生产力&#xff0c;比如可以根据自然语言输入自动生成 SQL 查询&#xff08;非技术用户也能轻松访问和分析数据了&#xff01;&#xff09…

为公网SSH远程Ubuntu配置固定的公网TCP端口地址主图

文章目录 为公网SSH远程Ubuntu配置固定的公网TCP端口地址 为公网SSH远程Ubuntu配置固定的公网TCP端口地址 在上篇文章中&#xff0c;我们通过cpolar建立的临时TCP数据隧道&#xff0c;成功连接了位于其他局域网下的Ubuntu系统&#xff0c;实现了不同操作系统、不同网络下的系统…

Web端即时通讯技术(SEE,webSocket)

目录 背景简介个人见解被动推送轮询简介实现 长轮询&#xff08;comet&#xff09;简介实现 比较 主动推送长连接&#xff08;SSE&#xff09;简介实现GETPOST 效果 webSocket简介WebSocket的工作原理:WebSocket的主要优点:WebSocket的主要缺点: 实现用法一用法二 **效果** 比较…

指定的驱动器号对于分配不可用怎么办?

驱动器号是计算机上驱动器的字母标识符也叫做“盘符”&#xff0c;例如内部硬盘、光驱、SD卡和外部U盘在文件资源管理器中可以直接看到的标志&#xff0c;如我们平常所说的C盘、D盘、H盘等。如果硬盘丢失了驱动器号&#xff0c;您将无法直接访问其中保存的文件&#xff0c;并且…

【小白必看】Python词云生成器详细解析及代码实现

文章目录 前言导入所需库将存好的Excel词频表读取成字典循环处理每个词频Excel文件定义词云样式和生成词云图完整代码运行效果截图知识点 结束语 前言 本文介绍了如何使用Python编写代码来生成词云图。在生成词云图之前&#xff0c;我们需要导入一些必需的库&#xff0c;包括nu…

中断控制器的驱动解析

这里主要分析 linux kernel 中 GIC v3 中断控制器的代码(drivers/irqchip/irq-gic-v3.c)。 设备树 先来看下一个中断控制器的设备树信息&#xff1a; gic: interrupt-controller51a00000 {compatible "arm,gic-v3";reg <0x0 0x51a00000 0 0x10000>, /* GI…

Python(四十四)嵌套循环

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

【C++详解】——智能指针

目录 为什么需要智能指针 抛异常引发内存泄漏 内存泄漏 什么是内存泄漏&#xff0c;内存泄漏的危害 内存泄漏分类 检测内存泄漏常用工具 如何避免内存泄漏 智能指针的使用及原理 RAII 智能指针的原理 各类智能指针介绍 auto_ptr unique_ptr shared_ptr weak_ptr …

vue3-Cannot use ‘in‘ operator to search for ‘path‘ in undefined

在创建vue3的路由时&#xff0c;报了这样的错&#xff1a;Cannot use ‘in’ operator to search for ‘path’ in undefined&#xff0c;经过多次排查发现是我在路由文件里面用错了createWebHashHistory()方法&#xff0c;将它用成了 变量。 一、报错情况 二、报错原因及解…