18 changed files with 1331 additions and 219 deletions
After Width: | Height: | Size: 2.4 KiB |
@ -0,0 +1,284 @@ |
|||
package com.whitefish.ring.ui.chart |
|||
|
|||
import androidx.compose.foundation.background |
|||
import androidx.compose.foundation.layout.* |
|||
import androidx.compose.foundation.shape.RoundedCornerShape |
|||
import androidx.compose.material3.Card |
|||
import androidx.compose.material3.Text |
|||
import androidx.compose.runtime.* |
|||
import androidx.compose.ui.Alignment |
|||
import androidx.compose.ui.Modifier |
|||
import androidx.compose.ui.draw.clip |
|||
import androidx.compose.ui.graphics.* |
|||
import androidx.compose.ui.graphics.drawscope.DrawScope |
|||
import androidx.compose.ui.graphics.drawscope.Stroke |
|||
import androidx.compose.ui.text.font.FontWeight |
|||
import androidx.compose.ui.unit.dp |
|||
import androidx.compose.ui.unit.sp |
|||
import androidx.compose.foundation.Canvas |
|||
import androidx.compose.ui.geometry.Offset |
|||
import kotlin.math.* |
|||
|
|||
@Composable |
|||
fun RecoveryChart( |
|||
modifier: Modifier = Modifier, |
|||
currentHour: Float = 18f, |
|||
recoveryData: List<Float> = generateSampleRecoveryData() |
|||
) { |
|||
Card( |
|||
modifier = modifier |
|||
.fillMaxWidth() |
|||
.clip(RoundedCornerShape(24.dp)), |
|||
shape = RoundedCornerShape(24.dp) |
|||
) { |
|||
Box( |
|||
modifier = Modifier |
|||
.background( |
|||
Brush.radialGradient( |
|||
colors = listOf( |
|||
Color(0xFF8B7ED8), |
|||
Color(0xFF6B5B95) |
|||
), |
|||
radius = 800f |
|||
) |
|||
) |
|||
.padding(24.dp) |
|||
) { |
|||
Column { |
|||
// 标题和箭头 |
|||
Row( |
|||
modifier = Modifier.fillMaxWidth(), |
|||
horizontalArrangement = Arrangement.SpaceBetween, |
|||
verticalAlignment = Alignment.CenterVertically |
|||
) { |
|||
Text( |
|||
text = "恢复曲线", |
|||
color = Color.White, |
|||
fontSize = 18.sp, |
|||
fontWeight = FontWeight.Medium |
|||
) |
|||
Text( |
|||
text = "→", |
|||
color = Color.White.copy(alpha = 0.8f), |
|||
fontSize = 24.sp |
|||
) |
|||
} |
|||
|
|||
Spacer(modifier = Modifier.height(24.dp)) |
|||
|
|||
// 图表区域 |
|||
Canvas( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.height(120.dp) |
|||
) { |
|||
drawRecoveryChart( |
|||
size = size, |
|||
recoveryData = recoveryData, |
|||
currentHour = currentHour |
|||
) |
|||
} |
|||
|
|||
Spacer(modifier = Modifier.height(24.dp)) |
|||
|
|||
// 建议文本 |
|||
Box( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.background( |
|||
Color.White.copy(alpha = 0.15f), |
|||
RoundedCornerShape(16.dp) |
|||
) |
|||
.padding(16.dp) |
|||
) { |
|||
Text( |
|||
text = "你当天的身体状态似乎不太好,请注意保持适量的运动,晚上早点休息,养成良好的锻炼和睡眠规律。", |
|||
color = Color.White.copy(alpha = 0.9f), |
|||
fontSize = 14.sp, |
|||
lineHeight = 20.sp |
|||
) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private fun DrawScope.drawRecoveryChart( |
|||
size: androidx.compose.ui.geometry.Size, |
|||
recoveryData: List<Float>, |
|||
currentHour: Float |
|||
) { |
|||
val padding = 40f |
|||
val chartWidth = size.width - padding * 2 |
|||
val chartHeight = size.height - padding * 2 |
|||
val chartBottom = size.height - padding |
|||
|
|||
// 绘制水平虚线 |
|||
val midLineY = chartBottom - chartHeight / 2 |
|||
drawDashedLine( |
|||
start = Offset(padding, midLineY), |
|||
end = Offset(size.width - padding, midLineY), |
|||
color = Color.White.copy(alpha = 0.4f) |
|||
) |
|||
|
|||
// 绘制时间标签 |
|||
val timeLabels = listOf("00:00", "6:00", "12:00", "18:00", "24:00") |
|||
timeLabels.forEachIndexed { index, label -> |
|||
val x = padding + (chartWidth / (timeLabels.size - 1)) * index |
|||
drawContext.canvas.nativeCanvas.apply { |
|||
// 注意:这里需要平台特定的实现来绘制文本 |
|||
// 在实际实现中,你可能需要使用TextPaint或其他方式 |
|||
} |
|||
} |
|||
|
|||
// 准备路径数据 |
|||
val points = recoveryData.mapIndexed { index, value -> |
|||
val x = padding + (chartWidth / (recoveryData.size - 1)) * index |
|||
val y = chartBottom - (value * chartHeight) |
|||
Offset(x, y) |
|||
} |
|||
|
|||
// 找到当前时间对应的点索引 |
|||
val currentIndex = (currentHour * recoveryData.size / 24f).toInt() |
|||
.coerceIn(0, recoveryData.size - 1) |
|||
|
|||
// 绘制主要恢复曲线(渐变色) |
|||
if (points.isNotEmpty()) { |
|||
val gradientBrush = Brush.linearGradient( |
|||
colors = listOf( |
|||
Color(0xFF4CAF50), // 绿色 |
|||
Color(0xFFFFEB3B), // 黄色 |
|||
Color(0xFFFF9800) // 橙色 |
|||
), |
|||
start = Offset(points.first().x, 0f), |
|||
end = Offset(points[currentIndex].x, 0f) |
|||
) |
|||
|
|||
// 绘制实际曲线(到当前时间) |
|||
drawSmoothCurve( |
|||
points = points.take(currentIndex + 1), |
|||
brush = gradientBrush, |
|||
strokeWidth = 4.dp.toPx() |
|||
) |
|||
|
|||
// 绘制预测曲线(白色虚线) |
|||
if (currentIndex < points.size - 1) { |
|||
drawSmoothCurve( |
|||
points = points.drop(currentIndex), |
|||
color = Color.White.copy(alpha = 0.8f), |
|||
strokeWidth = 3.dp.toPx(), |
|||
isDashed = true |
|||
) |
|||
} |
|||
|
|||
// 绘制当前位置的白色亮点 |
|||
drawCircle( |
|||
color = Color.White, |
|||
radius = 8.dp.toPx(), |
|||
center = points[currentIndex] |
|||
) |
|||
drawCircle( |
|||
color = Color.White.copy(alpha = 0.3f), |
|||
radius = 12.dp.toPx(), |
|||
center = points[currentIndex] |
|||
) |
|||
} |
|||
} |
|||
|
|||
private fun DrawScope.drawDashedLine( |
|||
start: Offset, |
|||
end: Offset, |
|||
color: Color, |
|||
dashWidth: Float = 8f, |
|||
gapWidth: Float = 8f |
|||
) { |
|||
val totalDistance = (end - start).getDistance() |
|||
val dashCount = (totalDistance / (dashWidth + gapWidth)).toInt() |
|||
|
|||
repeat(dashCount) { index -> |
|||
val startRatio = index * (dashWidth + gapWidth) / totalDistance |
|||
val endRatio = (index * (dashWidth + gapWidth) + dashWidth) / totalDistance |
|||
|
|||
if (endRatio <= 1f) { |
|||
val dashStart = start + (end - start) * startRatio |
|||
val dashEnd = start + (end - start) * endRatio.coerceAtMost(1f) |
|||
|
|||
drawLine( |
|||
color = color, |
|||
start = dashStart, |
|||
end = dashEnd, |
|||
strokeWidth = 2.dp.toPx() |
|||
) |
|||
} |
|||
} |
|||
} |
|||
|
|||
private fun DrawScope.drawSmoothCurve( |
|||
points: List<Offset>, |
|||
brush: Brush? = null, |
|||
color: Color = Color.Transparent, |
|||
strokeWidth: Float = 4f, |
|||
isDashed: Boolean = false |
|||
) { |
|||
if (points.size < 2) return |
|||
|
|||
val path = Path() |
|||
path.moveTo(points.first().x, points.first().y) |
|||
|
|||
// 使用二次贝塞尔曲线创建平滑路径 |
|||
for (i in 1 until points.size) { |
|||
val currentPoint = points[i] |
|||
val previousPoint = points[i - 1] |
|||
|
|||
val midPointX = (previousPoint.x + currentPoint.x) / 2 |
|||
val midPointY = (previousPoint.y + currentPoint.y) / 2 |
|||
|
|||
if (i == 1) { |
|||
path.lineTo(midPointX, midPointY) |
|||
} else { |
|||
path.quadraticTo(previousPoint.x, previousPoint.y, midPointX, midPointY) |
|||
} |
|||
|
|||
if (i == points.size - 1) { |
|||
path.lineTo(currentPoint.x, currentPoint.y) |
|||
} |
|||
} |
|||
|
|||
val pathEffect = if (isDashed) { |
|||
PathEffect.dashPathEffect(floatArrayOf(12f, 8f)) |
|||
} else null |
|||
|
|||
if (brush != null) { |
|||
drawPath( |
|||
path = path, |
|||
brush = brush, |
|||
style = Stroke( |
|||
width = strokeWidth, |
|||
pathEffect = pathEffect |
|||
) |
|||
) |
|||
} else { |
|||
drawPath( |
|||
path = path, |
|||
color = color, |
|||
style = Stroke( |
|||
width = strokeWidth, |
|||
pathEffect = pathEffect |
|||
) |
|||
) |
|||
} |
|||
} |
|||
|
|||
// 生成示例恢复数据 |
|||
private fun generateSampleRecoveryData(): List<Float> { |
|||
// 24小时的恢复数据点,模拟一天的身体状态变化 |
|||
return (0..47).map { hour -> |
|||
val timeInHours = hour * 0.5f |
|||
when { |
|||
timeInHours < 6f -> 0.2f + timeInHours * 0.05f // 夜间恢复 |
|||
timeInHours < 12f -> 0.5f + sin((timeInHours - 6f) * PI / 6f).toFloat() * 0.3f // 上午活跃 |
|||
timeInHours < 18f -> 0.8f - (timeInHours - 12f) * 0.05f // 下午下降 |
|||
else -> 0.5f - sin((timeInHours - 18f) * PI / 6f).toFloat() * 0.2f // 晚间 |
|||
}.coerceIn(0f, 1f) |
|||
} |
|||
} |
@ -0,0 +1,190 @@ |
|||
package com.whitefish.ring.ui.chart |
|||
|
|||
import androidx.compose.foundation.layout.* |
|||
import androidx.compose.foundation.rememberScrollState |
|||
import androidx.compose.foundation.verticalScroll |
|||
import androidx.compose.material3.* |
|||
import androidx.compose.runtime.* |
|||
import androidx.compose.ui.Alignment |
|||
import androidx.compose.ui.Modifier |
|||
import androidx.compose.ui.graphics.Color |
|||
import androidx.compose.ui.text.font.FontWeight |
|||
import androidx.compose.ui.unit.dp |
|||
import androidx.compose.ui.unit.sp |
|||
// import kotlinx.datetime.* |
|||
|
|||
@OptIn(ExperimentalMaterial3Api::class) |
|||
@Composable |
|||
fun RecoveryChartDemo( |
|||
modifier: Modifier = Modifier |
|||
) { |
|||
var currentHour by remember { mutableFloatStateOf(18f) } |
|||
|
|||
Column( |
|||
modifier = modifier |
|||
.fillMaxSize() |
|||
.verticalScroll(rememberScrollState()) |
|||
.padding(16.dp), |
|||
verticalArrangement = Arrangement.spacedBy(16.dp) |
|||
) { |
|||
Text( |
|||
text = "恢复曲线图表演示", |
|||
fontSize = 24.sp, |
|||
fontWeight = FontWeight.Bold, |
|||
modifier = Modifier.align(Alignment.CenterHorizontally) |
|||
) |
|||
|
|||
// 时间控制器 |
|||
Card( |
|||
modifier = Modifier.fillMaxWidth() |
|||
) { |
|||
Column( |
|||
modifier = Modifier.padding(16.dp), |
|||
verticalArrangement = Arrangement.spacedBy(12.dp) |
|||
) { |
|||
Text( |
|||
text = "当前时间: ${formatTime(currentHour)}", |
|||
fontSize = 16.sp, |
|||
fontWeight = FontWeight.Medium |
|||
) |
|||
|
|||
Slider( |
|||
value = currentHour, |
|||
onValueChange = { currentHour = it }, |
|||
valueRange = 0f..24f, |
|||
steps = 47 // 24小时 * 2 - 1 (每30分钟一步) |
|||
) |
|||
|
|||
Row( |
|||
modifier = Modifier.fillMaxWidth(), |
|||
horizontalArrangement = Arrangement.SpaceBetween |
|||
) { |
|||
Text("00:00", fontSize = 12.sp, color = Color.Gray) |
|||
Text("06:00", fontSize = 12.sp, color = Color.Gray) |
|||
Text("12:00", fontSize = 12.sp, color = Color.Gray) |
|||
Text("18:00", fontSize = 12.sp, color = Color.Gray) |
|||
Text("24:00", fontSize = 12.sp, color = Color.Gray) |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 第一个图表 - 基础版本 |
|||
Text( |
|||
text = "基础恢复曲线", |
|||
fontSize = 18.sp, |
|||
fontWeight = FontWeight.Medium |
|||
) |
|||
|
|||
RecoveryChart( |
|||
modifier = Modifier.fillMaxWidth(), |
|||
currentHour = currentHour |
|||
) |
|||
|
|||
// 第二个图表 - 带时间标签版本 |
|||
Text( |
|||
text = "带时间标签的恢复曲线", |
|||
fontSize = 18.sp, |
|||
fontWeight = FontWeight.Medium |
|||
) |
|||
|
|||
RecoveryChartWithTimeLabels( |
|||
modifier = Modifier.fillMaxWidth(), |
|||
actualData = arrayListOf(1.0f, 0.8f, 0.6f, 0.4f, 0.2f, 0.1f,1.0f, 0.8f, 0.6f, 0.4f, 0.2f, 0.1f,1.0f, 0.8f, 0.6f, 0.4f, 0.2f, 0.1f,1.0f, 0.8f, 0.6f, 0.4f, 0.2f, 0.1f), |
|||
predictedData = arrayListOf() |
|||
) |
|||
|
|||
// 功能说明 |
|||
Card( |
|||
modifier = Modifier.fillMaxWidth(), |
|||
colors = CardDefaults.cardColors( |
|||
containerColor = Color(0xFFF5F5F5) |
|||
) |
|||
) { |
|||
Column( |
|||
modifier = Modifier.padding(16.dp), |
|||
verticalArrangement = Arrangement.spacedBy(8.dp) |
|||
) { |
|||
Text( |
|||
text = "图表特性说明", |
|||
fontSize = 16.sp, |
|||
fontWeight = FontWeight.Medium |
|||
) |
|||
|
|||
val features = listOf( |
|||
"🌈 渐变色线条:从绿色到橙色的动态渐变", |
|||
"⭐ 白色亮点:标记当前时间位置,带光晕效果", |
|||
"📊 预测线段:白色线条显示未来预测数据", |
|||
"⏰ 时间刻度:准确的24小时时间标记", |
|||
"🎨 紫色背景:符合设计稿的渐变背景", |
|||
"📱 响应式设计:适配不同屏幕尺寸" |
|||
) |
|||
|
|||
features.forEach { feature -> |
|||
Text( |
|||
text = feature, |
|||
fontSize = 14.sp, |
|||
color = Color.DarkGray |
|||
) |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 数据说明 |
|||
Card( |
|||
modifier = Modifier.fillMaxWidth(), |
|||
colors = CardDefaults.cardColors( |
|||
containerColor = Color(0xFFE3F2FD) |
|||
) |
|||
) { |
|||
Column( |
|||
modifier = Modifier.padding(16.dp), |
|||
verticalArrangement = Arrangement.spacedBy(8.dp) |
|||
) { |
|||
Text( |
|||
text = "数据模型说明", |
|||
fontSize = 16.sp, |
|||
fontWeight = FontWeight.Medium |
|||
) |
|||
|
|||
Text( |
|||
text = "• 夜间 (00:00-06:00): 低水平,逐渐恢复\n" + |
|||
"• 上午 (06:00-12:00): 快速上升至峰值\n" + |
|||
"• 下午 (12:00-18:00): 从峰值逐渐下降\n" + |
|||
"• 晚上 (18:00-24:00): 持续下降至夜间水平", |
|||
fontSize = 14.sp, |
|||
color = Color.DarkGray |
|||
) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private fun formatTime(hour: Float): String { |
|||
val hours = hour.toInt() |
|||
val minutes = ((hour - hours) * 60).toInt() |
|||
return "${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}" |
|||
} |
|||
|
|||
// 根据当前时间生成演示的实际数据 |
|||
private fun generateDemoActualData(currentHour: Int): List<Float> { |
|||
val actualHours = currentHour.coerceIn(1, 23) // 至少1小时,最多23小时 |
|||
return (0 until actualHours).map { hour -> |
|||
when { |
|||
hour < 6 -> 0.3f + hour * 0.05f // 夜间缓慢上升 |
|||
hour < 12 -> 0.6f + (hour - 6) * 0.05f // 上午继续上升 |
|||
else -> 0.9f - (hour - 12) * 0.03f // 下午缓慢下降 |
|||
}.coerceIn(0f, 1f) |
|||
} |
|||
} |
|||
|
|||
// 根据当前时间生成演示的预测数据 |
|||
private fun generateDemoPredictedData(currentHour: Int): List<Float> { |
|||
val startHour = currentHour.coerceIn(1, 23) |
|||
return (startHour until 24).map { hour -> |
|||
when { |
|||
hour < 18 -> 0.8f - (hour - startHour) * 0.02f // 继续下降 |
|||
hour < 22 -> 0.6f - (hour - 18) * 0.05f // 傍晚下降 |
|||
else -> 0.4f - (hour - 22) * 0.05f // 夜间低水平 |
|||
}.coerceIn(0f, 1f) |
|||
} |
|||
} |
@ -0,0 +1,432 @@ |
|||
package com.whitefish.ring.ui.chart |
|||
|
|||
import androidx.compose.foundation.background |
|||
import androidx.compose.foundation.layout.* |
|||
import androidx.compose.foundation.shape.RoundedCornerShape |
|||
import androidx.compose.material3.Card |
|||
import androidx.compose.material3.Text |
|||
import androidx.compose.runtime.* |
|||
import androidx.compose.ui.Alignment |
|||
import androidx.compose.ui.Modifier |
|||
import androidx.compose.ui.draw.clip |
|||
import androidx.compose.ui.graphics.* |
|||
import androidx.compose.ui.graphics.drawscope.DrawScope |
|||
import androidx.compose.ui.graphics.drawscope.Stroke |
|||
import androidx.compose.ui.text.font.FontWeight |
|||
import androidx.compose.ui.unit.dp |
|||
import androidx.compose.ui.unit.sp |
|||
import androidx.compose.foundation.Canvas |
|||
import androidx.compose.foundation.Image |
|||
import androidx.compose.ui.geometry.Offset |
|||
import androidx.compose.ui.layout.ContentScale |
|||
import androidx.compose.ui.text.TextMeasurer |
|||
import androidx.compose.ui.text.TextStyle |
|||
import androidx.compose.ui.text.drawText |
|||
import androidx.compose.ui.text.rememberTextMeasurer |
|||
import org.jetbrains.compose.resources.painterResource |
|||
import ring.shared.generated.resources.Res |
|||
import ring.shared.generated.resources.bg_recovery_chart |
|||
import ring.shared.generated.resources.bg_recovery_score |
|||
import kotlin.math.* |
|||
|
|||
@Composable |
|||
fun RecoveryChartWithTimeLabels( |
|||
modifier: Modifier = Modifier, |
|||
actualData: List<Float> = generateSampleActualData(), // 实际数据(彩色曲线) |
|||
predictedData: List<Float> = generateSamplePredictedData() // 预测数据(白色曲线) |
|||
) { |
|||
val textMeasurer = rememberTextMeasurer() |
|||
|
|||
Card( |
|||
modifier = modifier |
|||
.clip(RoundedCornerShape(24.dp)), |
|||
shape = RoundedCornerShape(24.dp) |
|||
) { |
|||
|
|||
Box(modifier = Modifier.wrapContentSize()) { |
|||
Image( |
|||
painterResource(Res.drawable.bg_recovery_chart), |
|||
contentDescription = null, |
|||
modifier = Modifier.fillMaxSize(), |
|||
contentScale = ContentScale.Crop |
|||
) |
|||
Box( |
|||
modifier = Modifier.padding(horizontal = 24.dp) |
|||
) { |
|||
Column { |
|||
// 标题和箭头 |
|||
Row( |
|||
modifier = Modifier.fillMaxWidth().padding(top = 15.dp), |
|||
horizontalArrangement = Arrangement.SpaceBetween, |
|||
verticalAlignment = Alignment.CenterVertically |
|||
) { |
|||
Text( |
|||
text = "恢复曲线", |
|||
color = Color.White, |
|||
fontSize = 20.sp, |
|||
fontWeight = FontWeight.Medium |
|||
) |
|||
Text( |
|||
text = "→", |
|||
color = Color.White.copy(alpha = 0.8f), |
|||
fontSize = 24.sp |
|||
) |
|||
} |
|||
|
|||
Spacer(modifier = Modifier.height(24.dp)) |
|||
|
|||
// 图表区域 |
|||
Canvas( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.height(83.dp) |
|||
) { |
|||
drawRecoveryChartWithLabels( |
|||
size = size, |
|||
actualData = actualData, |
|||
predictedData = predictedData, |
|||
textMeasurer = textMeasurer |
|||
) |
|||
} |
|||
|
|||
Spacer(modifier = Modifier.height(16.dp)) |
|||
|
|||
} |
|||
|
|||
|
|||
} |
|||
|
|||
Box(modifier = Modifier.padding(start = 24.dp, end = 24.dp, bottom = 13.dp).align(Alignment.BottomCenter)) { |
|||
// 建议文本 |
|||
Row( |
|||
modifier = Modifier |
|||
.fillMaxWidth() |
|||
.height(80.dp) |
|||
.background( |
|||
Color.White.copy(alpha = 0.15f), |
|||
RoundedCornerShape(16.dp) |
|||
) |
|||
) { |
|||
Text( |
|||
text = "你当天的身体状态似乎不太好,请注意保持适量的运动,晚上早点休息,养成良好的锻炼和睡眠规律。", |
|||
color = Color.White.copy(alpha = 0.9f), |
|||
fontSize = 14.sp, |
|||
lineHeight = 20.sp, |
|||
modifier = Modifier.padding(horizontal = 14.dp, vertical = 16.dp) |
|||
) |
|||
} |
|||
|
|||
} |
|||
} |
|||
|
|||
|
|||
} |
|||
} |
|||
|
|||
private fun DrawScope.drawRecoveryChartWithLabels( |
|||
size: androidx.compose.ui.geometry.Size, |
|||
actualData: List<Float>, |
|||
predictedData: List<Float>, |
|||
textMeasurer: TextMeasurer |
|||
) { |
|||
val padding = 24f |
|||
val bottomPadding = 30f // 为时间标签留出更多空间 |
|||
val chartWidth = size.width - padding * 2 |
|||
val chartHeight = size.height - padding - bottomPadding |
|||
val chartBottom = size.height - bottomPadding |
|||
|
|||
// 绘制水平虚线 |
|||
val midLineY = chartBottom - chartHeight / 2 |
|||
drawDashedLine( |
|||
start = Offset(padding, midLineY), |
|||
end = Offset(size.width - padding, midLineY), |
|||
color = Color.White.copy(alpha = 0.4f) |
|||
) |
|||
|
|||
// 合并两组数据(实际数据 + 预测数据) |
|||
val allData = actualData + predictedData |
|||
val actualDataSize = actualData.size |
|||
|
|||
// 确保总共24个数据点(每小时一个点) |
|||
val totalExpectedPoints = 24 |
|||
val combinedData = if (allData.size == totalExpectedPoints) { |
|||
allData |
|||
} else { |
|||
// 如果数据点不是24个,进行插值或截取 |
|||
generateInterpolatedData(allData, totalExpectedPoints) |
|||
} |
|||
|
|||
// 准备路径数据 |
|||
val points = combinedData.mapIndexed { index, value -> |
|||
val x = padding + (chartWidth / (combinedData.size - 1)) * index |
|||
val y = chartBottom - (value * chartHeight) |
|||
Offset(x, y) |
|||
} |
|||
|
|||
// 绘制垂直虚线和时间标签 |
|||
val timeLabels = listOf("00:00", "6:00", "12:00", "18:00", "24:00") |
|||
val timeHours = listOf(0f, 6f, 12f, 18f, 24f) |
|||
val timeTextStyle = TextStyle( |
|||
color = Color.White.copy(alpha = 0.7f), |
|||
fontSize = 12.sp |
|||
) |
|||
|
|||
timeLabels.forEachIndexed { index, label -> |
|||
// 计算对应时间点的曲线Y坐标 |
|||
val timeHour = timeHours[index] |
|||
val dataIndex = (timeHour * combinedData.size / 24f).toInt() |
|||
.coerceIn(0, combinedData.size - 1) |
|||
|
|||
// 使用与曲线数据点相同的x坐标计算方式 |
|||
val x = if (points.isNotEmpty()) { |
|||
points[dataIndex].x |
|||
} else { |
|||
padding + (chartWidth / (timeLabels.size - 1)) * index |
|||
} |
|||
|
|||
val curveY = if (points.isNotEmpty()) { |
|||
points[dataIndex].y |
|||
} else { |
|||
chartBottom - chartHeight * 0.5f // 默认中间位置 |
|||
} |
|||
|
|||
// 绘制垂直虚线(从曲线位置到时间轴) |
|||
drawDashedLine( |
|||
start = Offset(x, curveY), |
|||
end = Offset(x, chartBottom), |
|||
color = Color.White.copy(alpha = 0.2f), |
|||
dashWidth = 4f, |
|||
gapWidth = 6f |
|||
) |
|||
|
|||
// 绘制时间标签 |
|||
val textLayoutResult = textMeasurer.measure(label, timeTextStyle) |
|||
drawText( |
|||
textLayoutResult = textLayoutResult, |
|||
topLeft = Offset( |
|||
x - textLayoutResult.size.width / 2, |
|||
chartBottom + 12.dp.toPx() |
|||
) |
|||
) |
|||
} |
|||
|
|||
// 绘制曲线 |
|||
if (points.isNotEmpty()) { |
|||
// 分离实际数据点和预测数据点 |
|||
val actualPoints = points.take(actualDataSize) |
|||
val predictedPoints = if (actualDataSize < points.size) { |
|||
// 从实际数据的最后一个点开始,包含预测数据 |
|||
points.drop(actualDataSize - 1) |
|||
} else { |
|||
emptyList() |
|||
} |
|||
|
|||
// 绘制实际数据曲线(彩色渐变) |
|||
if (actualPoints.isNotEmpty()) { |
|||
val gradientColors = listOf( |
|||
Color(0xFF00C851), // 亮绿色 |
|||
Color(0xFF66BB6A), // 中绿色 |
|||
Color(0xFFFFEB3B), // 黄色 |
|||
Color(0xFFFF9800), // 橙色 |
|||
Color(0xFFFF5722) // 深橙色 |
|||
) |
|||
|
|||
val gradientBrush = Brush.linearGradient( |
|||
colors = gradientColors, |
|||
start = Offset(actualPoints.first().x, 0f), |
|||
end = Offset(actualPoints.last().x, 0f) |
|||
) |
|||
|
|||
drawSmoothCurve( |
|||
points = actualPoints, |
|||
brush = gradientBrush, |
|||
strokeWidth = 4.dp.toPx() |
|||
) |
|||
} |
|||
|
|||
// 绘制预测数据曲线(白色) |
|||
if (predictedPoints.isNotEmpty()) { |
|||
drawSmoothCurve( |
|||
points = predictedPoints, |
|||
color = Color.White.copy(alpha = 0.9f), |
|||
strokeWidth = 3.dp.toPx() |
|||
) |
|||
} |
|||
|
|||
// 绘制分界点的白色亮点(在实际数据的最后一个点) |
|||
if (actualPoints.isNotEmpty()) { |
|||
val dividerPoint = actualPoints.last() |
|||
|
|||
// 外层光晕 |
|||
drawCircle( |
|||
color = Color.White.copy(alpha = 0.3f), |
|||
radius = 16.dp.toPx(), |
|||
center = dividerPoint |
|||
) |
|||
// 中层光晕 |
|||
drawCircle( |
|||
color = Color.White.copy(alpha = 0.6f), |
|||
radius = 10.dp.toPx(), |
|||
center = dividerPoint |
|||
) |
|||
// 核心亮点 |
|||
drawCircle( |
|||
color = Color.White, |
|||
radius = 6.dp.toPx(), |
|||
center = dividerPoint |
|||
) |
|||
} |
|||
} |
|||
} |
|||
|
|||
private fun DrawScope.drawDashedLine( |
|||
start: Offset, |
|||
end: Offset, |
|||
color: Color, |
|||
dashWidth: Float = 8f, |
|||
gapWidth: Float = 8f |
|||
) { |
|||
val totalDistance = (end - start).getDistance() |
|||
val dashCount = (totalDistance / (dashWidth + gapWidth)).toInt() |
|||
|
|||
repeat(dashCount) { index -> |
|||
val startRatio = index * (dashWidth + gapWidth) / totalDistance |
|||
val endRatio = (index * (dashWidth + gapWidth) + dashWidth) / totalDistance |
|||
|
|||
if (endRatio <= 1f) { |
|||
val dashStart = start + (end - start) * startRatio |
|||
val dashEnd = start + (end - start) * endRatio.coerceAtMost(1f) |
|||
|
|||
drawLine( |
|||
color = color, |
|||
start = dashStart, |
|||
end = dashEnd, |
|||
strokeWidth = 1.dp.toPx() |
|||
) |
|||
} |
|||
} |
|||
} |
|||
|
|||
private fun DrawScope.drawSmoothCurve( |
|||
points: List<Offset>, |
|||
brush: Brush? = null, |
|||
color: Color = Color.Transparent, |
|||
strokeWidth: Float = 4f, |
|||
isDashed: Boolean = false |
|||
) { |
|||
if (points.size < 2) return |
|||
|
|||
val path = Path() |
|||
path.moveTo(points.first().x, points.first().y) |
|||
|
|||
// 使用三次贝塞尔曲线创建更平滑的路径 |
|||
for (i in 1 until points.size) { |
|||
val currentPoint = points[i] |
|||
val previousPoint = points[i - 1] |
|||
|
|||
// 计算控制点来创建平滑曲线 |
|||
val controlPointDistance = (currentPoint.x - previousPoint.x) * 0.3f |
|||
|
|||
val control1 = Offset( |
|||
previousPoint.x + controlPointDistance, |
|||
previousPoint.y |
|||
) |
|||
val control2 = Offset( |
|||
currentPoint.x - controlPointDistance, |
|||
currentPoint.y |
|||
) |
|||
|
|||
path.cubicTo( |
|||
control1.x, control1.y, |
|||
control2.x, control2.y, |
|||
currentPoint.x, currentPoint.y |
|||
) |
|||
} |
|||
|
|||
val pathEffect = if (isDashed) { |
|||
PathEffect.dashPathEffect(floatArrayOf(12f, 8f)) |
|||
} else null |
|||
|
|||
if (brush != null) { |
|||
drawPath( |
|||
path = path, |
|||
brush = brush, |
|||
style = Stroke( |
|||
width = strokeWidth, |
|||
pathEffect = pathEffect, |
|||
cap = StrokeCap.Round, |
|||
join = StrokeJoin.Round |
|||
) |
|||
) |
|||
} else { |
|||
drawPath( |
|||
path = path, |
|||
color = color, |
|||
style = Stroke( |
|||
width = strokeWidth, |
|||
pathEffect = pathEffect, |
|||
cap = StrokeCap.Round, |
|||
join = StrokeJoin.Round |
|||
) |
|||
) |
|||
} |
|||
} |
|||
|
|||
// 生成插值数据 |
|||
private fun generateInterpolatedData(data: List<Float>, targetSize: Int): List<Float> { |
|||
if (data.isEmpty()) return List(targetSize) { 0.5f } |
|||
if (data.size == targetSize) return data |
|||
|
|||
return (0 until targetSize).map { index -> |
|||
val ratio = index.toFloat() / (targetSize - 1) |
|||
val sourceIndex = ratio * (data.size - 1) |
|||
val lowerIndex = sourceIndex.toInt().coerceIn(0, data.size - 1) |
|||
val upperIndex = (lowerIndex + 1).coerceIn(0, data.size - 1) |
|||
val fraction = sourceIndex - lowerIndex |
|||
|
|||
// 线性插值 |
|||
data[lowerIndex] * (1 - fraction) + data[upperIndex] * fraction |
|||
} |
|||
} |
|||
|
|||
// 生成示例实际数据(0-18小时,共18个点) |
|||
private fun generateSampleActualData(): List<Float> { |
|||
return (0..17).map { hour -> |
|||
val timeInHours = hour.toFloat() |
|||
|
|||
val baseValue = when { |
|||
timeInHours < 6f -> { |
|||
// 夜间:低水平,逐渐恢复 |
|||
0.2f + (timeInHours / 6f) * 0.3f |
|||
} |
|||
|
|||
timeInHours < 12f -> { |
|||
// 上午:快速上升 |
|||
0.5f + ((timeInHours - 6f) / 6f) * 0.4f |
|||
} |
|||
|
|||
else -> { |
|||
// 下午:逐渐下降 |
|||
0.9f - ((timeInHours - 12f) / 6f) * 0.3f |
|||
} |
|||
} |
|||
|
|||
// 添加轻微波动 |
|||
val noise = sin(timeInHours * 0.5f).toFloat() * 0.05f |
|||
(baseValue + noise).coerceIn(0f, 1f) |
|||
} |
|||
} |
|||
|
|||
// 生成示例预测数据(18-24小时,共6个点) |
|||
private fun generateSamplePredictedData(): List<Float> { |
|||
return (18..23).map { hour -> |
|||
val timeInHours = hour.toFloat() |
|||
|
|||
// 傍晚到夜间的预测:继续下降 |
|||
val baseValue = 0.6f - ((timeInHours - 18f) / 6f) * 0.4f |
|||
|
|||
// 添加轻微波动 |
|||
val noise = cos(timeInHours * 0.3f).toFloat() * 0.03f |
|||
(baseValue + noise).coerceIn(0f, 1f) |
|||
} |
|||
} |
@ -0,0 +1,166 @@ |
|||
package com.whitefish.ring.ui.components |
|||
|
|||
import androidx.compose.foundation.Canvas |
|||
import androidx.compose.foundation.layout.* |
|||
import androidx.compose.foundation.background |
|||
import androidx.compose.foundation.border |
|||
import androidx.compose.foundation.shape.CircleShape |
|||
import androidx.compose.material3.* |
|||
import androidx.compose.runtime.Composable |
|||
import androidx.compose.ui.Alignment |
|||
import androidx.compose.ui.Modifier |
|||
import androidx.compose.ui.graphics.Color |
|||
import androidx.compose.ui.graphics.StrokeCap |
|||
import androidx.compose.ui.graphics.drawscope.Stroke |
|||
import androidx.compose.ui.text.font.FontWeight |
|||
import androidx.compose.ui.text.style.TextAlign |
|||
import androidx.compose.ui.unit.dp |
|||
import androidx.compose.ui.unit.sp |
|||
import kotlin.math.cos |
|||
import kotlin.math.sin |
|||
|
|||
/** |
|||
* 圆弧进度卡片组件 |
|||
* @param currentMinutes 当前分钟数 |
|||
* @param totalMinutes 总分钟数 |
|||
* @param completionTime 完成时间描述 |
|||
* @param modifier 修饰符 |
|||
*/ |
|||
@Composable |
|||
fun CircularProgressCard( |
|||
currentMinutes: Int, |
|||
totalMinutes: Int, |
|||
completionTime: String, |
|||
modifier: Modifier = Modifier |
|||
) { |
|||
Column( |
|||
modifier = modifier |
|||
.fillMaxWidth() |
|||
.padding(horizontal = 24.dp, vertical = 16.dp), |
|||
horizontalAlignment = Alignment.CenterHorizontally |
|||
) { |
|||
Box( |
|||
contentAlignment = Alignment.Center |
|||
) { |
|||
// 圆弧进度条 |
|||
CircularProgressArc( |
|||
progress = currentMinutes.toFloat() / totalMinutes.toFloat(), |
|||
modifier = Modifier.width(195.dp).height(97.5.dp) |
|||
) |
|||
|
|||
// 中心文字 - 与圆弧底部对齐 |
|||
Column( |
|||
horizontalAlignment = Alignment.CenterHorizontally, |
|||
verticalArrangement = Arrangement.Center, |
|||
modifier = Modifier.offset(y = 35.dp) // 向下偏移,与圆弧底部对齐 |
|||
) { |
|||
Text( |
|||
text = currentMinutes.toString(), |
|||
style = MaterialTheme.typography.displayLarge.copy( |
|||
fontSize = 40.sp, |
|||
fontWeight = FontWeight.Bold |
|||
), |
|||
color = Color(0xFF352764) |
|||
) |
|||
} |
|||
} |
|||
|
|||
// 底部完成时间描述 - 紧贴圆弧下方 |
|||
Row( |
|||
modifier = Modifier.padding(top = 10.dp) |
|||
) { |
|||
Text( |
|||
text = completionTime, |
|||
style = MaterialTheme.typography.bodyMedium.copy( |
|||
fontSize = 14.sp, |
|||
fontWeight = FontWeight.Medium |
|||
), |
|||
color = Color(0xFF394298), |
|||
textAlign = TextAlign.Center |
|||
) |
|||
|
|||
Spacer(modifier = Modifier.width(8.dp)) |
|||
|
|||
// 问号图标 |
|||
Box( |
|||
modifier = Modifier |
|||
.size(16.dp) |
|||
.background( |
|||
color = Color.Transparent, |
|||
shape = CircleShape |
|||
) |
|||
.border( |
|||
width = 1.dp, |
|||
color = Color(0xFF9CA3AF), |
|||
shape = CircleShape |
|||
), |
|||
contentAlignment = Alignment.Center |
|||
) { |
|||
Text( |
|||
text = "?", |
|||
style = MaterialTheme.typography.bodySmall.copy( |
|||
fontSize = 10.sp, |
|||
fontWeight = FontWeight.Bold |
|||
), |
|||
color = Color(0xFF9CA3AF) |
|||
) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 圆弧进度组件 |
|||
* @param progress 进度值 (0.0 到 1.0) |
|||
* @param modifier 修饰符 |
|||
*/ |
|||
@Composable |
|||
private fun CircularProgressArc( |
|||
progress: Float, |
|||
modifier: Modifier = Modifier |
|||
) { |
|||
Canvas(modifier = modifier) { |
|||
|
|||
val strokeWidth = 12.dp.toPx() |
|||
val radius = (size.width - strokeWidth) / 2 |
|||
val center = androidx.compose.ui.geometry.Offset( |
|||
x = size.width / 2, |
|||
y = size.height - strokeWidth / 2 // 调整中心点,让半圆底部对齐 |
|||
) |
|||
|
|||
// 背景圆弧 (浅灰色) - 180度半圆 |
|||
drawArc( |
|||
color = Color(0xFFE5E7EB), |
|||
startAngle = 180f, // 从左侧开始 |
|||
sweepAngle = 180f, // 180度的半圆 |
|||
useCenter = false, |
|||
topLeft = androidx.compose.ui.geometry.Offset( |
|||
x = center.x - radius, |
|||
y = center.y - radius |
|||
), |
|||
size = androidx.compose.ui.geometry.Size(radius * 2, radius * 2), |
|||
style = Stroke( |
|||
width = strokeWidth, |
|||
cap = StrokeCap.Round |
|||
) |
|||
) |
|||
|
|||
// 进度圆弧 (紫色渐变) |
|||
val progressAngle = 180f * progress.coerceIn(0f, 1f) |
|||
drawArc( |
|||
color = Color(0xFF352764), |
|||
startAngle = 180f, |
|||
sweepAngle = progressAngle, |
|||
useCenter = false, |
|||
topLeft = androidx.compose.ui.geometry.Offset( |
|||
x = center.x - radius, |
|||
y = center.y - radius |
|||
), |
|||
size = androidx.compose.ui.geometry.Size(radius * 2, radius * 2), |
|||
style = Stroke( |
|||
width = strokeWidth, |
|||
cap = StrokeCap.Round |
|||
) |
|||
) |
|||
} |
|||
} |
@ -0,0 +1,74 @@ |
|||
package com.whitefish.ring.ui.components |
|||
|
|||
import androidx.compose.foundation.Canvas |
|||
import androidx.compose.foundation.layout.Arrangement |
|||
import androidx.compose.foundation.layout.Box |
|||
import androidx.compose.foundation.layout.Column |
|||
import androidx.compose.foundation.layout.padding |
|||
import androidx.compose.foundation.layout.size |
|||
import androidx.compose.material3.Text |
|||
import androidx.compose.runtime.Composable |
|||
import androidx.compose.ui.Alignment |
|||
import androidx.compose.ui.Modifier |
|||
import androidx.compose.ui.geometry.Offset |
|||
import androidx.compose.ui.geometry.Size |
|||
import androidx.compose.ui.graphics.Color |
|||
import androidx.compose.ui.graphics.StrokeCap |
|||
import androidx.compose.ui.graphics.drawscope.Stroke |
|||
import androidx.compose.ui.text.font.FontWeight |
|||
import androidx.compose.ui.unit.TextUnit |
|||
import androidx.compose.ui.unit.dp |
|||
import androidx.compose.ui.unit.sp |
|||
|
|||
@Composable |
|||
fun CircularScoreChart(score: Int,scoreText: String,title:String,scoreTextSize: TextUnit,bottomText: String = "") { |
|||
Box(contentAlignment = Alignment.BottomCenter) { |
|||
Canvas(modifier = Modifier.size(250.dp, 125.dp)) { |
|||
val strokeWidth = 12.dp.toPx() |
|||
val radius = (size.width - strokeWidth) / 2 |
|||
val center = Offset(size.width / 2, size.height) |
|||
|
|||
// 背景半圆弧 |
|||
drawArc( |
|||
color = Color(0xFFE0E0E0), |
|||
startAngle = 180f, |
|||
sweepAngle = 180f, |
|||
useCenter = false, |
|||
style = Stroke(strokeWidth, cap = StrokeCap.Round), |
|||
topLeft = Offset(center.x - radius, center.y - radius), |
|||
size = Size(radius * 2, radius * 2) |
|||
) |
|||
|
|||
// 进度半圆弧 |
|||
val sweepAngle = (score / 100f) * 180f |
|||
drawArc( |
|||
color = Color(0xFF5E35B1), |
|||
startAngle = 180f, |
|||
sweepAngle = sweepAngle, |
|||
useCenter = false, |
|||
style = Stroke(strokeWidth, cap = StrokeCap.Round), |
|||
topLeft = Offset(center.x - radius, center.y - radius), |
|||
size = Size(radius * 2, radius * 2) |
|||
) |
|||
} |
|||
|
|||
Column( |
|||
horizontalAlignment = Alignment.CenterHorizontally, |
|||
modifier = Modifier.padding(bottom = 8.dp) |
|||
) { |
|||
Text( |
|||
text = scoreText, |
|||
fontSize = scoreTextSize, |
|||
fontWeight = FontWeight.Bold, |
|||
color = Color(0xFF352764) |
|||
) |
|||
Text( |
|||
text = title, |
|||
fontSize = 14.sp, |
|||
color = Color(0xFF352764) |
|||
) |
|||
} |
|||
|
|||
|
|||
} |
|||
} |
@ -0,0 +1,108 @@ |
|||
package com.whitefish.ring.ui.components |
|||
|
|||
import androidx.compose.foundation.background |
|||
import androidx.compose.foundation.layout.* |
|||
import androidx.compose.foundation.shape.RoundedCornerShape |
|||
import androidx.compose.material3.* |
|||
import androidx.compose.runtime.Composable |
|||
import androidx.compose.ui.Alignment |
|||
import androidx.compose.ui.Modifier |
|||
import androidx.compose.ui.draw.clip |
|||
import androidx.compose.ui.graphics.Color |
|||
import androidx.compose.ui.text.font.FontWeight |
|||
import androidx.compose.ui.text.style.TextAlign |
|||
import androidx.compose.ui.unit.dp |
|||
import androidx.compose.ui.unit.sp |
|||
|
|||
/** |
|||
* 顶部导航栏组件 |
|||
* @param title 标题文字 |
|||
* @param month 月 |
|||
* @param day 总天数 |
|||
* @param onNavigateClick 右侧箭头点击事件 |
|||
* @param modifier 修饰符 |
|||
*/ |
|||
@OptIn(ExperimentalMaterial3Api::class) |
|||
@Composable |
|||
fun TopNavigationBar( |
|||
title: String, |
|||
month: Int, |
|||
day: Int, |
|||
onNavigateClick: () -> Unit = {}, |
|||
modifier: Modifier = Modifier |
|||
) { |
|||
Row( |
|||
modifier = modifier |
|||
.fillMaxWidth() |
|||
.padding(horizontal = 24.dp, vertical = 20.dp), |
|||
verticalAlignment = Alignment.CenterVertically, |
|||
horizontalArrangement = Arrangement.SpaceBetween |
|||
) { |
|||
// 左侧日期椭圆 |
|||
DateOval( |
|||
month = month, |
|||
day = day, |
|||
modifier = Modifier |
|||
.width(30.dp) |
|||
.height(53.dp) |
|||
) |
|||
|
|||
// 中间标题 |
|||
Text( |
|||
text = title, |
|||
style = MaterialTheme.typography.titleLarge.copy( |
|||
fontSize = 22.sp, |
|||
fontWeight = FontWeight.SemiBold |
|||
), |
|||
color = Color(0xFF394298), |
|||
modifier = Modifier |
|||
.weight(1f) |
|||
.padding(horizontal = 16.dp), |
|||
textAlign = TextAlign.Center |
|||
) |
|||
|
|||
// 右侧箭头 |
|||
IconButton( |
|||
onClick = onNavigateClick, |
|||
modifier = Modifier.size(48.dp) |
|||
) { |
|||
// Icon( |
|||
// imageVector = Icons.AutoMirrored.Filled.ArrowForward, |
|||
// contentDescription = "导航", |
|||
// tint = MaterialTheme.colorScheme.primary, |
|||
// modifier = Modifier.size(24.dp) |
|||
// ) |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 日期椭圆组件 |
|||
* @param currentDay 当前日期 |
|||
* @param totalDays 总天数 |
|||
* @param modifier 修饰符 |
|||
*/ |
|||
@Composable |
|||
private fun DateOval( |
|||
month: Int, |
|||
day: Int, |
|||
modifier: Modifier = Modifier |
|||
) { |
|||
Box( |
|||
modifier = modifier |
|||
.clip(RoundedCornerShape(24.dp)) |
|||
.background( |
|||
color = Color(0xFF2D2D3A) |
|||
), |
|||
contentAlignment = Alignment.Center |
|||
) { |
|||
Text( |
|||
text = "$month/\n$day", |
|||
style = MaterialTheme.typography.titleMedium.copy( |
|||
fontSize = 16.sp, |
|||
fontWeight = FontWeight.Medium |
|||
), |
|||
color = Color.White |
|||
) |
|||
} |
|||
} |
@ -1,57 +1,9 @@ |
|||
package com.whitefish.app.ui.home.recovery |
|||
package com.whitefish.ring.ui.home.recovery |
|||
|
|||
import androidx.lifecycle.ViewModel |
|||
import com.whitefish.ring.ui.home.recovery.RecoveryData |
|||
import kotlinx.coroutines.flow.MutableStateFlow |
|||
import kotlinx.coroutines.flow.StateFlow |
|||
import kotlinx.coroutines.flow.asStateFlow |
|||
|
|||
data class RecoveryUiState( |
|||
val recoveryData: List<RecoveryData> = emptyList(), |
|||
val isLoading: Boolean = false |
|||
) |
|||
|
|||
class RecoveryViewModel : ViewModel() { |
|||
|
|||
private val _uiState = MutableStateFlow(RecoveryUiState()) |
|||
val uiState: StateFlow<RecoveryUiState> = _uiState.asStateFlow() |
|||
|
|||
init { |
|||
loadRecoveryData() |
|||
} |
|||
|
|||
private fun loadRecoveryData() { |
|||
// 模拟加载恢复数据 |
|||
val mockData = listOf( |
|||
RecoveryData( |
|||
title = "今日恢复状态", |
|||
description = "基于昨晚睡眠质量和心率变异性分析", |
|||
recoveryIndex = 85, |
|||
restTime = "6小时", |
|||
status = "良好" |
|||
), |
|||
RecoveryData( |
|||
title = "压力水平", |
|||
description = "当前压力指数偏低,适合进行中等强度运动", |
|||
recoveryIndex = 72, |
|||
restTime = "4小时", |
|||
status = "正常" |
|||
), |
|||
RecoveryData( |
|||
title = "肌肉恢复", |
|||
description = "上次运动后肌肉恢复情况", |
|||
recoveryIndex = 90, |
|||
restTime = "2小时", |
|||
status = "优秀" |
|||
) |
|||
) |
|||
|
|||
_uiState.value = _uiState.value.copy(recoveryData = mockData) |
|||
} |
|||
|
|||
fun refresh() { |
|||
_uiState.value = _uiState.value.copy(isLoading = true) |
|||
loadRecoveryData() |
|||
_uiState.value = _uiState.value.copy(isLoading = false) |
|||
} |
|||
|
|||
|
|||
} |
Loading…
Reference in new issue