自定义View-饼状图(百分比图)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/mengks1987/article/details/84135481

在这里插入图片描述

按照设置的百分比数组,设置的百分比数组之和要等于1,如果大于1会出现覆盖的情况。
这个效果里面有2个关注点:
1、通过百分比数组绘制多大的环。
2、切换比例的时候动画效果。
3、正在变化时接收到百分比变化的处理

1、通过百分比数组绘制多大的环

圆环一圈是360度,如果百分比是0.2,那么圆环就绘制360 * 0.2 = 72,起始位置是在前面进度的结尾处。代码如下:

var startAngle = -90F
        for (i in 0 until mPercentages.size) {
            if (i > mPercentageColors.size - 1) {
                //如果没有为每一段百分比设置颜色值,则使用最后一种颜色值
                mPaint.color = mPercentageColors[mPercentageColors.size - 1]
            } else {
                mPaint.color = mPercentageColors[i]
            }
            val sweepAngle = mPercentages[i] * 360
            canvas?.drawArc(mArcRectF, startAngle, sweepAngle, false, mPaint)
            startAngle += sweepAngle
        }

2、切换比例的时候动画效果

1、补全数组

2个不同的百分比数组有可能长度不同,我们要较短那个后面补上0

/**
     * 补全数组,使2个数组的长度相等
     * fromValues : [0.1,0.2,0.4,0.3]
     *    toValues: [0.1,0.2,0.7]
     *
     * 将toValues变为:[0.1,0.2,0.7,0]
     */
    private fun completionArrays(fromValues: Array<Float>, toValues: Array<Float>): Values {
        return when {
            fromValues.size == toValues.size -> Values(fromValues, toValues)
            fromValues.size > toValues.size -> {
                //补全toValues
                var newValues: Array<Float> = Array(fromValues.size) { 0F }
                for (i in 0 until toValues.size) {
                    newValues[i] = toValues[i]
                }
                Values(fromValues, newValues)
            }
            else -> {
                //fromValues
                var newValues: Array<Float> = Array(toValues.size) { 0F }
                for (i in 0 until fromValues.size) {
                    newValues[i] = fromValues[i]
                }
                Values(newValues, toValues)
            }
        }
    }
    

Values是一个内部类:

data class Values(val fromValues: Array<Float>, val toValues: Array<Float>)

2、开启一个动画,根据动画的进度计算当前百分比

/**
     * 启动动画
     *
     */
    private fun startAnimator(fromValues: Array<Float>, toValues: Array<Float>) {
        isChange = true
        val (fv, tv) = completionArrays(fromValues, toValues)
        val animator = ValueAnimator.ofFloat(0F, 1F)
        animator.duration = mAnimDuration
        animator.addUpdateListener {
            computePercentages(it.animatedValue as Float, fv, tv)
            postInvalidate()
            if (it.animatedValue as Float == 1F) {
                //动画结束
                isChange = false
//                val list = mPercentages.filter { it > 0 }
//                mPercentages = Array(list.size) { i -> list[i] }
                mNextToValues?.let { startAnimator(mPercentages, mNextToValues!!) }
                mNextToValues = null
            }
        }
        animator.start()
    }
/**
     * 计算当前动画百分比的值
     */
    private fun computePercentages(animatedValue: Float, fromValues: Array<Float>, toValues: Array<Float>) {
        var newValues = Array(fromValues.size) { 0F }
        for (i in 0 until fromValues.size) {
            newValues[i] = fromValues[i] - (fromValues[i] - toValues[i]) * animatedValue
        }
        mPercentages = newValues
    }

根据当前的进度实时绘制饼状图。

3、正在变化时接收到百分比变化的处理

如果当前正在变化,又接收到百分比变化,则记录此次变化,等上个动画结束时马上再次启动动画。

整体code:
PieChart.kt:

class PieChart @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0) : View(context, attrs, defStyleAttr, defStyleRes) {

    /**
     * 百分比数组,数组和要=1
     */
    private var mPercentages: Array<Float> = arrayOf(1.0F)
    /**
     * 不同百分比的颜色值
     */
    private var mPercentageColors: Array<Int> = arrayOf(Color.parseColor("#fff4e0"),
            Color.parseColor("#f8b500"),
            Color.parseColor("#ff4d4d"),
            Color.parseColor("#42d3b7"),
            Color.parseColor("#334d5c"))

    /**
     * 圆环画笔
     */
    private var mPaint: Paint = Paint()
    /**
     * 圆环的大小
     */
    private var mArcRectF: RectF = RectF()
    /**
     * 圆环宽度
     */
    private var mStrokeWidth: Float = 20F

    /**
     * 圆环变化时是否使用动画
     */
    private var mUseAnimation: Boolean = true
    /**
     * 动画时长,单位ms
     */
    private var mAnimDuration: Long = 500L

    /**
     * 圆环正在改变时又有新的数据来记录下来,等圆环改变结束在刷新新的数据
     */
    private var mNextToValues: Array<Float>? = null
    private var isChange = false

    init {
        val a = context.theme.obtainStyledAttributes(attrs, R.styleable.PercentageRing, defStyleAttr, 0)
        mStrokeWidth = a.getDimension(R.styleable.PercentageRing_stroke_width, 10F)
        //设置圆环画笔
        mPaint.style = Paint.Style.STROKE
        mPaint.strokeWidth = mStrokeWidth
        mPaint.isAntiAlias = true
    }

    override fun onDraw(canvas: Canvas?) {
        drawRing(canvas)
    }

    /**
     * 绘制圆环
     *
     * @param canvas 画布
     */
    private fun drawRing(canvas: Canvas?) {
        var ringWidth = width
        var height = height
        mArcRectF.left = mStrokeWidth / 2
        mArcRectF.right = width - mStrokeWidth / 2
        mArcRectF.top = mStrokeWidth / 2
        mArcRectF.bottom = height - mStrokeWidth / 2
        var startAngle = -90F
        for (i in 0 until mPercentages.size) {
            if (i > mPercentageColors.size - 1) {
                //如果没有为每一段百分比设置颜色值,则使用最后一种颜色值
                mPaint.color = mPercentageColors[mPercentageColors.size - 1]
            } else {
                mPaint.color = mPercentageColors[i]
            }
            val sweepAngle = mPercentages[i] * 360
            canvas?.drawArc(mArcRectF, startAngle, sweepAngle, false, mPaint)
            startAngle += sweepAngle
        }
    }


    /**
     * 设置百分比数据和中间显示文案
     *
     * @param percentages 百分比
     */
    fun setPercentages(percentages: Array<Float>) {
        if (mUseAnimation) {
            if (isChange) {
                mNextToValues = percentages
                return
            }
            startAnimator(mPercentages, percentages)
        } else {
            mPercentages = percentages
            postInvalidate()
        }
    }

    fun setPercentages(percentages: Array<Float>, colors: Array<Int>) {
        mPercentages = percentages
        setColors(colors)
        postInvalidate()
    }

    /**
     * 设置圆环颜色
     *
     * @param colors 颜色RGB值
     */
    fun setColors(colors: Array<Int>) {
        mPercentageColors = colors
    }

    /**
     * 启动动画
     *
     */
    private fun startAnimator(fromValues: Array<Float>, toValues: Array<Float>) {
        isChange = true
        val (fv, tv) = completionArrays(fromValues, toValues)
        val animator = ValueAnimator.ofFloat(0F, 1F)
        animator.duration = mAnimDuration
        animator.addUpdateListener {
            computePercentages(it.animatedValue as Float, fv, tv)
            postInvalidate()
            if (it.animatedValue as Float == 1F) {
                //动画结束
                isChange = false
//                val list = mPercentages.filter { it > 0 }
//                mPercentages = Array(list.size) { i -> list[i] }
                mNextToValues?.let { startAnimator(mPercentages, mNextToValues!!) }
                mNextToValues = null
            }
        }
        animator.start()
    }

    /**
     * 补全数组,使2个数组的长度相等
     * fromValues : [0.1,0.2,0.4,0.3]
     *    toValues: [0.1,0.2,0.7]
     *
     * 将toValues变为:[0.1,0.2,0.7,0]
     */
    private fun completionArrays(fromValues: Array<Float>, toValues: Array<Float>): Values {
        return when {
            fromValues.size == toValues.size -> Values(fromValues, toValues)
            fromValues.size > toValues.size -> {
                //补全toValues
                var newValues: Array<Float> = Array(fromValues.size) { 0F }
                for (i in 0 until toValues.size) {
                    newValues[i] = toValues[i]
                }
                Values(fromValues, newValues)
            }
            else -> {
                //fromValues
                var newValues: Array<Float> = Array(toValues.size) { 0F }
                for (i in 0 until fromValues.size) {
                    newValues[i] = fromValues[i]
                }
                Values(newValues, toValues)
            }
        }
    }

    /**
     * 计算当前动画百分比的值
     */
    private fun computePercentages(animatedValue: Float, fromValues: Array<Float>, toValues: Array<Float>) {
        var newValues = Array(fromValues.size) { 0F }
        for (i in 0 until fromValues.size) {
            newValues[i] = fromValues[i] - (fromValues[i] - toValues[i]) * animatedValue
        }
        mPercentages = newValues
    }

    data class Values(val fromValues: Array<Float>, val toValues: Array<Float>)

    companion object {
        const val TAG = "PieChart"
    }
}

自定义属性 attr.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="PieChart">
        <attr name="stroke_width" format="dimension" />
    </declare-styleable>
</resources>

使用及测试 TestActivity.kt :

class TestActivity : AppCompatActivity() {

    val handler = object : Handler() {
        override fun handleMessage(msg: Message?) {
            pieChart.setPercentages(list[index++ % list.size])
            sendEmptyMessageDelayed(0, 1000)
        }
    }

    var list = arrayListOf<Array<Float>>(
            arrayOf(0.1F, 0.2F, 0.3F, 0.4F),
            arrayOf(0.2F, 0.3F, 0.2F, 0.3F),
            arrayOf(0.5F, 0.5F),
            arrayOf(0.1F, 0.2F, 0.3F, 0.4F),
            arrayOf(0.3F, 0.5F, 0.2F),
            arrayOf(0.1F, 0.8F, 0.1F)
    )
    var index = 0
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_test)
        handler.sendEmptyMessage(0)
    }
}

布局文件 activity_test.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <PieChart
        android:id="@+id/pieChart"
        android:layout_width="300dp"
        android:layout_height="300dp"
        app:stroke_width="@dimen/dp_11"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>

</android.support.constraint.ConstraintLayout>

猜你喜欢

转载自blog.csdn.net/mengks1987/article/details/84135481