美文网首页
一个饼状图

一个饼状图

作者: 100个大西瓜 | 来源:发表于2023-09-06 16:07 被阅读0次

    一个比较简单的效果如下


    饼状图

    附带一点简单的点击效果:点击后所在的扇形弹出一点,与原来的分隔开


    点击效果

    绘制扇形使用的api是Cavans

      public void drawArc(@NonNull RectF oval, float startAngle, float sweepAngle,  boolean useCenter, @NonNull Paint paint) {
            super.drawArc(oval, startAngle, sweepAngle, useCenter, paint);
        }
    
    

    传入的参数是扇形所在的圆形所在的矩形的四个参数,以及开始角度,扇形的角度,是否与圆心连接成封闭的图案,以及画笔工具

    这里只需要设置每个扇形的颜色、起始角度、扇形角度就可以了

    点击事件的处理:
    设置一个OnTouchListener,来监听屏幕的触摸事件,主要是通过按下事件的坐标来判断:
    1.根据坐标与圆心的距离来判断点是否处于扇形内,如果不是,则取消已经偏移的扇形(如果有),刷新界面,如果没有进入步骤2;
    2.根据坐标与圆心坐标的连线与x轴正方向所形成的夹角,通过三角函数相关的公式来计算出旋转角度的大小,依次与每个扇形的开始角度值、结束角度值进行比较,看符合该扇形的区间的,如果符合该区间,判断当前已经偏移的扇形模块A是否与该扇形B相同,如果相同则取消偏移,否则将A扇形取消偏移,使B扇形产生偏移,刷新界面;如果一个循环结束,仍没有相匹配的扇形,说明扇形没有填充完成,取消已经偏移的扇形,刷新界面;

    扇形的偏移处理:
    偏移的方向是扇形的起始角度与结束角度的中间值,也就是扇形的起始角度加上扇形的一半大小,在绘制该扇形前,先对 canvas.translate(),进行移动;
    然后确定偏移后的圆点坐标,当然了要配合 canvas.save() 和 canvas.restore() 来使用;

    感觉偏移相对值 (x,y)比较计算比较繁琐,因此在绘制前对cavans进行了旋转,目的是使偏移后的方向刚好是x轴的正方向,因此在操作canvas.translate()时更加简单;

    onDraw()的函数如下:

     override fun onDraw(canvas: Canvas) {
            super.onDraw(canvas)
            val cx = width / 2.0f
            val cy = height / 2.0f
            var startAngle = 0.0f
    
            for (i in mPieChartList.indices) {
                val pieChart = mPieChartList[i]
                mPaint.color = pieChart.color
                canvas.save()
                val half = (pieChart.angle / 2.0f)
                canvas.rotate(startAngle + half, cx, cy)
    
                if (i == mIndexClick) {
                    //点击中时:垂直方向固定,水平方向移动50个单位
                    canvas.translate(50f, 0f)
                }
                canvas.drawArc(mRectF, -half, pieChart.angle, true, mPaint)
                canvas.restore()
                startAngle += pieChart.angle
            }
        }
    

    全部代码如下:

    package com.shenby.widget.pie
    
    import android.content.Context
    import android.graphics.Canvas
    import android.graphics.Color
    import android.graphics.Paint
    import android.graphics.RectF
    import android.util.AttributeSet
    import android.view.MotionEvent
    import android.view.View
    import androidx.annotation.ColorInt
    import kotlin.math.abs
    import kotlin.math.asin
    import kotlin.math.pow
    import kotlin.math.sqrt
    
    /**
     * 饼状图,从x轴方向 顺时针开始绘制
     * todo 还需要增加一个可以调整起始角度的入口
     */
    class PieChartView(context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) :
        View(context, attrs, defStyleAttr, defStyleRes) {
    
        constructor(context: Context) : this(context, null)
        constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)
        constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : this(
            context,
            attrs,
            defStyleAttr,
            0
        )
    
        private val mAngles = arrayOf(
            60f, 60f, 60f, 90f, 90f,
        )
        private val mColors = arrayOf(Color.GREEN, Color.BLUE, Color.YELLOW, Color.RED, Color.CYAN)
        private val mPaint = Paint()
        private var mRadius = 0
        private var mRadiusPow2 = 0f
        private val mRectF = RectF()
    
        private var mIndexClick = 2
    
        var mStartAngle = 60f
    
        var mPieChartList: MutableList<PieChart> = mutableListOf()
            set(list) {
                field.clear()
                val sum = list.sumOf { it.value }
                //计算总和,然后得到角度值
                for (pieChart in list) {
                    pieChart.angle = (pieChart.value / sum * 360).toFloat()
                }
                field = list
                postInvalidate()
            }
    
    
        init {
            mPaint.style = Paint.Style.FILL
            //add for test,增加预览模式的填充内容
            if (isInEditMode) {
                val listOf = mutableListOf<PieChart>()
                for (i in mAngles.indices) {
                    listOf.add(PieChart(mColors[i], mAngles[i].toDouble()))
                }
                mPieChartList = listOf
            }
    
        }
    
    
        override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
            super.onSizeChanged(w, h, oldw, oldh)
            val padding = 60
            //Math.min
            mRadius = w.coerceAtMost(h) / 2 - padding
            mRadiusPow2 = mRadius.toFloat().pow(2)
            mRectF.apply {
                top = (h / 2 - mRadius).toFloat()
                bottom = (h / 2 + mRadius).toFloat()
                left = (w / 2 - mRadius).toFloat()
                right = (w / 2 + mRadius).toFloat()
            }
    
        }
    
    
        override fun onDraw(canvas: Canvas) {
            super.onDraw(canvas)
            val cx = width / 2.0f
            val cy = height / 2.0f
            var startAngle = 0.0f
    
            for (i in mPieChartList.indices) {
                val pieChart = mPieChartList[i]
                mPaint.color = pieChart.color
                canvas.save()
                val half = (pieChart.angle / 2.0f)
                canvas.rotate(startAngle + half, cx, cy)
    
                if (i == mIndexClick) {
                    //点击中时:垂直方向固定,水平方向移动50个单位
                    canvas.translate(50f, 0f)
                }
                canvas.drawArc(mRectF, -half, pieChart.angle, true, mPaint)
                canvas.restore()
                startAngle += pieChart.angle
            }
        }
    
    
        override fun onTouchEvent(event: MotionEvent): Boolean {
            return when (event.actionMasked) {
                MotionEvent.ACTION_DOWN -> {
                    val handleClickListener = handleClickListener(event)
                    if (handleClickListener) {
                        performClick()
                    }
                    handleClickListener
                }
                MotionEvent.ACTION_UP -> {
                    false
                }
                else -> false
            }
        }
    
    
        override fun performClick(): Boolean {
            return super.performClick()
        }
    
    
        /**
         * 点击事件
         * 1.判断是不是在圆内,如果在圆外,将indexClick 改为-1,刷新界面
         * 2.如果在圆内,根据角度 判断属于哪一个扇形,如果与原来的相同,将indexClick 改为-1, 否则更新indexClick,再刷新界面
         */
        private fun handleClickListener(event: MotionEvent): Boolean {
            val cx = width / 2.0f
            val cy = height / 2.0f
            val x = event.x
            val y = event.y
    
            val deltaX = x - cx
            val deltaY = y - cy
            //Math.pow,Math.sqrt
            val lengthPow = (deltaX.pow(2) + deltaY.pow(2)).toDouble()
            if (lengthPow > mRadiusPow2) {
                updateIndex()
                return true
            }
            val length = sqrt(lengthPow)
    
            val angle = loadAngle(deltaX, deltaY, length)
            //Log.d(TAG, "handleClickListener: ($cx $cy) ($x $y ) ($deltaX $deltaY)   angle =$angle")
    
            var startAngle = 0.0f
            for (i in mPieChartList.indices) {
                val pieChart = mPieChartList[i]
                val endAngle = startAngle + pieChart.angle
                if (angle in startAngle..endAngle) {
                    val index = if (mIndexClick == i) {
                        -1
                    } else {
                        i
                    }
                    updateIndex(index)
                    return true
                }
                startAngle = endAngle
            }
    
            //没有匹配上的
            updateIndex()
    
    
            return true
        }
    
    
        private fun updateIndex(index: Int = -1) {
            if (mIndexClick == index) {
                return
            }
            mIndexClick = index
            invalidate()
        }
    
        /**
         * 计算角度,应该有更好的方式待确定
         */
        private fun loadAngle(offsetX: Float, offsetY: Float, length: Double): Double {
            //正弦
            val sina = abs(offsetY) / length
            //Math.asin
            val asin = asin(sina)
            //角度大小
            val angle = asin * (180 / Math.PI)
            return if (offsetX >= 0) {
                if (offsetY >= 0) {
                    angle
                } else {
                    360 - angle
                }
            } else {
                if (offsetY >= 0) {
                    180 - angle
                } else {
                    180 + angle
                }
            }
        }
    
        companion object {
            const val TAG = "PieChartView"
        }
    
        /**
         * @param color 颜色
         * @param value 数值
         */
        data class PieChart(@ColorInt val color: Int, val value: Double) {
            /**
             * 角度,由总值计算出来的
             */
            var angle: Float = 0.0f
        }
    
    
    }
    
    

    相关文章

      网友评论

          本文标题:一个饼状图

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