package com.example.ui.widget
import androidx.compose.animation.core.*
import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ProgressIndicatorDefaults
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.unit.Dp
import kotlinx.coroutines.launch
/**
* 单圈旋转进度条(6 秒整圈,可暂停/继续/重置)
*
* @param modifier 外部修饰
* @param size 直径
* @param strokeWidth 线宽
* @param color 进度条颜色
* @param strokeCap 线头形状
* @param onFinish 自然结束回调
*/
@Composable
fun SingleTurnSpinner(
modifier: Modifier = Modifier,
size: Dp = ProgressIndicatorDefaults.CircularStrokeWidth,
strokeWidth: Dp = ProgressIndicatorDefaults.CircularStrokeWidth,
color: Color = Color.Blue,
strokeCap: StrokeCap = StrokeCap.Round,
onFinish: () -> Unit = {}
) {
val scope = rememberCoroutineScope()
val angle = remember { Animatable(0f) }
var isRunning by remember { mutableStateOf(false) }
// 对外暴露的控制器
val controller = remember {
object : SpinnerController {
override fun start() {
if (isRunning) return
isRunning = true
val remaining = 360f - angle.value
val remainingTime = (remaining / 360f * 6000).toLong().coerceAtLeast(0L)
scope.launch {
angle.animateTo(
targetValue = 360f,
animationSpec = tween(remainingTime.toInt(), easing = LinearEasing)
)
isRunning = false
onFinish()
}
}
override fun pause() {
if (!isRunning) return
angle.stop()
isRunning = false
}
override fun reset() {
angle.stop()
scope.launch { angle.snapTo(0f) }
isRunning = false
}
override val currentAngle: Float get() = angle.value
override val running: Boolean get() = isRunning
}
}
// 真正 UI
CircularProgressIndicator(
progress = 1f, // 始终画满环
modifier = modifier
.size(size)
.rotate(angle.value),
strokeWidth = strokeWidth,
color = color,
strokeCap = strokeCap
)
// 把控制器通过 CompositionLocal 暴露(可选)
SpinnerControllerProvider(controller) {
// 空容器,仅用于携带 LocalSpinnerController
}
}
/* -------------------- 控制接口 -------------------- */
interface SpinnerController {
fun start()
fun pause()
fun reset()
val currentAngle: Float
val running: Boolean
}
/* -------------------- CompositionLocal 方便获取 -------------------- */
val LocalSpinnerController = staticCompositionLocalOf<SpinnerController?> { null }
@Composable
private fun SpinnerControllerProvider(
controller: SpinnerController,
content: @Composable () -> Unit
) {
CompositionLocalProvider(LocalSpinnerController provides controller) {
content()
}
}
/* -------------------- 使用示例 -------------------- */
@Composable
fun DemoScreen() {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
// 直接拿来用
val controller = LocalSpinnerController.current!!
SingleTurnSpinner(
size = 120.dp,
strokeWidth = 6.dp,
color = MaterialTheme.colorScheme.primary
)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(onClick = { controller.start() }) { Text("开始") }
Button(onClick = { controller.pause() }) { Text("暂停") }
Button(onClick = { controller.reset() }) { Text("重置") }
}
}
}