扇形图自定义
实现
总体思路:根据每个数据所占的比例*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