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

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


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<PieData> = 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<PieData>?): 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<PieData>) {
        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使用:

<com.sunqi.circlechart.CircleChartView
        android:id="@+id/cc_view"
        android:layout_marginLeft="30dp"
        android:layout_centerHorizontal="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
class MainActivity : AppCompatActivity() {

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

        val list = ArrayList<PieData>()
        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()
            }

        })
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容