美文网首页
Android(Kotlin) PieChartView(环形饼

Android(Kotlin) PieChartView(环形饼

作者: 想看烟花么 | 来源:发表于2021-05-26 22:31 被阅读0次

    代码的世界虽逻辑繁华,却又大道至简。

    画环形饼图常见的大概有两种画法:

    1.画个半径略小的圆覆盖掉中心
    设置userCenter = true,
    然后调用drawArc(RectF,startAngle,sweep,userCenter,Paint),
    最后drawCircle(...) 就会呈现环状了
    参考资料:
    https://www.jianshu.com/p/c9a12370631d

    2.把画笔设置成描边模式并设置线条宽度及userCenter = false,
    mPiePaint.style = Paint.Style.STROKE
    mPiePaint.textAlign = Paint.Align.LEFT
    mPiePaint.strokeWidth = dp2px(21f)
    然后调用drawArc(RectF,startAngle,sweep,userCenter,Paint)(本篇采用此方式)

    动画思路来自:

    https://blog.csdn.net/petterp/article/details/84928711
    只需要明白,如果存在多个颜色的话,在绘制第二个以后颜色时,每次都要先绘制先前所有颜色,再绘制当前颜色,即可理解,这也就是动画的基本逻辑。

    效果图:

    image.png

    自定义参数

     <!-- PieChartView -->
        <declare-styleable name="PieChartView">
            <!-- outer radius -->
            <attr name="pie_chart_outer_radius" format="dimension" />
            <!--  ring width -->
            <attr name="pie_chart_ring_width" format="dimension" />
            <!-- line length of annotation -->
            <attr name="pie_chart_line_length" format="dimension" />
            <!-- text size of description -->
            <attr name="pie_chart_text_size" format="dimension" />
            <!-- blank top and bottom-->
            <attr name="pie_chart_blank_top_bottom" format="dimension" />
            <!-- blank of left and right -->
            <attr name="pie_chart_blank_left_right" format="dimension" />
            <!-- margin of left,top,right,bottom -->
            <attr name="pie_chart_margin" format="dimension" />
        </declare-styleable>
    

    使用

    <com.patrick.moti.PieChartView
                android:id="@+id/pie_chart_view"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:pie_chart_blank_left_right="66dp"
                app:pie_chart_blank_top_bottom="50dp"
                app:pie_chart_line_length="12dp"
                app:pie_chart_outer_radius="106dp"
                app:pie_chart_margin="8dp"
                app:pie_chart_ring_width="21dp"
                app:pie_chart_text_size="12sp" />
    
    val pieChartView = findViewById<PieChartView>(R.id.pie_chart_view)
            val dataList = mutableListOf<PieChartView.PieData>()
            dataList.add(
                PieChartView.PieData(
                    "Abcdefghijklmn & opqrstuvwxyz1",
                    45.00,
                    "#FFFF00"
                )
            )
            dataList.add(
                PieChartView.PieData(
                    "Other1",
                    90.00,
                    "#FF0099"
                )
            )
            dataList.add(
                PieChartView.PieData(
                    "Abcdefghijklmn & opqrstuvwxyz2",
                    135.00,
                    "#FF9900"
                )
            )
            dataList.add(
                PieChartView.PieData(
                    "Other2",
                    180.00,
                    "#FF5678"
                )
            )
            dataList.add(
                PieChartView.PieData(
                    "abcdefghijklmn & opqrstuvwxyz3",
                    225.00,
                    "#FF2345"
                )
            )
            dataList.add(
                PieChartView.PieData(
                    "Shoping5",
                    270.00,
                    "#FF00FF"
                )
            )
            dataList.add(
                PieChartView.PieData(
                    "abcdefghijklmn & opqrstuvwxyz4",
                    315.00,
                    "#FF8828"
                )
            )
            pieChartView.initData(dataList, 1260.00, "$")
    

    实现

    package com.example.view
    
    import android.animation.ValueAnimator
    import android.content.Context
    import android.graphics.*
    import android.os.Build
    import android.text.Layout
    import android.text.StaticLayout
    import android.text.TextPaint
    import android.util.AttributeSet
    import android.util.Log
    import android.view.View
    import com.example.laboratory.R
    import com.example.tools.AmountFormatUtil
    import kotlin.math.abs
    import kotlin.math.cos
    import kotlin.math.max
    import kotlin.math.sin
    
    
    /**
     * Pie Chart View
     * @author patrick
     * @date 6/28/21
     */
    open class PieChartView(context: Context, attributes: AttributeSet?) : View(context, attributes) {
        private var mHasInit = false
        private var mPieDataList: List<IPieData>? = null
        private var mTotalNumber: Double = 0.0
        private var mCoinSymbol: String = "$"
        private var mOuterRadius = 0f
        private var mRingWidth = 0f
        private var mLineLength = 0f
        private var mTextSize = 0f
        private var mBlankLeftAndRight = 0f
        private var mBlankTopAndBottom = 0f
        private var mMargin = 0f
    
        //paint
        private var mPiePaint = Paint()
        private var mLinePaint = Paint()
        private var mTextPaint = TextPaint()
    
        //draw ring.
        private var mValueAnimator = ValueAnimator()
        private var mCurrentAngle = 0f
        private var mCursor = 0
        private val mCircleRectF = RectF()
        private var mCX = 0f
        private var mCY = 0f
        private var mPieLeft = 0f
        private var mPieTop = 0f
        private var mPieRight = 0f
        private var mPieBottom = 0f
    
        //resource of draw.
        private lateinit var mStartAngleArray: Array<Float>
        private lateinit var mColorArray: Array<Int>
        private lateinit var mTextRectArray: Array<TextCalculateParams?>
    
        companion object {
            const val MAX_ANGLE = 360f
            const val COLOR_EMPTY = "#D7D8D8"
        }
    
        init {
            val typeArray = context.obtainStyledAttributes(attributes, R.styleable.PieChartView)
            mOuterRadius =
                typeArray.getDimension(R.styleable.PieChartView_pie_chart_outer_radius, dp2px(106f))
            mRingWidth =
                typeArray.getDimension(R.styleable.PieChartView_pie_chart_ring_width, dp2px(21f))
            mLineLength =
                typeArray.getDimension(R.styleable.PieChartView_pie_chart_line_length, dp2px(12f))
            mTextSize = typeArray.getDimension(R.styleable.PieChartView_pie_chart_text_size, dp2px(12f))
            mMargin = typeArray.getDimension(
                R.styleable.PieChartView_pie_chart_margin,
                dp2px(8f)
            )
            mBlankLeftAndRight =
                typeArray.getDimension(R.styleable.PieChartView_pie_chart_blank_left_right, dp2px(66f))
            mBlankTopAndBottom =
                typeArray.getDimension(
                    R.styleable.PieChartView_pie_chart_blank_top_bottom,
                    dp2px(50f)
                )
            typeArray.recycle()
            //default circleX and circleY
            mPieLeft = mBlankLeftAndRight + mMargin
            mPieTop = mBlankTopAndBottom + mMargin
            mCX = mPieLeft + mOuterRadius
            mCY = mPieTop + mOuterRadius
            mPieRight = mCX + mOuterRadius
            mPieBottom = mCY + mOuterRadius
        }
    
        fun initData(
            pieDataList: List<IPieData>,
            total: Double,
            typeface: Typeface? = null,
            coinSymbol: String? = null
        ) {
            mPieDataList = pieDataList
            coinSymbol?.let {
                mCoinSymbol = coinSymbol
            }
            val pieDataSize = pieDataList.size
    
            initPaint(typeface)
    
            initCollection(pieDataSize)
    
            handleLogicThenInitCircleRectF(pieDataSize, total, pieDataList)
            //reset.
            mCursor = 0
            mHasInit = true
            //start draw with animation.
            startDrawWithAnimation(pieDataSize)
        }
    
        private fun initPaint(typeface: Typeface?) {
            mPiePaint.isAntiAlias = true
            mPiePaint.style = Paint.Style.STROKE
            mPiePaint.strokeWidth = mRingWidth
    
            mLinePaint.style = Paint.Style.STROKE
            mLinePaint.isAntiAlias = true
            mLinePaint.strokeWidth = dp2px(1f)
    
            mTextPaint.style = Paint.Style.FILL
            mTextPaint.isAntiAlias = true
            mTextPaint.textAlign = Paint.Align.LEFT
            mTextPaint.textSize = mTextSize
            typeface?.let {
                mTextPaint.typeface = typeface
            }
        }
    
        private fun initCollection(dataListSize: Int) {
            mStartAngleArray = Array(
                dataListSize + 1
            ) { 0f }
            mStartAngleArray[0] = 0f
            val defaultResSize = if (dataListSize > 0) {
                dataListSize
            } else {
                1
            }
            mColorArray = Array(defaultResSize) { Color.parseColor(COLOR_EMPTY) }
            mTextRectArray = Array(defaultResSize) { null }
        }
    
        private fun handleLogicThenInitCircleRectF(
            pieDataSize: Int,
            total: Double,
            pieDataList: List<IPieData>
        ) {
            //Collect data
            mTotalNumber = 0.0
            if ((pieDataSize == 1 && pieDataList[0] is PieData) || pieDataSize > 1) {
                if (total <= 0.0) {
                    GDLog.w("pie_chart_view", "total value <= 0 is limited,please have a check.")
                }
                for (i in pieDataList.indices) {
                    val pieData = pieDataList[i] as PieData
                    mTotalNumber += pieData.valueItem
                }
                if (mTotalNumber <= 0.0) {
                    GDLog.e("pie_chart_view", "mTotalNumber value <= 0 is limited,please have a check.")
                    return
                }
                var mMaxBlankTopAndBottom = mBlankTopAndBottom
                var maxPercentage = 0.0
                var allAdjustPercentage = 0.0
                var adjustCount = 0
                var needFix = false
                for (i in pieDataList.indices) {
                    val pieData = pieDataList[i] as PieData
                    val percentage = if (pieDataSize == 1) {
                        1.0
                    } else {
                        (pieData.valueItem / mTotalNumber)
                    }
                    maxPercentage = max(maxPercentage, percentage)
                    if (percentage > 0 && percentage < 0.01) {
                        needFix = true
                        adjustCount += 1
                        allAdjustPercentage += percentage
                    }
                }
                for (i in pieDataList.indices) {
                    val pieData = pieDataList[i] as PieData
                    val percentage = if (pieDataSize == 1) {
                        1.0
                    } else {
                        (pieData.valueItem / mTotalNumber)
                    }
                    val newPercent = when {
                        percentage > 0 && percentage < 0.01 -> {
                            0.01
                        }
                        needFix && percentage == maxPercentage -> {
                            needFix = false
                            maxPercentage + allAdjustPercentage - 0.01 * adjustCount
                        }
                        else -> {
                            percentage
                        }
                    }
                    val pieAngle = (newPercent * MAX_ANGLE).toFloat()
                    mStartAngleArray[i + 1] = mStartAngleArray[i] + pieAngle
                    mColorArray[i] = Color.parseColor(pieData.color)
    
                    val halfAngle: Float? = when {
                        newPercent > 0.9 -> {
                            -90f
                        }
                        newPercent < 0.05 -> {
                            //do not draw line and text,so return null as mark.
                            null
                        }
                        else -> {
                            -mStartAngleArray[i] - pieAngle / 2f
                        }
                    }
                    halfAngle?.let { textAngle ->
                        //params for line draw.
                        val toRadians = textAngle * Math.PI / 180
                        val lineEndX: Float =
                            ((mOuterRadius + mLineLength) * cos(toRadians) + mCX).toFloat()
                        //params for text draw.
                        val tempLineEndY: Float =
                        ((mOuterRadius + mLineLength) * sin(toRadians) + mCY).toFloat()
                        val toEdgeWidth = mCX - abs(lineEndX - mCX) - mMargin
                        val allowMaxWidth = if(tempLineEndY > mPieBottom || tempLineEndY < mPieTop){
                            //It's between quadrant 1 and 2 || quadrant 3 and 4
                            toEdgeWidth + (toEdgeWidth - toEdgeWidth * abs(cos(toRadians)))
                        }else{
                            toEdgeWidth.toDouble()
                        }
                        val categoryName = getTruncatedString(pieData.categoryName, allowMaxWidth)
                        val valueItem =
                            getTruncatedString(
                                AmountFormatUtil.formatWithoutDollarSign(pieData.valueItem),
                                allowMaxWidth,
                                AmountFormatUtil.AMOUNT_SYMBOLS_POSITIVE
                            )
                        val maxTextWidth = max(
                            mTextPaint.measureText(categoryName),
                            mTextPaint.measureText(valueItem)
                        ).toInt()
                        val text = "${categoryName}\n${valueItem}"
    
                        val textLayout = StaticLayout.Builder.obtain(
                            text,
                            0,
                            text.length,
                            mTextPaint,
                            maxTextWidth
                        )
                            .setIncludePad(false)
                            .setAlignment(Layout.Alignment.ALIGN_CENTER)
                            .build()
                        mTextRectArray[i] =
                            TextCalculateParams(
                                staticLayout = textLayout,
                                radians = toRadians,
                                lineRectF = RectF(),
                                textX = 0f,
                                textY = 0f
                            )
                        //mMaxBlankTopAndBottom = max(mMaxBlankTopAndBottom, textLayout.height.toFloat()+mLineLength)
                    } ?: kotlin.run {
                        mTextRectArray[i] = null
                    }
                }
                mCY = mMaxBlankTopAndBottom + mOuterRadius + mMargin
                //mCY updated then do prepare for drawing line and text content.
                //mCX don't need to update,because text content will \n automatically if beyond the left-right edge.
                for (i in mTextRectArray.indices) {
                    mTextRectArray[i]?.let {
                        val lineStartX: Float = (mOuterRadius * cos(it.radians) + mCX).toFloat()
                        val lineEndX: Float =
                            ((mOuterRadius + mLineLength) * cos(it.radians) + mCX).toFloat()
    
                        val lineStartY: Float = (mOuterRadius * sin(it.radians) + mCY).toFloat()
                        val lineEndY: Float =
                            ((mOuterRadius + mLineLength) * sin(it.radians) + mCY).toFloat()
                        it.lineRectF = RectF(lineStartX, lineStartY, lineEndX, lineEndY)
                        val textX =
                            lineEndX + (it.staticLayout.width / 2) * cos(it.radians) - it.staticLayout.width / 2
                        val textY =
                            ((mOuterRadius + mLineLength + it.staticLayout.height / 2) * sin(it.radians) + mCY) - it.staticLayout.height / 2
                        it.textX = textX.toFloat()
                        it.textY = textY.toFloat()
                    }
                }
            } else {
                if (pieDataSize == 1) {
                    val pieData = pieDataList[0] as PieEmptyData
                    mStartAngleArray[1] = MAX_ANGLE
                    mColorArray[0] = Color.parseColor(pieData.color)
                }
                //use mCX default,it is initialized
                //use mCY default,it is initialized
            }
            initCircleRectF()
        }
    
        private fun getTruncatedString(
            fullText: String,
            maxWidth: Double,
            amountSymbols: String? = ""
        ): String {
            var truncatedString = ""
            for (string in fullText.toCharArray()) {
                val tempStr = "$truncatedString$string"
                val width = mTextPaint.measureText("$amountSymbols$tempStr...")
                if (width == maxWidth.toFloat()) {
                    truncatedString = "$amountSymbols$tempStr..."
                    break
                }
                if (width > maxWidth) {
                    truncatedString = "$amountSymbols$truncatedString..."
                    break
                }
                truncatedString = tempStr
            }
            val finalText = if (truncatedString.startsWith("$amountSymbols")) {
                truncatedString
            } else {
                "$amountSymbols$truncatedString"
            }
            return finalText.replace("\n", "")
        }
    
        private fun initCircleRectF() {
            val innerRadius = mOuterRadius - mRingWidth
            mCircleRectF.left = mCX - innerRadius - mRingWidth / 2
            mCircleRectF.top = mCY - innerRadius - mRingWidth / 2
            mCircleRectF.right = mCX + innerRadius + mRingWidth / 2
            mCircleRectF.bottom = mCY + innerRadius + mRingWidth / 2
        }
    
        private fun startDrawWithAnimation(dataSize: Int) {
            mValueAnimator.setFloatValues(0f, MAX_ANGLE)
            mValueAnimator.addUpdateListener {
                mCurrentAngle = it.animatedValue as Float
                if (mCurrentAngle <= 0) {
                    return@addUpdateListener
                }
                if (dataSize > 1) {
                    //algorithm: check and change the color of piePaint.
                    for (i in mCursor + 1 until mStartAngleArray.size) {
                        if (mCurrentAngle >= mStartAngleArray[i] && mCurrentAngle < MAX_ANGLE) {
                            mCursor = i
                        }
                    }
                }
                GDLog.d("pieChart_change", "$mCursor | $mCurrentAngle")
                invalidate()
            }
            mValueAnimator.duration = 600L
            mValueAnimator.startDelay = 50L
            mValueAnimator.start()
        }
    
    
        override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec)
            val widthMode = MeasureSpec.getMode(widthMeasureSpec)
            val heightMode = MeasureSpec.getMode(heightMeasureSpec)
            val widthSize = MeasureSpec.getSize(widthMeasureSpec)
            val heightSize = MeasureSpec.getSize(heightMeasureSpec)
    
            val realWidth = if (widthMode == MeasureSpec.EXACTLY) {
                (widthSize + mMargin * 2).toInt()
            } else {
                (mCX * 2).toInt()
            }
            val realHeight = if (heightMode == MeasureSpec.EXACTLY) {
                (heightSize + mMargin * 2).toInt()
            } else {
                (mCY * 2).toInt()
            }
            setMeasuredDimension(realWidth, realHeight)
        }
    
    
        override fun onDraw(canvas: Canvas) {
            if (!mHasInit) {
                super.onDraw(canvas)
                return
            }
            for (i in 0..mCursor) {
                mPiePaint.color = mColorArray[i]
                val startAngle = mStartAngleArray[i]
                val sweep = mCurrentAngle - mStartAngleArray[i]
                canvas.drawArc(
                    mCircleRectF,
                    -startAngle,
                    -sweep,
                    false,
                    mPiePaint
                )
                mTextRectArray[i]?.let { textParams ->
                    mLinePaint.color = mColorArray[i]
                    canvas.drawLine(
                        textParams.lineRectF.left,
                        textParams.lineRectF.top,
                        textParams.lineRectF.right,
                        textParams.lineRectF.bottom,
                        mLinePaint
                    )
                    //<test code>
    //                canvas.drawRect(
    //                    textParams.textX,
    //                    textParams.textY,
    //                    textParams.textX + textParams.staticLayout.width,
    //                    textParams.textY + textParams.staticLayout.height,
    //                    mLinePaint
    //                )
                    //<test code/>
                    mTextPaint.color = mColorArray[i]
                    canvas.translate(textParams.textX, textParams.textY)
                    textParams.staticLayout.draw(canvas)
                    canvas.translate(-textParams.textX, -textParams.textY)
                }
            }
        }
    
        fun getOuterRadius(): Float {
            return mOuterRadius
        }
    
        fun getRingWidth(): Float {
            return mRingWidth
        }
    
        override fun onDetachedFromWindow() {
            super.onDetachedFromWindow()
            mHasInit = false
        }
    
        private fun dp2px(radius: Float): Float {
            return (context.resources.displayMetrics.density * radius)
        }
    
        data class TextCalculateParams(
            val staticLayout: StaticLayout,
            val radians: Double,
            var lineRectF: RectF,
            var textX: Float,
            var textY: Float
        )
    
    
        interface IPieData {
            val color: String
        }
    
        class PieEmptyData(
            override val color: String
        ) : IPieData
    
        class PieData(
            val categoryName: String,
            val valueItem: Double,
            override val color: String
        ) : IPieData
    }
    
    我也是有底线的,感谢您的耐心。

    相关文章

      网友评论

          本文标题:Android(Kotlin) PieChartView(环形饼

          本文链接:https://www.haomeiwen.com/subject/msqlsltx.html