先展示一下静态效果图
介绍一下我们的实现流程:
- 首先整个经验条有一个圆角边框的背景打底;
- 然后给经验条绘制一条轨道,让用户比较直观地看到总进度的长度;
- 在轨道的上层绘制我们的渐变色经验条;
- 在经验条的上层绘制等级分割点,已达成的等级大圆点,未达成就是小圆点;
- 最终再给View加上经验变化的动画效果就实现了。
动态与静态效果图:
按流程实现效果
0.准备工作
定义需要用到的变量
重要的变量都加上注释了,没有注释的就是我觉得没必要注释🚀
ini
//整个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,完成测量工作。
kotlin
/**
* 测量各种尺寸
*/
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进行偏移,移除内边距对我们绘制的影响。
save 与restore 这两个API是成对使用的,save会保存画布的当前状态,然后我们就可以对画布进行偏移、旋转和缩放等操作,等我们绘制完之后再调用restore,就可以使用画布回到之前的状态了。
translate 方法可以对画布进行偏移,偏移之后我们的所有绘制操作就都基于偏移后的坐标了。
drawBackground 实现了底部边框的绘制,只有一行代码,实现非常简单。
drawRoundRect 这个API的效果就是绘制一个圆角矩形,传入四个边角坐标、圆角和画笔即可。
效果图:
kotlin
/**
* 绘制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 里面也做了一个画布的偏移,然后绘制一个圆角矩形,跟上面的很相似,相信大家都能看懂。
效果图:
kotlin
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去绘制经验条,这里看起来没什么大的区别,但是需要注意的点有两个:
- 绘制轨道的第一个参数x0,从一开始的0F开始修改成了从经验条的右侧开始,这样做是为了尽可能减少过度绘制,也就是下面效果图的图1,轨道实际上只绘制了一部分,如果这里看不懂的话,可以直接忽略;
- 绘制渐变进度条的宽度是从0F开始,到mLineWidth * mExperiencePercent结束,mExperiencePercent是前面computerLevelInfo 计算好的百分比,至于它为什么有渐变色,已经在setShaderColor设置了shader属性,支持了线性渐变。
效果图:
kotlin
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已经计算好了,在循环里面会通过当前的经验进度是否大于等于当前等级的进度,如果是的话就是已达成的等级,否则就是未达成。
这里的drawCircle API可以让我们在画布上绘制一个圆,只要传入圆心坐标、半径和画笔即可。
效果图:
scss
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,就可以实现经验条增加经验的动画了。
kotlin
//动画相关
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方法去启动动画修改。
kotlin
/**
* 外界更新经验
*/
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:
ini
<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:
ini
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