一个比较简单的效果如下
附带一点简单的点击效果:点击后所在的扇形弹出一点,与原来的分隔开
绘制扇形使用的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
}
}