Android Kotlin自定义View绘制圆形图表,选中放大

绘制一个图表,可以选中状态如下


Android Kotlin自定义View绘制圆形图表,选中放大_第1张图片
1550647418892.gif

简单说一下流程:
1.基本原理是遍历绘制扇形每次绘制记录起始点,然后再绘制一个白色圆覆盖便形成一个图标圆环
2.绘制一个大圆为选中圆,设置非选中部分为透明
3.通过点击xy坐标获取点击圆的位置,重绘
4.反向绘制圆加载动画
具体看代码里面详细注释描述:

class CircleChartView : View {
    //饼状图画笔
    private lateinit var mPiePaint: Paint
    //阴影画笔
    private var mPaintShadow: Paint? = null
    //默认第一张图半径
    private val mRadiusOne = DensityUtils.dp2px(context, 80f).toFloat()
    private val mRadiusOneCover = DensityUtils.dp2px(context, 55f).toFloat()
    private val mRadiusTwo = DensityUtils.dp2px(context, 85f).toFloat()
    private val mRadiusTwoCover = DensityUtils.dp2px(context, 50f).toFloat()
    private val mRadiusInside = DensityUtils.dp2px(context, 47f).toFloat()
    private val mRadiusInsideCover = DensityUtils.dp2px(context, 45f).toFloat()

    //圆和view边框距离
    private val TOFRAME = DensityUtils.dp2px(context, 40f).toFloat()

    //构成饼状图的数据集合
    private var mPieDataList: List = ArrayList()
    //绘制弧形的sweep数组
    private var mPieSweep: FloatArray? = null
    //初始画弧所在的角度
    private val START_DEGREE = -90
    private var animPro: Float = 0.toFloat()
    private var isStartAnim = true

    //默认圆
    private val mRectFOne = RectF()
    private val mRectFSelect = RectF()
    private val mRectFInside = RectF()
    private val mRectFCover = RectF()

    //扇形外部text起始点坐标
    private var outTextX: Float = 0.toFloat()
    private var outTextY: Float = 0.toFloat()

    private var centerMoney = ""
    private var totalText = ""

    private var mListener: OnSpecialTypeClickListener? = null

    interface OnSpecialTypeClickListener {
        fun onSpecialTypeClick(index: Int, type: String)
    }

    fun setOnSpecialTypeClickListener(listener: OnSpecialTypeClickListener?) {
        this.mListener = listener
    }

    constructor(context: Context?) : this(context, null)
    constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
        init()
    }

    //初始化画笔
    private fun init() {
        mPiePaint = Paint()
        mPiePaint.isAntiAlias = true
        mPiePaint.style = Paint.Style.FILL

        mPaintShadow = Paint()
        mPaintShadow?.color = Color.WHITE
        mPaintShadow?.style = Paint.Style.FILL
        mPaintShadow?.maskFilter = BlurMaskFilter(2f, BlurMaskFilter.Blur.NORMAL)

        initRectF()
        initSelectRectF()
        initInsideSelectRectF()
        initRectFCover()
    }

    /**
     * 初始化绘制弧形所在矩形的四点坐标
     */
    private fun initRectFCover() {
        mRectFCover.left = TOFRAME - 1
        mRectFCover.top = TOFRAME - 1
        mRectFCover.right = 2 * mRadiusOne + TOFRAME + 1f
        mRectFCover.bottom = 2 * mRadiusOne + TOFRAME + 1f
    }

    /**
     * 初始化绘制弧形所在矩形的四点坐标
     */
    private fun initRectF() {
        mRectFOne.left = TOFRAME
        mRectFOne.top = TOFRAME
        mRectFOne.right = 2 * mRadiusOne + TOFRAME
        mRectFOne.bottom = 2 * mRadiusOne + TOFRAME
    }


    /**
     * 选中圆
     */
    private fun initSelectRectF() {
        mRectFSelect.left = TOFRAME - DensityUtils.dp2px(context, 5f)
        mRectFSelect.top = TOFRAME - DensityUtils.dp2px(context, 5f)
        mRectFSelect.right = 2 * mRadiusTwo + TOFRAME - DensityUtils.dp2px(context, 5f)
        mRectFSelect.bottom = 2 * mRadiusTwo + TOFRAME - DensityUtils.dp2px(context, 5f)
    }

    /**
     * 内部圆
     */
    private fun initInsideSelectRectF() {
        mRectFInside.left = TOFRAME + DensityUtils.dp2px(context, 33f)
        mRectFInside.top = TOFRAME + DensityUtils.dp2px(context, 33f)
        mRectFInside.right = 2 * mRadiusInside + TOFRAME + DensityUtils.dp2px(context, 33f).toFloat()
        mRectFInside.bottom = 2 * mRadiusInside + TOFRAME + DensityUtils.dp2px(context, 33f).toFloat()
    }

    fun setTotalText(totalText: String) {
        this.totalText = totalText
    }

    fun setTextMoney(centerMoney: String) {
        this.centerMoney = centerMoney
    }

    fun startAnimation() {
        val valueAnimator = ValueAnimator.ofFloat(0f, 1f)
        //从0到1 意思是从没有到原本设置的那个值
        valueAnimator.interpolator = AccelerateDecelerateInterpolator()
        valueAnimator.addUpdateListener { animation ->
            animPro = animation.animatedValue as Float
            //原来值的完成度
            invalidate()
            //重绘
        }
        valueAnimator.duration = 2000
        valueAnimator.start()
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        setLayerType(View.LAYER_TYPE_SOFTWARE, null)
        if (!mPieDataList.isEmpty()) {
            //起始是从-90°位置开始画
            var pieStartOne = START_DEGREE.toFloat()
            var pieStartTwo = START_DEGREE.toFloat()
            var pieStartInside = START_DEGREE.toFloat()
            if (mPieSweep == null) {
                mPieSweep = FloatArray(mPieDataList.size)
            }

            //底部圆
            for (i in mPieDataList.indices) {
                //设置弧形颜色
                mPiePaint.color = Color.parseColor(mPieDataList[i].color)
                //绘制弧形区域,以构成饼状图
                val pieSweep = getProportion(i) * 360
                canvas.drawArc(mRectFOne, pieStartOne, mPieSweep!![i], true, mPiePaint)
                //获取下一个弧形的起点
                pieStartOne += pieSweep
            }
            if (isStartAnim) {
                isStartAnim = false
                startAnimation()
            }

            mPiePaint.color = Color.parseColor("#ffffff")//白色
            canvas.drawArc(mRectFCover, -90f, -360 * (1 - animPro), true, mPiePaint)

            mPiePaint.color = Color.parseColor("#ffffff")//白色
            canvas.drawCircle(mRadiusOne + TOFRAME, mRadiusOne + TOFRAME, mRadiusOneCover, mPiePaint)

            //选中圆
            for (i in mPieDataList.indices) {
                val pieSweep = getProportion(i) * 360
                //设置弧形颜色
                if (mPieDataList[i].isSelected) {
                    //获取外部位置
                    drawText(pieStartTwo, pieSweep)
                    mPiePaint.color = Color.parseColor(mPieDataList[i].color)
                } else {
                    mPiePaint.color = Color.parseColor(mPieDataList[i].on_select_color)
                }
                //绘制弧形区域,以构成饼状图
                canvas.drawArc(mRectFSelect, pieStartTwo, mPieSweep!![i], true, mPiePaint)
                //获取下一个弧形的起点
                pieStartTwo += pieSweep
            }

            mPiePaint.color = Color.parseColor("#ffffff")//白色
            canvas.drawCircle(mRadiusOne + TOFRAME, mRadiusOne + TOFRAME, mRadiusTwoCover, mPiePaint)

            //内部圆
            for (i in mPieDataList.indices) {
                val pieSweep = getProportion(i) * 360
                //设置弧形颜色
                mPiePaint.color = Color.parseColor(mPieDataList[i].color)
                //绘制弧形区域,以构成饼状图
                canvas.drawArc(mRectFInside, pieStartInside, mPieSweep!![i], true, mPiePaint)
                //获取下一个弧形的起点
                pieStartInside += pieSweep
            }

            mPiePaint.color = Color.parseColor("#ffffff")//白色
            canvas.drawCircle(mRadiusOne + TOFRAME, mRadiusOne + TOFRAME, mRadiusInsideCover, mPiePaint)

            //画一个矩形
            mPiePaint.color = Color.TRANSPARENT
            mPiePaint.style = Paint.Style.FILL
            canvas.drawRect(mRectFInside, mPiePaint)


            //绘制文字
            mPiePaint.isAntiAlias = true
            mPiePaint.color = Color.BLUE
            mPiePaint.style = Paint.Style.FILL
            //该方法即为设置基线上那个点究竟是left,center,还是right  这里我设置为center
            mPiePaint.textAlign = Paint.Align.CENTER
            val fontMetrics = mPiePaint.fontMetrics
            val top = fontMetrics?.top//为基线到字体上边框的距离,即上图中的top
            val bottom = fontMetrics?.bottom//为基线到字体下边框的距离,即上图中的bottom
            val baseLineY = (mRectFInside.centerY() - top!! / 2 - bottom!! / 2).toInt()//基线中间点的y轴计算公式
            mPiePaint.textSize = 22f
            mPiePaint.color = Color.parseColor("#999999")
            canvas.drawText(totalText, mRectFInside.centerX(), (baseLineY - 20).toFloat(), mPiePaint)

            mPiePaint.textSize = 24f
            mPiePaint.color = Color.parseColor("#333333")
            mPiePaint.typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD)
            canvas.drawText(centerMoney, mRectFInside.centerX(), (baseLineY + 10).toFloat(), mPiePaint)

            //绘制外围选中
            for (i in mPieDataList.indices) {
                if (mPieDataList[i].isSelected) {
                    //绘制圆形
                    canvas.drawCircle(outTextX, outTextY, DensityUtils.dp2px(context, 22.5f).toFloat(), mPaintShadow) //阴影
                    mPiePaint.color = Color.parseColor(mPieDataList[i].color)
                    canvas.drawCircle(outTextX, outTextY, DensityUtils.dp2px(context, 21.5f).toFloat(), mPiePaint)
                    mPiePaint.color = Color.parseColor("#ffffff")
                    canvas.drawCircle(outTextX, outTextY, DensityUtils.dp2px(context, 17.5f).toFloat(), mPiePaint)
                    //绘制提示内容
                    mPiePaint.textSize = 18f
                    mPiePaint.color = Color.BLACK
                    mPiePaint.typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD)
                    canvas.drawText(mPieDataList[i].type, outTextX, outTextY + 5, mPiePaint)
                }
            }
        } else {
            //无数据时,显示灰色圆环
            mPiePaint.color = Color.parseColor("#dadada")//灰色
            canvas.drawCircle(mRadiusOne, mRadiusOne, mRadiusOne, mPiePaint)
        }
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val length = (2 * (mRadiusOne + TOFRAME)).toInt()
        setMeasuredDimension(length, length)
    }

    /**
     * 获取圆弧外位置
     */
    private fun drawText(pieStart: Float, pieSweep: Float) {
        //        //弧形区域角平分线距离-90角度
        val a = pieStart + pieSweep / 2 + 90f
        if (a < 90) {
            //右上部
            outTextX = mRadiusTwo + TOFRAME + (mRadiusTwo * Math.sin(java.lang.Double.parseDouble(((a / 360).toDouble() * 2.0 * Math.PI).toString()))).toFloat()
            outTextY = mRadiusTwo + TOFRAME - (mRadiusTwo * Math.cos(java.lang.Double.parseDouble(((a / 360).toDouble() * 2.0 * Math.PI).toString()))).toFloat() - 40f
        } else if (a == 90f) {
            outTextX = mRadiusTwo + TOFRAME + (mRadiusTwo * Math.sin(java.lang.Double.parseDouble(((a / 360).toDouble() * 2.0 * Math.PI).toString()))).toFloat() + 10f
            outTextY = mRadiusTwo + TOFRAME - (mRadiusTwo * Math.cos(java.lang.Double.parseDouble(((a / 360).toDouble() * 2.0 * Math.PI).toString()))).toFloat()
        } else if (a < 180) {
            //右下
            outTextX = mRadiusTwo + TOFRAME + (mRadiusTwo * Math.sin(java.lang.Double.parseDouble(((a / 360).toDouble() * 2.0 * Math.PI).toString()))).toFloat() + 10f
            outTextY = mRadiusTwo + TOFRAME - (mRadiusTwo * Math.cos(java.lang.Double.parseDouble(((a / 360).toDouble() * 2.0 * Math.PI).toString()))).toFloat() + 10
        } else if (a == 180f) {
            outTextX = mRadiusTwo + TOFRAME + (mRadiusTwo * Math.sin(java.lang.Double.parseDouble(((a / 360).toDouble() * 2.0 * Math.PI).toString()))).toFloat() - 20
            outTextY = mRadiusTwo + TOFRAME - (mRadiusTwo * Math.cos(java.lang.Double.parseDouble(((a / 360).toDouble() * 2.0 * Math.PI).toString()))).toFloat() + 10
        } else if (a < 270) {
            //左下
            outTextX = mRadiusTwo + TOFRAME + (mRadiusTwo * Math.sin(java.lang.Double.parseDouble(((a / 360).toDouble() * 2.0 * Math.PI).toString()))).toFloat() - 30
            outTextY = mRadiusTwo + TOFRAME - (mRadiusTwo * Math.cos(java.lang.Double.parseDouble(((a / 360).toDouble() * 2.0 * Math.PI).toString()))).toFloat() + 20
        } else if (a == 270f) {
            outTextX = mRadiusTwo + TOFRAME + (mRadiusTwo * Math.sin(java.lang.Double.parseDouble(((a / 360).toDouble() * 2.0 * Math.PI).toString()))).toFloat() - 60
            outTextY = mRadiusTwo + TOFRAME - (mRadiusTwo * Math.cos(java.lang.Double.parseDouble(((a / 360).toDouble() * 2.0 * Math.PI).toString()))).toFloat()
        } else if (a < 360) {
            //左上
            outTextX = mRadiusTwo + TOFRAME + (mRadiusTwo * Math.sin(java.lang.Double.parseDouble(((a / 360).toDouble() * 2.0 * Math.PI).toString()))).toFloat() - 40
            outTextY = mRadiusTwo + TOFRAME - (mRadiusTwo * Math.cos(java.lang.Double.parseDouble(((a / 360).toDouble() * 2.0 * Math.PI).toString()))).toFloat() + 10
        }
    }


    /**
     * 所在区域占总区域比例
     */
    fun getProportion(i: Int): Float {
        return mPieDataList[i].value / getSumData(mPieDataList)
    }

    /**
     * 获取各区域数值总和
     */
    fun getSumData(mPieDataList: List?): Float {
        if (mPieDataList == null || mPieDataList.isEmpty()) {
            return 0f
        }
        var mSum = 0f
        for (i in mPieDataList.indices) {
            mSum += mPieDataList[i].value
        }
        return mSum
    }


    /**
     * 设置需要绘制的数据集合
     */
    fun setPieDataList(pieDataList: List) {
        this.mPieDataList = pieDataList
        if (mPieSweep == null) {
            mPieSweep = FloatArray(mPieDataList!!.size)
        }
        for (i in pieDataList.indices) {
            mPieSweep!![i] = getProportion(i) * 360
        }
    }


    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> doOnSpecialTypeClick(event)
        }
        return super.onTouchEvent(event)
    }

    private fun doOnSpecialTypeClick(event: MotionEvent) {
        if (mPieDataList.isEmpty()) {
            return
        }
        val which = touchOnWhichPart(event)
        if (mListener != null && which != 1000) {
            for (i in mPieDataList.indices) {
                mPieDataList[i].isSelected = i == which
            }
            mListener!!.onSpecialTypeClick(which, mPieDataList[which].type)
        }
    }

    /**
     * 点击位置
     */
    private fun touchOnWhichPart(event: MotionEvent): Int {
        val x = event.x
        val y = event.y
        var mWhich = 1000//错误码
        var a = 0.0//与-90的夹角
        var sum = 0f//所占比例和
        val newRadius = mRadiusOne + TOFRAME//用来参与计算的半径+间距
        //在圆内
        if (Math.pow((x - newRadius).toDouble(), 2.0) + Math.pow((y - newRadius).toDouble(), 2.0) <= Math.pow(mRadiusOne.toDouble(), 2.0)) {
            if (event.x > newRadius) {
                //圆的右半部
                if (event.y > newRadius) {
                    //圆的下半部(综上:右下  0-90)
                    a = Math.PI / 2 + Math.atan(java.lang.Double.parseDouble(((y - newRadius) / (x - newRadius)).toString()))
                } else {
                    //右上(-90-0)
                    a = Math.atan(java.lang.Double.parseDouble(((x - newRadius) / (newRadius - y)).toString()))
                }
            } else {
                //圆的左半部
                if (event.y > newRadius) {
                    //圆的下半部(综上:左下 90-180)
                    a = Math.PI + Math.atan(java.lang.Double.parseDouble(((newRadius - x) / (y - newRadius)).toString()))
                } else {
                    //左上  180-270
                    a = 2 * Math.PI - Math.atan(java.lang.Double.parseDouble(((newRadius - x) / (newRadius - y)).toString()))
                }
            }

            for (i in mPieDataList.indices) {
                if (i < mPieDataList.size - 1) {
                    sum += getProportion(i)
                } else {
                    sum = 1f
                }

                if (a / (2 * Math.PI) <= sum) {
                    mWhich = i
                    break
                }
            }
        }
        return mWhich
    }
data class PieData(val type:String,val value:Float,val color:String,val on_select_color:String,val isSelected:Boolean)

测试Demo使用:


class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val list = ArrayList()
        list.add(PieData("20%", 20f, "#2828ff", "#00000000", false))
        list.add(PieData("10%", 10f, "#5b5b5b", "#00000000", false))
        list.add(PieData("55%", 55f, "#d200d2", "#00000000", false))
        list.add(PieData("35%", 35f, "#02df08", "#00000000", false))
        list.add(PieData("60%", 60f, "#9999cc", "#00000000", false))
        list.add(PieData("40%", 40f, "#ff5809", "#00000000", false))
        cc_view.setTotalText("总数")
        cc_view.setTextMoney("100000¥")
        cc_view.setPieDataList(list)
        cc_view.setOnSpecialTypeClickListener(object : CircleChartView.OnSpecialTypeClickListener {
            override fun onSpecialTypeClick(index: Int, type: String) {
                cc_view.invalidate()
            }

        })
    }
}

你可能感兴趣的:(Android Kotlin自定义View绘制圆形图表,选中放大)