Compose 利用Canvas绘制圆形进度条、线性进度条、饼图、柱状图
绘制圆形进度条
进度背景、进度、标题文字、进度文字
@Composable
@OptIn(ExperimentalTextApi::class)
private fun CircleProgress(
title: String = "完成率",//标题
titleSize: Float = 18f,//标题大小
progressSize: Float = 16f,//进度字体大小
progress: Float = 0f,//进度
textColor: Color = Color.White,//字体颜色
bgColor: Color = Color(0xFF1A5476),//背景颜色
progressColor: Color = Color(0xFF1CCDCB),//进度颜色
strokeWidth: Dp = 60.dp,//进度条宽度
spaceWidth: Dp = 200.dp//画布大小
) {
Box(
Modifier
.wrapContentSize()
.background(Color(0xFF1D405D))
) {
val measurer = rememberTextMeasurer()
Canvas(modifier = Modifier.size(spaceWidth), onDraw = {
val radius = (size.width.minus(strokeWidth.value)).div(2)
val space = size.width
//背景
drawCircle(
bgColor, radius = radius, style = Stroke(width = strokeWidth.value)
)
//进度
drawArc(
color = progressColor,
topLeft = Offset(strokeWidth.value / 2, strokeWidth.value / 2),
size = Size(space - strokeWidth.value, space - strokeWidth.value),
useCenter = false,
startAngle = 0f,
sweepAngle = if (progress > 100f) 360f else progress.times(360f.div(100f)),
style = Stroke(width = strokeWidth.value, cap = StrokeCap.Round)
)
var resultTitle = measurer.measure(
AnnotatedString(title), style = TextStyle(
textColor, fontSize = TextUnit(
titleSize, TextUnitType.Sp
)
)
)
drawText(
resultTitle, topLeft = Offset(
(space - resultTitle.size.width).div(2),
space.div(2).minus(resultTitle.size.height).minus(4.dp.value)
)
)
val np = DecimalFormat("0.00")
val str = np.format(progress)
var result = measurer.measure(
AnnotatedString("$str%"), style = TextStyle(
textColor, fontSize = TextUnit(
progressSize, TextUnitType.Sp
)
)
)
drawText(
result, topLeft = Offset(
(space - result.size.width).div(2), space.div(2).plus(4.dp.value)
)
)
})
}
}
绘制线性进度条
进度背景、进度
@Composable
private fun LinearProgress(
progress: Float = 0f,
bgColor: Color = Color(0xFF1A5476),
progressColor: Color = Color(0xFF1CCDCB),
strokeWidth: Dp = 20.dp,
cornerWidth: Dp = 40.dp
) {
Box(
Modifier.wrapContentSize()
// .background(Color(0xFF1D405D))
) {
Canvas(modifier = Modifier
.fillMaxWidth()
.padding(horizontal = cornerWidth)
.height(strokeWidth), onDraw = {
//背景
drawRoundRect(
bgColor, cornerRadius = CornerRadius(cornerWidth.value, cornerWidth.value)
)
drawRoundRect(
progressColor, topLeft = Offset(0f, 0f), size = Size(
if (progress > 100f) size.width else progress.times(
size.width.div(100)
), size.height
), cornerRadius = CornerRadius(cornerWidth.value, cornerWidth.value)
)
})
}
}
绘制饼图
滑动手势方向拆分、旋转角度计算、扇形点击范围判断、放大缩小手势计算、文字填充(需要进一步实现)
data class PieChartPiece(
val color: Color, val startAngle: Float, val sweepAngle: Float
)
private fun containsPoint(//判断点是否在区域内
width: Float, startAngle: Float, sweepAngle: Float, dotX: Int, dotY: Int
): Boolean {
var region = Region();
var path = Path()
val rectF = RectF(0f, 0f, width, width)
val rect = Rect(0, 0, width.toInt(), width.toInt())
path.moveTo(width.div(2), width.div(2))
path.lineTo(width, width.div(2))
path.addArc(rectF, startAngle, sweepAngle)
path.lineTo(width.div(2), width.div(2))
path.close()
region.setPath(path, Region(rect))
return region.contains(dotX, dotY)
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun PieChart(charts: MutableList<PieChartPiece>) {
var rawX by remember {
mutableFloatStateOf(0f)
}
var rawY by remember {
mutableFloatStateOf(0f)
}
var degree by remember {
mutableFloatStateOf(0f)
}
var initRawX = 0f;
var initRawY = 0f;
var pointerId by remember {
mutableIntStateOf(1)
}
val textMeasurer = rememberTextMeasurer()
Box(
Modifier.wrapContentSize()
) {
Canvas(modifier = Modifier
.size(200.dp)
.pointerInteropFilter(requestDisallowInterceptTouchEvent = RequestDisallowInterceptTouchEvent(),
onTouchEvent = {
when (it.action) {
MotionEvent.ACTION_DOWN -> {
initRawX = it.x
initRawY = it.y
Log.e("TAG", "ACTION_DOWN $initRawX $initRawY ")
return@pointerInteropFilter true
}
MotionEvent.ACTION_MOVE, MotionEvent.ACTION_UP -> {
rawX = it.x - initRawX
rawY = it.y - initRawY
Log.e("TAG", "ACTION_MOVE_UP $rawX $rawY ")
if (rawX == 0f && rawY == 0f || MotionEvent.ACTION_UP == it.action) {
pointerId++
} else {
pointerId = 1
}
return@pointerInteropFilter true
}
}
false
}), onDraw = {
if (pointerId == 1 && rawX != 0f && rawY != 0f) {
var arcLength = size.width.times(Math.PI).toFloat()
var piece = arcLength.div(360)
degree += if (abs(rawX) > abs(rawY)) {
if (size.height.div(2) > initRawY) {
rawX.div(piece)
} else {
-rawX.div(piece)
}
} else {
if (size.width.div(2) > initRawX) {
-rawY.div(piece)
} else {
rawY.div(piece)
}
}
}
Log.e("TAG", "$rawX $rawY degree $degree")
rotate(degree) {
repeat(charts.size) {
val chart = charts[it]
var scaleCurrent =
if (pointerId != 1 && rawX == 0f && rawY == 0f && containsPoint(
size.width,
degree.plus(chart.startAngle),
chart.sweepAngle,
initRawX.toInt(),
initRawY.toInt()
)
) 1.1f else 1f
Log.e("TAG", "scaleCurrent $scaleCurrent")
scale(
scaleCurrent,
pivot = Offset(
size.width.div(2), size.height.div(2)
)
) {
drawArc(
chart.color,
chart.startAngle,
chart.sweepAngle,
useCenter = true,
topLeft = Offset.Zero,
style = Fill
)
val angle = chart.startAngle.plus(chart.sweepAngle.div(2))
var y = sin(angle) * size.width.div(2)
var x = cos(angle) * size.width.div(2)
when (if (angle > 360) angle.minus((angle % 360) * 360) else angle) {
in 0f..90f -> {//右下
}
in 91f..180f -> {//左下
x = -abs(x)
}
in 181f..270f -> {//左上
x = -abs(x)
y = -abs(y)
}
in 271f..360f -> {//右上
y = -abs(y)
}
}
var measurer = textMeasurer.measure(chart.sweepAngle.toString())
drawText(
measurer,
Color(0xFFab4b52),
topLeft = Offset(
size.width.div(2) - measurer.size.width.div(2) + x.div(2),
size.height.div(2) - measurer.size.height.div(2) + y.div(2)
)
)
}
}
}
})
}
}
//引用方式
val charts = mutableListOf<PieChartPiece>()
charts.add(PieChartPiece(Color.Yellow, 0f, 60f))
charts.add(PieChartPiece(Color.Cyan, 60f, 300f))
PieChart(charts)
绘制简单柱状图表
横竖坐标轴、0000代指字体尺寸测量
@Composable
private fun LinearBar() {
val withD = LocalConfiguration.current.screenWidthDp
val m = rememberTextMeasurer()
Box(
Modifier
.wrapContentSize()
.background(Color.LightGray)
) {
Canvas(modifier = Modifier
.fillMaxWidth()
.height(withD.div(2).dp), onDraw = {
var wt = m.measure("0000").size.width
var ht = m.measure("0000").size.height
var hl = size.height - ht.toFloat()
drawLine(//竖线
Color.Magenta,
start = Offset(wt.toFloat(), 0f),
end = Offset(wt.toFloat(), hl)
)
drawLine(//横线
Color.Magenta,
start = Offset(wt.toFloat(), hl),
end = Offset(size.width, hl)
)
for (i in 1 until 6) {
drawText(
m,
"${i}000",
topLeft = Offset(0f, hl.times(5 - i).div(5)),
style = TextStyle(Color.DarkGray, fontStyle = FontStyle.Italic)
)
}
val barWidth = wt.toFloat()
val space = wt.div(2).toFloat()
for (i in 1 until 6) {
var topLeft = wt + space * i + barWidth * (i - 1) + barWidth.div(2)
drawText(
m,
"item$i",
topLeft = Offset(
topLeft,
size.height - ht
),
style = TextStyle(Color.DarkGray, fontWeight = FontWeight.Medium)
)
drawLine(
Color(0xFF1D405D),
start = Offset(topLeft + barWidth.div(2), Random.nextFloat() * hl),
end = Offset(topLeft + barWidth.div(2), hl),
strokeWidth = barWidth
)
}
})
}
}