Android扇形图自定义

扇形图自定义

示例.png
示例 (2).png
示例 (3).png
示例 (4).png
示例 (5).png

实现

总体思路:根据每个数据所占的比例*360得到其在圆中所属的弧度,然后调用drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter,@NonNull Paint paint)画出对应区域,即可成为扇形图。字体需要调用drawText(@NonNull String text, float x, float y, @NonNull Paint paint),计算出坐标,然后画出。
1.在设置数据时拿到所有数据的总和;

  if (data.isNotEmpty()) {
            for (i in 0 until data.size) {
                mTotalNum = data[i].num!! + mTotalNum!!
            }
        }

2.重写onSizeChanged函数,得到圆形的圆心位置以及半径,算出字体所在半径,算出画圆时需要的矩形坐标;

//圆心位置
                centerPosition!!.x = w / 2
                centerPosition!!.y = h / 2
                //半径
                minWidth =
                        Math.min(w - paddingLeft - paddingRight, h - paddingBottom - paddingTop)
raduis=(minWidth!! / 2).toFloat()
 dataRaduis = raduis!! * 3 / 4
        //矩形坐标
        mRectF!!.left = centerPosition!!.x - raduis!!
        mRectF!!.top = centerPosition!!.y - raduis!!
        mRectF!!.right = centerPosition!!.x + raduis!!
        mRectF!!.bottom = centerPosition!!.y + raduis!!

3.重写onDraw函数,计算每一个块数据对应的开始弧度和滑过的弧度,并画出对应的区域,计算字体所在的x和y坐标,画出;

mStartAngle = 0f
        mSweepAngle = 0f
        for (i in 0 until mData!!.size) {
            mPieChartPaint!!.color = mData!![i].color!!

            mSweepAngle = (mData!![i].num!! / mTotalNum!!) * 360 * mPercent!!

            //画圆
            canvas.drawArc(mRectF!!, mStartAngle!!, mSweepAngle!!, true, mPieChartPaint!!)
            mStartAngle = mStartAngle!! + mSweepAngle!!
            if (mLayoutType == "pointingInstructions") {
                //指向说明
                pointData(canvas, i)
            } else {
                //画数据
                drawData(canvas, i)
            }
        }
 private fun drawData(canvas: Canvas, i: Int) {
        val x = centerPosition!!.x + dataRaduis!! *
                Math.sin(Math.toRadians((90 + mStartAngle!! - mSweepAngle!! / 2).toDouble())).toFloat()
        val y = centerPosition!!.y - dataRaduis!! *
                Math.cos(Math.toRadians((90 + mStartAngle!! - mSweepAngle!! / 2).toDouble())).toFloat()
 canvas.drawText(mData!![i].name!!, x, y, mDataPaint!!)
                canvas.drawText(
                    mData!![i].num!!.toString() + mData!![i].unit,
                    x,
                    y - mDataPaint!!.ascent() + 5,
                    mUnitPaint!!
    }

扩展

以上,一个基本的扇形图算是完成了,但是在实际的运用当中,一个有扩展的扇形图很有必要,所以在以上的基础上我们扩展一下。
1.加入动画,毕竟一个有动画的扇形图比没动画图的扇形图更加让人赏心悦目,所以加入动画很有必要了。每次得到绘画的进度mPercent,在绘画扇形时每次重新计算对应的弧度的进度mSweepAngle 和起始弧度mStartAngle ;

 private fun startAnim(animTime: Int) {
        mAnimator = ValueAnimator.ofFloat(0f, 1f)
        mAnimator!!.duration = animTime.toLong()
        mAnimator!!.addUpdateListener {
            mPercent = it.animatedValue as Float?
            postInvalidate()
        }
        mAnimator!!.start()
    }
 mSweepAngle = (mData!![i].num!! / mTotalNum!!) * 360 * mPercent!!
mStartAngle = mStartAngle!! + mSweepAngle!!

2.当数据比较小,导致无法在对应的扇形图中填入对应的信息,补充以下解决办法:
a.选择指向说明,引申出在外部说明,如示例.png。调用drawLine画出两条线,在最后一条线的末尾坐标画出对应的说明。

 private fun pointData(canvas: Canvas, i: Int) {
        val xP = centerPosition!!.x + raduis!! *
                Math.sin(Math.toRadians((90 + mStartAngle!! - mSweepAngle!! / 2).toDouble())).toFloat()
        val yP = centerPosition!!.y - raduis!! *
                Math.cos(Math.toRadians((90 + mStartAngle!! - mSweepAngle!! / 2).toDouble())).toFloat()
        val xEdP = centerPosition!!.x + (raduis!! + 20) *
                Math.sin(Math.toRadians((90 + mStartAngle!! - mSweepAngle!! / 2).toDouble())).toFloat()
        val yEdP = centerPosition!!.y - (raduis!! + 20) *
                Math.cos(Math.toRadians((90 + mStartAngle!! - mSweepAngle!! / 2).toDouble())).toFloat()
        var xLast = 0f
        xLast = if (mStartAngle!! - mSweepAngle!! / 2 >= 270 || mStartAngle!! - mSweepAngle!! / 2 <= 90) {
            xEdP + 30
        } else {
            xEdP - 30
        }
        canvas.drawLine(xP, yP, xEdP, yEdP, mPointingPaint!!)
        canvas.drawLine(xEdP, yEdP, xLast, yEdP, mPointingPaint!!)
        canvas.drawText(mData!![i].name!!, xLast, yEdP, mDataPaint!!)
        canvas.drawText(
            mData!![i].num!!.toString() + mData!![i].unit,
            xLast,
            yEdP - mDataPaint!!.ascent() + 5,
            mUnitPaint!!
        )
    }

b.在右侧或者底部说明,动态加入RecyclerView,在RecyclerView中填入说明,在扇形图中填入对应的数字或者比列即可。让MyPieChartView继承FrameLayout,动态加入RecyclerView控件,匹配对应的adapter

 private fun addHorizontal() {
        mRecyclerView = RecyclerView(context)
        mRecyclerView!!.layoutManager = LinearLayoutManager(context)
        mRecyclerView!!.isNestedScrollingEnabled = false
        if (adapter == null) {
            adapter = mAdapter(mContext!!, mData)
            mRecyclerView!!.adapter = adapter
        } else {
            adapter!!.notifyDataSetChanged()
        }

        val relativeLayout = RelativeLayout(context)
        val params = LayoutParams(
            ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup
                .LayoutParams.WRAP_CONTENT
        )
        params.gravity = Gravity.RIGHT or Gravity.CENTER_VERTICAL
        relativeLayout.layoutParams = params
        val p2 = RelativeLayout.LayoutParams(
            ViewGroup.LayoutParams
                .WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT
        )
        relativeLayout.addView(mRecyclerView, p2)
        addView(relativeLayout)
    }

    private fun addVertical() {
        mRecyclerView = RecyclerView(context)
        mRecyclerView!!.layoutManager = GridLayoutManager(context, 3)
        mRecyclerView!!.isNestedScrollingEnabled = false
        if (adapter == null) {
            adapter = mAdapter(mContext!!, mData)
            mRecyclerView!!.adapter = adapter
        } else {
            adapter!!.notifyDataSetChanged()
        }

        val relativeLayout = RelativeLayout(context)
        val params = LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup
                .LayoutParams.WRAP_CONTENT
        )
        params.gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL
        relativeLayout.layoutParams = params
        val p2 = RelativeLayout.LayoutParams(
            ViewGroup.LayoutParams
                .MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT
        )
        relativeLayout.addView(mRecyclerView, p2)
        addView(relativeLayout)
    }

相关api

属性 名称
dataSize 数据大小
dataColor 数据颜色
numSize 数字或者比例大小
numColor 数字或者比例颜色
layoutType 布局方式(default 普通样式 vertical 竖向布局 horizontal 横向布局 pointingInstructions 指向说明)
verticalMargin 竖向布局时扇形与说明的间距
horiMargin 横向布局时扇形与说明的间距
animTime 动画时长
pointingColor 指向说明的线的颜色
pointingWidth 指向说明的线的宽度

使用方法

将MyPieChartView,Util,PieChartData,PieChartType,PieChartConstant,attrs.xml下的MyPieChartView拷贝到自己的项目中即可使用。
github:https://github.com/2016lc/MyCircleProgress

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • 【Android 自定义View之绘图】 基础图形的绘制 一、Paint与Canvas 绘图需要两个工具,笔和纸。...
    Rtia阅读 12,156评论 5 34
  • 一、概述 1. 四线格与基线 小时候,我们在刚开始学习写字母时,用的本子是四线格的,我们必须把字母按照规则写在四线...
    addapp阅读 7,996评论 2 17
  • 系列文章之 Android中自定义View(一)系列文章之 Android中自定义View(二)系列文章之 And...
    YoungerDev阅读 4,624评论 3 11
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 13,573评论 2 59
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 177,409评论 25 709

友情链接更多精彩内容