绘制一个图表,可以选中状态如下
简单说一下流程:
1.基本原理是遍历绘制扇形每次绘制记录起始点,然后再绘制一个白色圆覆盖便形成一个图标圆环
2.绘制一个大圆为选中圆,设置非选中部分为透明
3.通过点击xy坐标获取点击圆的位置,重绘
4.反向绘制圆加载动画
具体看代码里面详细注释描述:
class CircleChartView : View {
//饼状图画笔
private lateinit var mPiePaint: Paint
//阴影画笔
private var mPaintShadow: Paint? = null
//默认第一张图半径
private val mRadiusOne = DensityUtils.dp2px(context, 80f).toFloat()
private val mRadiusOneCover = DensityUtils.dp2px(context, 55f).toFloat()
private val mRadiusTwo = DensityUtils.dp2px(context, 85f).toFloat()
private val mRadiusTwoCover = DensityUtils.dp2px(context, 50f).toFloat()
private val mRadiusInside = DensityUtils.dp2px(context, 47f).toFloat()
private val mRadiusInsideCover = DensityUtils.dp2px(context, 45f).toFloat()
//圆和view边框距离
private val TOFRAME = DensityUtils.dp2px(context, 40f).toFloat()
//构成饼状图的数据集合
private var mPieDataList: List = ArrayList()
//绘制弧形的sweep数组
private var mPieSweep: FloatArray? = null
//初始画弧所在的角度
private val START_DEGREE = -90
private var animPro: Float = 0.toFloat()
private var isStartAnim = true
//默认圆
private val mRectFOne = RectF()
private val mRectFSelect = RectF()
private val mRectFInside = RectF()
private val mRectFCover = RectF()
//扇形外部text起始点坐标
private var outTextX: Float = 0.toFloat()
private var outTextY: Float = 0.toFloat()
private var centerMoney = ""
private var totalText = ""
private var mListener: OnSpecialTypeClickListener? = null
interface OnSpecialTypeClickListener {
fun onSpecialTypeClick(index: Int, type: String)
}
fun setOnSpecialTypeClickListener(listener: OnSpecialTypeClickListener?) {
this.mListener = listener
}
constructor(context: Context?) : this(context, null)
constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init()
}
//初始化画笔
private fun init() {
mPiePaint = Paint()
mPiePaint.isAntiAlias = true
mPiePaint.style = Paint.Style.FILL
mPaintShadow = Paint()
mPaintShadow?.color = Color.WHITE
mPaintShadow?.style = Paint.Style.FILL
mPaintShadow?.maskFilter = BlurMaskFilter(2f, BlurMaskFilter.Blur.NORMAL)
initRectF()
initSelectRectF()
initInsideSelectRectF()
initRectFCover()
}
/**
* 初始化绘制弧形所在矩形的四点坐标
*/
private fun initRectFCover() {
mRectFCover.left = TOFRAME - 1
mRectFCover.top = TOFRAME - 1
mRectFCover.right = 2 * mRadiusOne + TOFRAME + 1f
mRectFCover.bottom = 2 * mRadiusOne + TOFRAME + 1f
}
/**
* 初始化绘制弧形所在矩形的四点坐标
*/
private fun initRectF() {
mRectFOne.left = TOFRAME
mRectFOne.top = TOFRAME
mRectFOne.right = 2 * mRadiusOne + TOFRAME
mRectFOne.bottom = 2 * mRadiusOne + TOFRAME
}
/**
* 选中圆
*/
private fun initSelectRectF() {
mRectFSelect.left = TOFRAME - DensityUtils.dp2px(context, 5f)
mRectFSelect.top = TOFRAME - DensityUtils.dp2px(context, 5f)
mRectFSelect.right = 2 * mRadiusTwo + TOFRAME - DensityUtils.dp2px(context, 5f)
mRectFSelect.bottom = 2 * mRadiusTwo + TOFRAME - DensityUtils.dp2px(context, 5f)
}
/**
* 内部圆
*/
private fun initInsideSelectRectF() {
mRectFInside.left = TOFRAME + DensityUtils.dp2px(context, 33f)
mRectFInside.top = TOFRAME + DensityUtils.dp2px(context, 33f)
mRectFInside.right = 2 * mRadiusInside + TOFRAME + DensityUtils.dp2px(context, 33f).toFloat()
mRectFInside.bottom = 2 * mRadiusInside + TOFRAME + DensityUtils.dp2px(context, 33f).toFloat()
}
fun setTotalText(totalText: String) {
this.totalText = totalText
}
fun setTextMoney(centerMoney: String) {
this.centerMoney = centerMoney
}
fun startAnimation() {
val valueAnimator = ValueAnimator.ofFloat(0f, 1f)
//从0到1 意思是从没有到原本设置的那个值
valueAnimator.interpolator = AccelerateDecelerateInterpolator()
valueAnimator.addUpdateListener { animation ->
animPro = animation.animatedValue as Float
//原来值的完成度
invalidate()
//重绘
}
valueAnimator.duration = 2000
valueAnimator.start()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
setLayerType(View.LAYER_TYPE_SOFTWARE, null)
if (!mPieDataList.isEmpty()) {
//起始是从-90°位置开始画
var pieStartOne = START_DEGREE.toFloat()
var pieStartTwo = START_DEGREE.toFloat()
var pieStartInside = START_DEGREE.toFloat()
if (mPieSweep == null) {
mPieSweep = FloatArray(mPieDataList.size)
}
//底部圆
for (i in mPieDataList.indices) {
//设置弧形颜色
mPiePaint.color = Color.parseColor(mPieDataList[i].color)
//绘制弧形区域,以构成饼状图
val pieSweep = getProportion(i) * 360
canvas.drawArc(mRectFOne, pieStartOne, mPieSweep!![i], true, mPiePaint)
//获取下一个弧形的起点
pieStartOne += pieSweep
}
if (isStartAnim) {
isStartAnim = false
startAnimation()
}
mPiePaint.color = Color.parseColor("#ffffff")//白色
canvas.drawArc(mRectFCover, -90f, -360 * (1 - animPro), true, mPiePaint)
mPiePaint.color = Color.parseColor("#ffffff")//白色
canvas.drawCircle(mRadiusOne + TOFRAME, mRadiusOne + TOFRAME, mRadiusOneCover, mPiePaint)
//选中圆
for (i in mPieDataList.indices) {
val pieSweep = getProportion(i) * 360
//设置弧形颜色
if (mPieDataList[i].isSelected) {
//获取外部位置
drawText(pieStartTwo, pieSweep)
mPiePaint.color = Color.parseColor(mPieDataList[i].color)
} else {
mPiePaint.color = Color.parseColor(mPieDataList[i].on_select_color)
}
//绘制弧形区域,以构成饼状图
canvas.drawArc(mRectFSelect, pieStartTwo, mPieSweep!![i], true, mPiePaint)
//获取下一个弧形的起点
pieStartTwo += pieSweep
}
mPiePaint.color = Color.parseColor("#ffffff")//白色
canvas.drawCircle(mRadiusOne + TOFRAME, mRadiusOne + TOFRAME, mRadiusTwoCover, mPiePaint)
//内部圆
for (i in mPieDataList.indices) {
val pieSweep = getProportion(i) * 360
//设置弧形颜色
mPiePaint.color = Color.parseColor(mPieDataList[i].color)
//绘制弧形区域,以构成饼状图
canvas.drawArc(mRectFInside, pieStartInside, mPieSweep!![i], true, mPiePaint)
//获取下一个弧形的起点
pieStartInside += pieSweep
}
mPiePaint.color = Color.parseColor("#ffffff")//白色
canvas.drawCircle(mRadiusOne + TOFRAME, mRadiusOne + TOFRAME, mRadiusInsideCover, mPiePaint)
//画一个矩形
mPiePaint.color = Color.TRANSPARENT
mPiePaint.style = Paint.Style.FILL
canvas.drawRect(mRectFInside, mPiePaint)
//绘制文字
mPiePaint.isAntiAlias = true
mPiePaint.color = Color.BLUE
mPiePaint.style = Paint.Style.FILL
//该方法即为设置基线上那个点究竟是left,center,还是right 这里我设置为center
mPiePaint.textAlign = Paint.Align.CENTER
val fontMetrics = mPiePaint.fontMetrics
val top = fontMetrics?.top//为基线到字体上边框的距离,即上图中的top
val bottom = fontMetrics?.bottom//为基线到字体下边框的距离,即上图中的bottom
val baseLineY = (mRectFInside.centerY() - top!! / 2 - bottom!! / 2).toInt()//基线中间点的y轴计算公式
mPiePaint.textSize = 22f
mPiePaint.color = Color.parseColor("#999999")
canvas.drawText(totalText, mRectFInside.centerX(), (baseLineY - 20).toFloat(), mPiePaint)
mPiePaint.textSize = 24f
mPiePaint.color = Color.parseColor("#333333")
mPiePaint.typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD)
canvas.drawText(centerMoney, mRectFInside.centerX(), (baseLineY + 10).toFloat(), mPiePaint)
//绘制外围选中
for (i in mPieDataList.indices) {
if (mPieDataList[i].isSelected) {
//绘制圆形
canvas.drawCircle(outTextX, outTextY, DensityUtils.dp2px(context, 22.5f).toFloat(), mPaintShadow) //阴影
mPiePaint.color = Color.parseColor(mPieDataList[i].color)
canvas.drawCircle(outTextX, outTextY, DensityUtils.dp2px(context, 21.5f).toFloat(), mPiePaint)
mPiePaint.color = Color.parseColor("#ffffff")
canvas.drawCircle(outTextX, outTextY, DensityUtils.dp2px(context, 17.5f).toFloat(), mPiePaint)
//绘制提示内容
mPiePaint.textSize = 18f
mPiePaint.color = Color.BLACK
mPiePaint.typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD)
canvas.drawText(mPieDataList[i].type, outTextX, outTextY + 5, mPiePaint)
}
}
} else {
//无数据时,显示灰色圆环
mPiePaint.color = Color.parseColor("#dadada")//灰色
canvas.drawCircle(mRadiusOne, mRadiusOne, mRadiusOne, mPiePaint)
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val length = (2 * (mRadiusOne + TOFRAME)).toInt()
setMeasuredDimension(length, length)
}
/**
* 获取圆弧外位置
*/
private fun drawText(pieStart: Float, pieSweep: Float) {
// //弧形区域角平分线距离-90角度
val a = pieStart + pieSweep / 2 + 90f
if (a < 90) {
//右上部
outTextX = mRadiusTwo + TOFRAME + (mRadiusTwo * Math.sin(java.lang.Double.parseDouble(((a / 360).toDouble() * 2.0 * Math.PI).toString()))).toFloat()
outTextY = mRadiusTwo + TOFRAME - (mRadiusTwo * Math.cos(java.lang.Double.parseDouble(((a / 360).toDouble() * 2.0 * Math.PI).toString()))).toFloat() - 40f
} else if (a == 90f) {
outTextX = mRadiusTwo + TOFRAME + (mRadiusTwo * Math.sin(java.lang.Double.parseDouble(((a / 360).toDouble() * 2.0 * Math.PI).toString()))).toFloat() + 10f
outTextY = mRadiusTwo + TOFRAME - (mRadiusTwo * Math.cos(java.lang.Double.parseDouble(((a / 360).toDouble() * 2.0 * Math.PI).toString()))).toFloat()
} else if (a < 180) {
//右下
outTextX = mRadiusTwo + TOFRAME + (mRadiusTwo * Math.sin(java.lang.Double.parseDouble(((a / 360).toDouble() * 2.0 * Math.PI).toString()))).toFloat() + 10f
outTextY = mRadiusTwo + TOFRAME - (mRadiusTwo * Math.cos(java.lang.Double.parseDouble(((a / 360).toDouble() * 2.0 * Math.PI).toString()))).toFloat() + 10
} else if (a == 180f) {
outTextX = mRadiusTwo + TOFRAME + (mRadiusTwo * Math.sin(java.lang.Double.parseDouble(((a / 360).toDouble() * 2.0 * Math.PI).toString()))).toFloat() - 20
outTextY = mRadiusTwo + TOFRAME - (mRadiusTwo * Math.cos(java.lang.Double.parseDouble(((a / 360).toDouble() * 2.0 * Math.PI).toString()))).toFloat() + 10
} else if (a < 270) {
//左下
outTextX = mRadiusTwo + TOFRAME + (mRadiusTwo * Math.sin(java.lang.Double.parseDouble(((a / 360).toDouble() * 2.0 * Math.PI).toString()))).toFloat() - 30
outTextY = mRadiusTwo + TOFRAME - (mRadiusTwo * Math.cos(java.lang.Double.parseDouble(((a / 360).toDouble() * 2.0 * Math.PI).toString()))).toFloat() + 20
} else if (a == 270f) {
outTextX = mRadiusTwo + TOFRAME + (mRadiusTwo * Math.sin(java.lang.Double.parseDouble(((a / 360).toDouble() * 2.0 * Math.PI).toString()))).toFloat() - 60
outTextY = mRadiusTwo + TOFRAME - (mRadiusTwo * Math.cos(java.lang.Double.parseDouble(((a / 360).toDouble() * 2.0 * Math.PI).toString()))).toFloat()
} else if (a < 360) {
//左上
outTextX = mRadiusTwo + TOFRAME + (mRadiusTwo * Math.sin(java.lang.Double.parseDouble(((a / 360).toDouble() * 2.0 * Math.PI).toString()))).toFloat() - 40
outTextY = mRadiusTwo + TOFRAME - (mRadiusTwo * Math.cos(java.lang.Double.parseDouble(((a / 360).toDouble() * 2.0 * Math.PI).toString()))).toFloat() + 10
}
}
/**
* 所在区域占总区域比例
*/
fun getProportion(i: Int): Float {
return mPieDataList[i].value / getSumData(mPieDataList)
}
/**
* 获取各区域数值总和
*/
fun getSumData(mPieDataList: List?): Float {
if (mPieDataList == null || mPieDataList.isEmpty()) {
return 0f
}
var mSum = 0f
for (i in mPieDataList.indices) {
mSum += mPieDataList[i].value
}
return mSum
}
/**
* 设置需要绘制的数据集合
*/
fun setPieDataList(pieDataList: List) {
this.mPieDataList = pieDataList
if (mPieSweep == null) {
mPieSweep = FloatArray(mPieDataList!!.size)
}
for (i in pieDataList.indices) {
mPieSweep!![i] = getProportion(i) * 360
}
}
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> doOnSpecialTypeClick(event)
}
return super.onTouchEvent(event)
}
private fun doOnSpecialTypeClick(event: MotionEvent) {
if (mPieDataList.isEmpty()) {
return
}
val which = touchOnWhichPart(event)
if (mListener != null && which != 1000) {
for (i in mPieDataList.indices) {
mPieDataList[i].isSelected = i == which
}
mListener!!.onSpecialTypeClick(which, mPieDataList[which].type)
}
}
/**
* 点击位置
*/
private fun touchOnWhichPart(event: MotionEvent): Int {
val x = event.x
val y = event.y
var mWhich = 1000//错误码
var a = 0.0//与-90的夹角
var sum = 0f//所占比例和
val newRadius = mRadiusOne + TOFRAME//用来参与计算的半径+间距
//在圆内
if (Math.pow((x - newRadius).toDouble(), 2.0) + Math.pow((y - newRadius).toDouble(), 2.0) <= Math.pow(mRadiusOne.toDouble(), 2.0)) {
if (event.x > newRadius) {
//圆的右半部
if (event.y > newRadius) {
//圆的下半部(综上:右下 0-90)
a = Math.PI / 2 + Math.atan(java.lang.Double.parseDouble(((y - newRadius) / (x - newRadius)).toString()))
} else {
//右上(-90-0)
a = Math.atan(java.lang.Double.parseDouble(((x - newRadius) / (newRadius - y)).toString()))
}
} else {
//圆的左半部
if (event.y > newRadius) {
//圆的下半部(综上:左下 90-180)
a = Math.PI + Math.atan(java.lang.Double.parseDouble(((newRadius - x) / (y - newRadius)).toString()))
} else {
//左上 180-270
a = 2 * Math.PI - Math.atan(java.lang.Double.parseDouble(((newRadius - x) / (newRadius - y)).toString()))
}
}
for (i in mPieDataList.indices) {
if (i < mPieDataList.size - 1) {
sum += getProportion(i)
} else {
sum = 1f
}
if (a / (2 * Math.PI) <= sum) {
mWhich = i
break
}
}
}
return mWhich
}
data class PieData(val type:String,val value:Float,val color:String,val on_select_color:String,val isSelected:Boolean)
测试Demo使用:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val list = ArrayList()
list.add(PieData("20%", 20f, "#2828ff", "#00000000", false))
list.add(PieData("10%", 10f, "#5b5b5b", "#00000000", false))
list.add(PieData("55%", 55f, "#d200d2", "#00000000", false))
list.add(PieData("35%", 35f, "#02df08", "#00000000", false))
list.add(PieData("60%", 60f, "#9999cc", "#00000000", false))
list.add(PieData("40%", 40f, "#ff5809", "#00000000", false))
cc_view.setTotalText("总数")
cc_view.setTextMoney("100000¥")
cc_view.setPieDataList(list)
cc_view.setOnSpecialTypeClickListener(object : CircleChartView.OnSpecialTypeClickListener {
override fun onSpecialTypeClick(index: Int, type: String) {
cc_view.invalidate()
}
})
}
}