UI 묶음 A: 도넛차트 정리 — 캐릭터/mL/게이지/카드 (회의 #4 #5 #6 #7)

#4 도넛 안 캐릭터/mL 위치
  - bladdySize: donutSize*0.85 → donutSize*0.85*0.75 (75% 축소)
  - volFontSize: 40sp → 50sp (125%)
  - offset 위로: -0.03 → -0.08 (더 위로)
  - 캐릭터-mL Spacer: 4dp → 0dp (간격 축소)
  → 윗공간 비던 부분 흡수, 그룹이 도넛 가운데 컴팩트 정렬

#5 게이지(stroke arc)가 도넛 외곽선 이탈 → 동일 크기 맞춤
  Compose Canvas의 drawCircle/drawArc는 default로 size 경계에 stroke를
  그리는데 strokeWidth의 절반이 밖으로 튀어나옴.
  → 명시적 inner radius 계산:
     outerR = size.minDimension / 2
     innerR = outerR - strokeWidth / 2
     drawCircle/drawArc 모두 innerR로 그림 → stroke가 size 안에 들어옴.

#6 우측 InfoCard 'Session Max' → MaxRatioCard (% + 측정/capacity)
  새 컴포저블 MaxRatioCard:
    위: "{pct}%" (큰 글씨, urgency 색)
    아래: "{measured} / {capacity} mL"
  예: 47% / 235 / 500 mL

#7 좌측 InfoCard 'Current' → 'Current Measurement'

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 16:58:39 +09:00
parent 249b5f1aaa
commit bd99cae98c
2 changed files with 53 additions and 14 deletions
@@ -38,20 +38,33 @@ fun DonutChartView(
.semantics { contentDescription = "Bladder fill $pct percent, ${urgency.message}" }
) {
val strokeWidth = lineWidth.toPx()
// Stroke 외곽이 Canvas 경계 밖으로 튀어나오는 걸 막기 위해 radius를 inner로 명시.
// 회의 #5: 게이지가 도넛 외곽선을 이탈해 보이던 문제 해결.
val outerR = this.size.minDimension / 2f
val innerR = outerR - strokeWidth / 2f
val center = androidx.compose.ui.geometry.Offset(this.size.width / 2f, this.size.height / 2f)
// Background track
drawCircle(
color = Color.Gray.copy(alpha = 0.12f),
radius = innerR,
center = center,
style = Stroke(width = strokeWidth)
)
// Progress arc
// Progress arc — same radius
if (animatedProgress > 0f) {
val arcSize = androidx.compose.ui.geometry.Size(innerR * 2, innerR * 2)
val arcOffset = androidx.compose.ui.geometry.Offset(
center.x - innerR, center.y - innerR
)
drawArc(
color = gradientColors.first(),
startAngle = -90f,
sweepAngle = 360f * animatedProgress,
useCenter = false,
topLeft = arcOffset,
size = arcSize,
style = Stroke(width = strokeWidth, cap = StrokeCap.Round)
)
}
@@ -1015,9 +1015,9 @@ fun PiezoMonitoringView(appState: AppState) {
val baseDonutSize = donutFitSize
val donutSize = baseDonutSize * 0.95f
val donutLineWidth = if (isCompact) 26.dp else 34.dp
val bladdySize = donutSize * 0.85f
// ml 폰트 키움 — 회의 피드백 "숫자가 너무 작아서 안보임"
val volFontSize = 40.sp
// 회의 #4: 캐릭터 75% 축소, mL 125% 키움 → 컴팩트하게 도넛 가운데
val bladdySize = donutSize * 0.85f * 0.75f
val volFontSize = 50.sp
val maxVolumeForDonut = appState.piezoSettings.maxVolume.toDouble()
val donutProgress = if (maxVolumeForDonut > 0 && maxVolumeMl > 0)
@@ -1042,7 +1042,8 @@ fun PiezoMonitoringView(appState: AppState) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.matchParentSize().offset(y = -(donutSize * 0.03f))
// 회의 #4: 캐릭터+mL 그룹을 위로 더 올려 도넛 가운데 컴팩트 정렬 (이전 0.03 → 0.08)
modifier = Modifier.matchParentSize().offset(y = -(donutSize * 0.08f))
) {
// Charles 833c110: Bladdy + 3-tap dev mode toggle (HomeView 패턴 통일)
Box(contentAlignment = Alignment.Center) {
@@ -1067,16 +1068,16 @@ fun PiezoMonitoringView(appState: AppState) {
}
)
}
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(0.dp)) // 캐릭터-mL 간격 축소
Text(
// Voiding 직후엔 displayMaxVolumeMl=0이고 estimatedVolume도 0 → "0 mL"가 의미 없음 → "— mL"로
if (displayMaxVolumeMl > 0) "${"%.0f".format(displayMaxVolumeMl)} mL"
else if (appState.estimatedVolume > 0) "${appState.estimatedVolume} mL"
else "— mL",
fontSize = volFontSize,
fontWeight = if (useScrollLayout) FontWeight.Bold else FontWeight.ExtraBold,
fontWeight = FontWeight.ExtraBold,
color = urgency.accentColor,
letterSpacing = if (useScrollLayout) 0.sp else (-1).sp,
letterSpacing = (-1).sp,
)
}
}
@@ -1087,15 +1088,20 @@ fun PiezoMonitoringView(appState: AppState) {
val fillPct = if (maxVol > 0 && maxVolumeMl > 0) (maxVolumeMl / maxVol * 100).coerceAtMost(100.0) else 0.0
val levelVal = if (maxVol > 0 && maxVolumeMl > 0) ((maxVolumeMl / maxVol) * BladderLevel.MAX_LEVEL).toInt().coerceIn(0, BladderLevel.MAX_LEVEL) else 0
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
// Auto 윈도우 < 5일 때는 표시 안 함
// 좌측: Current Measurement (회의 #7)
val showMeasurement = if (isAutoMeasuring) autoVolumeWindow.size >= 5 && lastVolumeMl > 0 else lastVolumeMl > 0
val currentMeasurement = if (showMeasurement) "%.0f".format(lastVolumeMl) else "—"
InfoCard("Current", currentMeasurement, "mL", urgency.accentColor, Modifier.weight(1f))
// 우측 카드: title 명시 "Session Max", 값은 maxMeasured (단위 mL).
// 이전엔 title이 "X mL / Y mL"로 들어가고 값에 % 표기되어 의미 모호했음.
// % 정보는 도넛 차트가 이미 시각화 중 → 카드는 raw 숫자만.
InfoCard("Current Measurement", currentMeasurement, "mL", urgency.accentColor, Modifier.weight(1f))
// 우측: 측정 max / max capacity 비율 (회의 #6) — 위에 XX%, 아래 raw 분수
val maxMeasured = if (displayMaxVolumeMl > 0) "%.0f".format(displayMaxVolumeMl) else "—"
InfoCard("Session Max", maxMeasured, "mL", urgency.accentColor, Modifier.weight(1f))
val capacityVal = appState.piezoSettings.maxVolume
MaxRatioCard(
pct = fillPct.toInt(),
measured = maxMeasured,
capacity = capacityVal,
color = urgency.accentColor,
modifier = Modifier.weight(1f)
)
}
// Control buttons: Void/Catheterize / Auto / Spot
@@ -2137,3 +2143,23 @@ private fun InfoCard(title: String, value: String, unit: String, color: Color, m
}
}
}
/** 회의 #6: 측정 max / max capacity 비율 카드. 위에 XX%, 아래에 "측정값 / capacity mL". */
@Composable
private fun MaxRatioCard(pct: Int, measured: String, capacity: Int, color: Color, modifier: Modifier = Modifier) {
Card(
modifier = modifier.shadow(4.dp, RoundedCornerShape(14.dp), spotColor = Color.Black.copy(alpha = 0.04f)),
shape = RoundedCornerShape(14.dp),
colors = CardDefaults.cardColors(containerColor = MlCardBackground)
) {
val fs = (LocalConfiguration.current.screenWidthDp / 360f).coerceIn(1.0f, 1.8f)
Column(
modifier = Modifier.padding(vertical = 14.dp).fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text("${pct}%", fontSize = (20 * fs).sp, fontWeight = FontWeight.Bold, color = color)
Text("$measured / $capacity mL", fontSize = (12 * fs).sp, color = MlSecondaryText)
}
}
}