一个饼状图

一个比较简单的效果如下


饼状图

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


点击效果

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


}

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

推荐阅读更多精彩内容

  • html5新增的canvas标签允许脚本语言动态渲染位图像,在基于Web的图像显示方面比Flash更加立体,也更加...
    _三月阅读 4,259评论 3 24
  • 自定义View 简单实践-02 饼图-01 1.成品效果 2.思路分析 这个效果比较简单,我们只需要在同一个矩形范...
    花椒人生阅读 243评论 0 0
  • 好久没有写过自定义的文章,这次重拾这方面的内容,从雷达图开始。首先看一下效果图: 这就是大概的效果图了,下面我们就...
    Jack921阅读 790评论 1 7
  • ——》个人平时笔记,看到的同学欢迎指正错误,文中多处摘录于各大博主精华、书籍 1、在自定义View中,drawAr...
    木溪bo阅读 367评论 0 0
  • 前言 Canvas本意是画布的意思,然而将它理解为绘制工具一点也不为过。通过Canvas提供的API,你可以在画布...
    炼_1086阅读 5,491评论 0 0