1. 程式人生 > >自定義View-餅狀圖(百分比圖)

自定義View-餅狀圖(百分比圖)

在這裡插入圖片描述

按照設定的百分比陣列,設定的百分比陣列之和要等於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>