一个饼状图

2023-09-06  本文已影响0人  100个大西瓜

一个比较简单的效果如下


饼状图

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


点击效果

绘制扇形使用的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
    }


}

上一篇下一篇

猜你喜欢

热点阅读