UI: PiezoPersonalizationView 전면 리디자인 — 평행 2-카드
빌드 107-119 적용: - Bladdy 히어로 BoxWithConstraints 동적 사이징, 카드 영역 수직 중앙 정렬 - Card 1 "Maximum Bladder Capacity" 34sp ExtraBold + 500/mL (64+24sp) + 56dp 솔리드 StepperButton + 슬라이더 - Card 2 "Catheter Threshold" 34sp ExtraBold + Level/7/10 3분할(24+64+24sp) + StepperButton + Level 1~10 슬라이더 - 부제 모두 제거, 섹션 간격 64dp - Slider 값 부동소수점 절삭 버그 fix: it.toInt() → it.roundToInt() Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+161
-193
@@ -23,29 +23,20 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.min
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.example.medilightv2android.AppState
|
||||
import com.example.medilightv2android.ble.BleManager
|
||||
import com.example.medilightv2android.models.*
|
||||
import com.example.medilightv2android.ui.components.BladdyIdleRiveView
|
||||
import com.example.medilightv2android.ui.theme.*
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun PiezoPersonalizationView(appState: AppState) {
|
||||
val context = LocalContext.current
|
||||
val bleManager = remember { BleManager.getInstance(context) }
|
||||
val handler = remember { Handler(Looper.getMainLooper()) }
|
||||
var currentStep by remember { mutableIntStateOf(1) }
|
||||
var optimizationComplete by remember { mutableStateOf(false) }
|
||||
val totalSteps = 1
|
||||
|
||||
fun startOptimization() {
|
||||
bleManager.onPiezoDataReceived = {
|
||||
optimizationComplete = true
|
||||
}
|
||||
bleManager.sendPiezoPowerOn(freqOption = 2, cycles = 5)
|
||||
handler.postDelayed({ optimizationComplete = true }, 6000)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -64,197 +55,174 @@ fun PiezoPersonalizationView(appState: AppState) {
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Max Volume만 표시
|
||||
when (currentStep) {
|
||||
1 -> MaxVolumeStep(appState)
|
||||
// Bladdy hero — 동적 사이징
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
val bladdySize = min(maxWidth, 220.dp)
|
||||
BladdyIdleRiveView(size = bladdySize)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
// Navigation button
|
||||
if (currentStep <= totalSteps) {
|
||||
Button(
|
||||
onClick = { appState.personalizationComplete() },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 32.dp)
|
||||
.padding(bottom = 32.dp)
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent),
|
||||
contentPadding = PaddingValues()
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize().background(
|
||||
Brush.horizontalGradient(listOf(MlTeal, MlPrimary)),
|
||||
RoundedCornerShape(16.dp)
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("Continue", fontSize = 18.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PostureStep(appState: AppState) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text("Select Posture", fontSize = 24.sp, fontWeight = FontWeight.Bold)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text("Choose your measurement position", fontSize = 14.sp, color = MlSecondaryText)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Column(modifier = Modifier.padding(horizontal = 32.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Posture.entries.forEach { posture ->
|
||||
val isSelected = appState.piezoSettings.posture == posture
|
||||
SelectionCard(
|
||||
title = posture.displayName,
|
||||
isSelected = isSelected,
|
||||
onClick = { appState.piezoSettings = appState.piezoSettings.copy(posture = posture) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SensorPositionStep(appState: AppState) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text("Sensor Position", fontSize = 24.sp, fontWeight = FontWeight.Bold)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text("Place the sensor on your body", fontSize = 14.sp, color = MlSecondaryText)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Column(modifier = Modifier.padding(horizontal = 32.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
SensorPosition.entries.forEach { position ->
|
||||
val isSelected = appState.piezoSettings.sensorPosition == position
|
||||
SelectionCard(
|
||||
title = position.displayName,
|
||||
subtitle = position.description,
|
||||
isSelected = isSelected,
|
||||
onClick = { appState.piezoSettings = appState.piezoSettings.copy(sensorPosition = position) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MaxVolumeStep(appState: AppState) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text("Bladder Capacity", fontSize = 24.sp, fontWeight = FontWeight.Bold)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text("Set your maximum bladder capacity (mL)", fontSize = 14.sp, color = MlSecondaryText)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Row(verticalAlignment = Alignment.Bottom) {
|
||||
Text("${appState.piezoSettings.maxVolume}", fontSize = 56.sp, fontWeight = FontWeight.Bold, color = MlPrimary)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("mL", fontSize = 22.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText,
|
||||
modifier = Modifier.padding(bottom = 10.dp))
|
||||
// 카드 영역 — 수직 중앙 정렬
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(64.dp, Alignment.CenterVertically),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
MaxVolumeCard(appState)
|
||||
CatheterThresholdCard(appState)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(32.dp)) {
|
||||
IconButton(onClick = {
|
||||
appState.piezoSettings = appState.piezoSettings.copy(
|
||||
maxVolume = (appState.piezoSettings.maxVolume - 50).coerceIn(200, 800)
|
||||
)
|
||||
}) {
|
||||
Icon(Icons.Default.Remove, null, tint = MlPrimary, modifier = Modifier.size(44.dp))
|
||||
}
|
||||
IconButton(onClick = {
|
||||
appState.piezoSettings = appState.piezoSettings.copy(
|
||||
maxVolume = (appState.piezoSettings.maxVolume + 50).coerceIn(200, 800)
|
||||
)
|
||||
}) {
|
||||
Icon(Icons.Default.Add, null, tint = MlPrimary, modifier = Modifier.size(44.dp))
|
||||
}
|
||||
}
|
||||
|
||||
Slider(
|
||||
value = appState.piezoSettings.maxVolume.toFloat(),
|
||||
onValueChange = { appState.piezoSettings = appState.piezoSettings.copy(maxVolume = it.toInt()) },
|
||||
valueRange = 200f..800f,
|
||||
steps = 11,
|
||||
modifier = Modifier.padding(horizontal = 48.dp),
|
||||
colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary)
|
||||
)
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth().padding(horizontal = 48.dp)) {
|
||||
Text("200ml", fontSize = 12.sp, color = MlSecondaryText)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text("800ml", fontSize = 12.sp, color = MlSecondaryText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OptimizationStep(isComplete: Boolean) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
if (isComplete) "Optimization Complete" else "Optimizing...",
|
||||
fontSize = 24.sp, fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
BladdyIdleRiveView(size = 160.dp)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
if (isComplete) {
|
||||
Icon(Icons.Default.CheckCircle, null, tint = MlTeal, modifier = Modifier.size(48.dp))
|
||||
} else {
|
||||
CircularProgressIndicator(color = MlPrimary)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text("Calibrating sensor parameters", fontSize = 14.sp, color = MlSecondaryText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SelectionCard(title: String, subtitle: String? = null, isSelected: Boolean, onClick: () -> Unit) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.then(if (isSelected) Modifier.border(2.dp, MlPrimary, RoundedCornerShape(16.dp)) else Modifier),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MlCardBackground)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
// Continue button
|
||||
Button(
|
||||
onClick = { appState.personalizationComplete() },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 32.dp)
|
||||
.padding(bottom = 32.dp)
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent),
|
||||
contentPadding = PaddingValues()
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(if (subtitle != null) 48.dp else 56.dp)
|
||||
.clip(RoundedCornerShape(if (subtitle != null) 12.dp else 14.dp))
|
||||
.background(if (isSelected) MlPrimary else MlPrimary.copy(alpha = 0.1f)),
|
||||
modifier = Modifier.fillMaxSize().background(
|
||||
Brush.horizontalGradient(listOf(MlTeal, MlPrimary)),
|
||||
RoundedCornerShape(16.dp)
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(title.take(3), color = if (isSelected) Color.White else MlPrimary, fontWeight = FontWeight.Bold, fontSize = 12.sp)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(title, fontSize = if (subtitle != null) 16.sp else 18.sp, fontWeight = FontWeight.SemiBold)
|
||||
subtitle?.let {
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(it, fontSize = 13.sp, color = MlSecondaryText)
|
||||
}
|
||||
}
|
||||
|
||||
if (isSelected) {
|
||||
Icon(Icons.Default.CheckCircle, null, tint = MlPrimary, modifier = Modifier.size(24.dp))
|
||||
Text("Continue", fontSize = 18.sp, fontWeight = FontWeight.SemiBold, color = Color.White)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MaxVolumeCard(appState: AppState) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MlPrimary.copy(alpha = 0.06f))
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 20.dp, vertical = 20.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text("Maximum Bladder Capacity", fontSize = 34.sp, fontWeight = FontWeight.ExtraBold)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Row(verticalAlignment = Alignment.Bottom) {
|
||||
Text("${appState.piezoSettings.maxVolume}", fontSize = 64.sp,
|
||||
fontWeight = FontWeight.ExtraBold, color = MlPrimary)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("mL", fontSize = 24.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText,
|
||||
modifier = Modifier.padding(bottom = 12.dp))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(32.dp)) {
|
||||
StepperButton(icon = Icons.Default.Remove) {
|
||||
appState.piezoSettings = appState.piezoSettings.copy(
|
||||
maxVolume = (appState.piezoSettings.maxVolume - 50).coerceIn(200, 800)
|
||||
)
|
||||
}
|
||||
StepperButton(icon = Icons.Default.Add) {
|
||||
appState.piezoSettings = appState.piezoSettings.copy(
|
||||
maxVolume = (appState.piezoSettings.maxVolume + 50).coerceIn(200, 800)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Slider(
|
||||
value = appState.piezoSettings.maxVolume.toFloat(),
|
||||
onValueChange = {
|
||||
appState.piezoSettings = appState.piezoSettings.copy(maxVolume = it.roundToInt())
|
||||
},
|
||||
valueRange = 200f..800f,
|
||||
steps = 11,
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CatheterThresholdCard(appState: AppState) {
|
||||
val threshold = appState.bladderLevel.catheterizationThreshold
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MlTeal.copy(alpha = 0.06f))
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 20.dp, vertical = 20.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text("Catheter Threshold", fontSize = 34.sp, fontWeight = FontWeight.ExtraBold)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// "Level 7 / 10" — 3분할 픽셀 매칭 (Card 1 "500 mL"와 동일 톤)
|
||||
Row(verticalAlignment = Alignment.Bottom) {
|
||||
Text("Level", fontSize = 24.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText,
|
||||
modifier = Modifier.padding(bottom = 12.dp))
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("$threshold", fontSize = 64.sp,
|
||||
fontWeight = FontWeight.ExtraBold, color = MlPrimary)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("/10", fontSize = 24.sp, fontWeight = FontWeight.SemiBold, color = MlSecondaryText,
|
||||
modifier = Modifier.padding(bottom = 12.dp))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(32.dp)) {
|
||||
StepperButton(icon = Icons.Default.Remove) {
|
||||
appState.updateCatheterizationThreshold((threshold - 1).coerceIn(1, 10))
|
||||
}
|
||||
StepperButton(icon = Icons.Default.Add) {
|
||||
appState.updateCatheterizationThreshold((threshold + 1).coerceIn(1, 10))
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Slider(
|
||||
value = threshold.toFloat(),
|
||||
onValueChange = {
|
||||
appState.updateCatheterizationThreshold(it.roundToInt().coerceIn(1, 10))
|
||||
},
|
||||
valueRange = 1f..10f,
|
||||
steps = 8,
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StepperButton(icon: androidx.compose.ui.graphics.vector.ImageVector, onClick: () -> Unit) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(56.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MlPrimary)
|
||||
.clickable(onClick = onClick),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(icon, null, tint = Color.White, modifier = Modifier.size(28.dp))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user