compose自定义控件

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("重置") }

}

}

}

​​​​

相关推荐
码上有光5 分钟前
Linux进程通信——共享内存、消息队列和信号量
android·linux·运维·共享内存·通信
Android打工仔4 小时前
Kotlin 协程源码解析:DispatchedContinuation 里的 Dispatcher 从哪里来?
android·kotlin
打工仔折腾 AI4 小时前
从BPE到SentencePiece:Transformer分词原理与Python实战对比
android·人工智能·python·深度学习·langchain·transformer·ai agent 实战
Dovis(誓平步青云)5 小时前
几个方案来回选不定?做一个随时切换的候选推荐页
android·java·服务器·开发语言·javascript·数据库·智能化
恋猫de小郭9 小时前
KMP 又改了编译流程, Separate Compilation 禁止了 `commonMain` 的依赖穿透
android·前端·flutter
执明wa9 小时前
Android RecyclerView 实战:点击频道栏切换对应内容
android·开发语言·windows·microsoft·android studio
新鲜势力呀9 小时前
PHP 定时数据同步实战:从第三方接口超时到异步同步 + 增量更新架构优化全过程
android
星间都市山脉11 小时前
RecoveryUI 圆角屏安全边距配置及代码调用链
android·java·linux·windows·ubuntu
打工仔折腾 AI11 小时前
DiPlay 实测:iPhone 绕过硬件盒子直连 BYD 车机的思路拆解
android·人工智能·后端·python·gradle·iphone·ai agent 实战
hengdonghui12 小时前
Writeup 4 NUAA 2017 robots
android·ctf·re