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

}

}

}

​​​​

相关推荐
石山岭10 小时前
自己动手写了一个 Android 虚拟定位 App:GPSSimulate 技术实
android·前端
杉氧12 小时前
副作用 (Side Effects) 全攻略:如何像大师一样掌控 Composable 的生命周期?
android·架构·android jetpack
Kapaseker17 小时前
Kotlin Toolchain 0.11 发布:主要是把 Amper 干没了
android·kotlin
三少爷的鞋18 小时前
Android 现代架构不需要事件总线进阶篇
android
杉氧1 天前
深入理解 Compose 重组机制:快照系统如何驱动 UI 精准刷新?
android·架构·android jetpack
召钱熏1 天前
状态枚举正确≠渲染正确:一个语音按钮的状态机边界修复实录
android·前端
杉氧1 天前
深度解析:Jetpack Compose 核心架构与底层原理 —— 十年安卓老兵的“破茧重生”
android·架构·android jetpack
通玄1 天前
Jetpack Compose 入门系列(七):ViewModel 与界面状态管理
android
落魄Android在线炒饭1 天前
Android Framework 开发技巧:android.jar 生成与系统快速编译验证
android
如此风景1 天前
Kotlin Flow操作符学习
android·kotlin