はじめに
今回は進捗用のメータをCanvasで実装していきます
コード
@Composable
fun ProgressMeter(
currentStep: Int,
modifier: Modifier = Modifier,
) {
// 現在ステップを 0.0 - 1.0 の進捗率に変換し、ゲージ更新をアニメーションさせる部分
val animatedProgress = animateFloatAsState(
targetValue = currentStep.coerceIn(0, 6).toFloat() / 6f,
animationSpec = tween(durationMillis = 600),
label = ""
)
Canvas(modifier = modifier) {
// ゲージ全体の見た目を決める基本サイズを組み立てる部分
val strokeWidth = 12.dp.toPx()
val centerDotRadius = 6.dp.toPx()
val needleBaseWidth = 6.dp.toPx()
val needleTailLength = 8.dp.toPx()
val center = Offset(x = size.width / 2f, y = size.height - strokeWidth / 2f)
val radius = minOf(
size.width / 2f - strokeWidth / 2f,
size.height - strokeWidth / 2f
)
val arcTopLeft = Offset(x = center.x - radius, y = center.y - radius)
val arcSize = Size(width = radius * 2f, height = radius * 2f)
val segmentSweepAngle = (180f - 4f * 6f) / 6f
val clampedStep = currentStep.coerceIn(1, 6)
val activeSegmentIndex = clampedStep - 1
// 6 分割のメーターを描き、未達成部分と達成部分の色を描き分ける部分
repeat(6) { index ->
val segmentStartAngle = 180f + index * (segmentSweepAngle + 4f)
val segmentProgress =
(animatedProgress.value * 6f - index).coerceIn(0f, 1f)
drawArc(
color = Color(0xFFEBEBEB),
startAngle = segmentStartAngle,
sweepAngle = segmentSweepAngle,
useCenter = false,
topLeft = arcTopLeft,
size = arcSize,
style = Stroke(width = strokeWidth, cap = StrokeCap.Butt)
)
if (segmentProgress > 0f) {
drawArc(
color = if (index == activeSegmentIndex) {
Color(0xFFFFB238)
} else {
Color(0xFFFFC870)
},
startAngle = segmentStartAngle,
sweepAngle = segmentSweepAngle * segmentProgress,
useCenter = false,
topLeft = arcTopLeft,
size = arcSize,
style = Stroke(width = strokeWidth, cap = StrokeCap.Butt)
)
}
}
// 現在位置を指す針の角度を計算し、四角形に近いポリゴンとして描画する部分
val needleAngle = 180f + activeSegmentIndex * (segmentSweepAngle + 4f) + segmentSweepAngle / 2f
val needleLength = radius - strokeWidth
val tip = center + polarOffset(needleAngle, needleLength)
val tail = center + polarOffset(needleAngle + 180f, needleTailLength)
val leftBase = center + polarOffset(needleAngle + 90f, needleBaseWidth / 2f)
val rightBase = center + polarOffset(needleAngle - 90f, needleBaseWidth / 2f)
drawPath(
path = Path().apply {
moveTo(tail.x, tail.y)
lineTo(leftBase.x, leftBase.y)
lineTo(tip.x, tip.y)
lineTo(rightBase.x, rightBase.y)
close()
},
color = Color(0xFF272525)
)
// 針の根元に円を重ねて、メーター中心の見た目を整える部分
drawCircle(
color = Color(0xFF272525),
radius = centerDotRadius,
center = center
)
}
}
// 角度と長さから X/Y 座標への移動量を作り、円弧上の点を計算する補助関数
private fun polarOffset(
angleInDegrees: Float,
length: Float,
): Offset {
val angleInRadians = Math.toRadians(angleInDegrees.toDouble())
return Offset(
x = (cos(angleInRadians) * length).toFloat(),
y = (sin(angleInRadians) * length).toFloat()
)
}
最後に
マックスを変更すればいろんなものに使えるので便利だと思います