先展示一下静态作用图

介绍一下咱们的完成流程:

  1. 首先整个经历条有一个圆角边框的布景打底;
  2. 然后给经历条制作一条轨迹,让用户比较直观地看到总进展的长度;
  3. 在轨迹的上层制作咱们的突变色经历条;
  4. 在经历条的上层制作等级切割点,已达到的等级大圆点,未达到便是小圆点;
  5. 终究再给View加上经历变化的动画作用就完成了。

动态与静态作用图:

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

按流程完成作用

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的作用便是制作一个圆角矩形,传入四个边角坐标、圆角和画笔即可。

作用图:

/**
 * 制作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里边也做了一个画布的偏移,然后制作一个圆角矩形,跟上面的很类似,信任大家都能看懂。
作用图:

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

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特点,支持了线性突变。

作用图:

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

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能够让咱们在画布上制作一个圆,只要传入圆心坐标、半径和画笔即可。
作用图:

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

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