feat: responsive layout for foldable phones (compact < 400dp)

Reduce padding, icon sizes, and chart dimensions on narrow screens.
Hide title text and battery % in compact mode to prevent top bar
overflow. Scale donut chart 240→180dp and SpO2 text 56→40sp.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-25 10:36:53 +09:00
parent 9feaae1939
commit 76dbf5fe12
2 changed files with 56 additions and 46 deletions
@@ -20,6 +20,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -48,6 +49,9 @@ fun PiezoMonitoringView(appState: AppState) {
var reconnectionState by remember { mutableStateOf(BleReconnectionState.DISCONNECTED) } var reconnectionState by remember { mutableStateOf(BleReconnectionState.DISCONNECTED) }
var isAutoMeasuring by remember { mutableStateOf(false) } var isAutoMeasuring by remember { mutableStateOf(false) }
val screenWidth = LocalConfiguration.current.screenWidthDp
val isCompact = screenWidth < 400
val isDemoMode = appState.isDemoMode val isDemoMode = appState.isDemoMode
val urgency = appState.bladderLevel.urgency val urgency = appState.bladderLevel.urgency
val batteryLvl by bleManager.batteryLevel val batteryLvl by bleManager.batteryLevel
@@ -131,30 +135,31 @@ fun PiezoMonitoringView(appState: AppState) {
.background(urgency.backgroundColor) .background(urgency.backgroundColor)
) { ) {
// Top bar // Top bar
val hPad = if (isCompact) 12.dp else 24.dp
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 16.dp), .padding(horizontal = hPad, vertical = if (isCompact) 8.dp else 16.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
IconButton(onClick = { appState.goHome() }) { IconButton(onClick = { appState.goHome() }, modifier = Modifier.size(if (isCompact) 36.dp else 48.dp)) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back", tint = MlPrimary) Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back", tint = MlPrimary, modifier = Modifier.size(if (isCompact) 20.dp else 24.dp))
}
if (!isCompact) {
Text("Bladder Monitor", fontSize = 20.sp, fontWeight = FontWeight.Bold, modifier = Modifier.padding(start = 8.dp))
} }
Text("Bladder Monitor", fontSize = 20.sp, fontWeight = FontWeight.Bold, modifier = Modifier.padding(start = 8.dp))
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
if (isAutoMeasuring) { if (isAutoMeasuring) {
Row( Box(
modifier = Modifier modifier = Modifier
.clip(RoundedCornerShape(8.dp)) .clip(RoundedCornerShape(8.dp))
.background(Color.Red.copy(alpha = 0.12f)) .background(Color.Red.copy(alpha = 0.12f))
.padding(horizontal = 8.dp, vertical = 4.dp), .padding(horizontal = 6.dp, vertical = 3.dp)
verticalAlignment = Alignment.CenterVertically
) { ) {
Box(modifier = Modifier.size(8.dp).clip(CircleShape).background(Color.Red)) Text(if (isCompact) "A" else "Auto", fontSize = 11.sp, fontWeight = FontWeight.Bold, color = Color.Red)
Spacer(modifier = Modifier.width(4.dp))
Text("Auto", fontSize = 12.sp, fontWeight = FontWeight.Bold, color = Color.Red)
} }
Spacer(modifier = Modifier.width(4.dp))
} }
// Catheter count // Catheter count
@@ -163,27 +168,26 @@ fun PiezoMonitoringView(appState: AppState) {
.clip(RoundedCornerShape(8.dp)) .clip(RoundedCornerShape(8.dp))
.background(MlPrimary.copy(alpha = 0.1f)) .background(MlPrimary.copy(alpha = 0.1f))
.clickable { showAddCatheterDialog = true } .clickable { showAddCatheterDialog = true }
.padding(horizontal = 8.dp, vertical = 4.dp), .padding(horizontal = 6.dp, vertical = 3.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon(Icons.Default.Add, "Add catheter", tint = MlPrimary, modifier = Modifier.size(14.dp)) Icon(Icons.Default.Add, "Add catheter", tint = MlPrimary, modifier = Modifier.size(12.dp))
Spacer(modifier = Modifier.width(2.dp)) Text("$catheterCount", fontSize = 12.sp, fontWeight = FontWeight.Bold, color = MlPrimary)
Text("$catheterCount", fontSize = 13.sp, fontWeight = FontWeight.Bold, color = MlPrimary)
} }
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
IconButton(onClick = { appState.showHistory() }) { IconButton(onClick = { appState.showHistory() }, modifier = Modifier.size(if (isCompact) 36.dp else 48.dp)) {
Icon(Icons.Default.History, "History", tint = MlSecondaryText) Icon(Icons.Default.History, "History", tint = MlSecondaryText, modifier = Modifier.size(if (isCompact) 20.dp else 24.dp))
} }
IconButton(onClick = { showSettings = !showSettings }) { IconButton(onClick = { showSettings = !showSettings }, modifier = Modifier.size(if (isCompact) 36.dp else 48.dp)) {
Icon(Icons.Default.Settings, "Settings", tint = if (showSettings) MlPrimary else MlSecondaryText) Icon(Icons.Default.Settings, "Settings", tint = if (showSettings) MlPrimary else MlSecondaryText, modifier = Modifier.size(if (isCompact) 20.dp else 24.dp))
} }
// Battery // Battery / Demo
if (isDemoMode) { if (isDemoMode) {
Text("DEMO", fontSize = 11.sp, fontWeight = FontWeight.Bold, color = Color.White, Text("DEMO", fontSize = 10.sp, fontWeight = FontWeight.Bold, color = Color.White,
modifier = Modifier.clip(RoundedCornerShape(4.dp)).background(Color(0xFFFF9500)).padding(horizontal = 6.dp, vertical = 2.dp)) modifier = Modifier.clip(RoundedCornerShape(4.dp)).background(Color(0xFFFF9500)).padding(horizontal = 4.dp, vertical = 2.dp))
} else { } else if (!isCompact) {
Text("$batteryLvl%", fontSize = 13.sp, color = MlSecondaryText) Text("$batteryLvl%", fontSize = 13.sp, color = MlSecondaryText)
} }
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
@@ -212,10 +216,10 @@ fun PiezoMonitoringView(appState: AppState) {
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(horizontal = 24.dp) .padding(horizontal = hPad)
.padding(top = 16.dp, bottom = 32.dp), .padding(top = if (isCompact) 8.dp else 16.dp, bottom = if (isCompact) 16.dp else 32.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(20.dp) verticalArrangement = Arrangement.spacedBy(if (isCompact) 12.dp else 20.dp)
) { ) {
// Settings panel // Settings panel
if (showSettings) { if (showSettings) {
@@ -261,15 +265,16 @@ fun PiezoMonitoringView(appState: AppState) {
) )
// Donut chart with Bladdy // Donut chart with Bladdy
val donutSize = if (isCompact) 180.dp else 240.dp
Box(contentAlignment = Alignment.Center) { Box(contentAlignment = Alignment.Center) {
DonutChartView( DonutChartView(
progress = appState.bladderLevel.progress, progress = appState.bladderLevel.progress,
urgency = urgency, urgency = urgency,
lineWidth = 28.dp, lineWidth = if (isCompact) 20.dp else 28.dp,
size = 240.dp size = donutSize
) )
Column(horizontalAlignment = Alignment.CenterHorizontally) { Column(horizontalAlignment = Alignment.CenterHorizontally) {
BladdyRiveView(urgency = urgency, size = 100.dp) BladdyRiveView(urgency = urgency, size = if (isCompact) 70.dp else 100.dp)
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
Text( Text(
"${appState.estimatedVolume} ml", "${appState.estimatedVolume} ml",
@@ -23,6 +23,7 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -45,6 +46,9 @@ fun VivamyoMonitoringView(appState: AppState) {
val nirsManager = remember { NirsManager.instance } val nirsManager = remember { NirsManager.instance }
val handler = remember { Handler(Looper.getMainLooper()) } val handler = remember { Handler(Looper.getMainLooper()) }
val screenWidth = LocalConfiguration.current.screenWidthDp
val isCompact = screenWidth < 400
var selectedTab by remember { mutableIntStateOf(0) } var selectedTab by remember { mutableIntStateOf(0) }
var showClearConfirm by remember { mutableStateOf(false) } var showClearConfirm by remember { mutableStateOf(false) }
var isToggling by remember { mutableStateOf(false) } var isToggling by remember { mutableStateOf(false) }
@@ -227,28 +231,29 @@ fun VivamyoMonitoringView(appState: AppState) {
.background(Color.White) .background(Color.White)
) { ) {
// Top bar // Top bar
val hPad = if (isCompact) 12.dp else 24.dp
Row( Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp, vertical = 16.dp), modifier = Modifier.fillMaxWidth().padding(horizontal = hPad, vertical = if (isCompact) 8.dp else 16.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
IconButton(onClick = { IconButton(onClick = {
nirsManager.reset() nirsManager.reset()
appState.goHome() appState.goHome()
}) { }, modifier = Modifier.size(if (isCompact) 36.dp else 48.dp)) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back", tint = MlPrimary) Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back", tint = MlPrimary, modifier = Modifier.size(if (isCompact) 20.dp else 24.dp))
} }
Column(modifier = Modifier.padding(start = 8.dp)) { Column(modifier = Modifier.padding(start = if (isCompact) 4.dp else 8.dp)) {
Text("Vivamyo", fontSize = 20.sp, fontWeight = FontWeight.Bold) Text("Vivamyo", fontSize = if (isCompact) 16.sp else 20.sp, fontWeight = FontWeight.Bold)
Text("NIRS Monitoring", fontSize = 13.sp, color = MlSecondaryText) if (!isCompact) Text("NIRS Monitoring", fontSize = 13.sp, color = MlSecondaryText)
} }
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
if (isDemoMode) { if (isDemoMode) {
Text("DEMO", fontSize = 11.sp, fontWeight = FontWeight.Bold, color = Color.White, Text("DEMO", fontSize = 10.sp, fontWeight = FontWeight.Bold, color = Color.White,
modifier = Modifier.clip(RoundedCornerShape(4.dp)).background(Color(0xFFFF9500)).padding(horizontal = 6.dp, vertical = 2.dp)) modifier = Modifier.clip(RoundedCornerShape(4.dp)).background(Color(0xFFFF9500)).padding(horizontal = 4.dp, vertical = 2.dp))
} else { } else {
Text("$batteryLvl%", fontSize = 13.sp, color = MlSecondaryText) Text("$batteryLvl%", fontSize = if (isCompact) 11.sp else 13.sp, color = MlSecondaryText)
} }
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(if (isCompact) 4.dp else 8.dp))
Box(modifier = Modifier.size(10.dp).clip(CircleShape).background(if (isDemoMode || isConnected) Color.Green else Color.Red)) Box(modifier = Modifier.size(10.dp).clip(CircleShape).background(if (isDemoMode || isConnected) Color.Green else Color.Red))
} }
@@ -265,27 +270,27 @@ fun VivamyoMonitoringView(appState: AppState) {
} }
// Tab picker // Tab picker
TabRow(selectedTabIndex = selectedTab, modifier = Modifier.padding(horizontal = 24.dp)) { TabRow(selectedTabIndex = selectedTab, modifier = Modifier.padding(horizontal = hPad)) {
Tab(selected = selectedTab == 0, onClick = { selectedTab = 0 }) { Text("SpO2", modifier = Modifier.padding(12.dp)) } Tab(selected = selectedTab == 0, onClick = { selectedTab = 0 }) { Text("SpO2", modifier = Modifier.padding(if (isCompact) 8.dp else 12.dp)) }
Tab(selected = selectedTab == 1, onClick = { selectedTab = 1 }) { Text("Oxy/Deoxy", modifier = Modifier.padding(12.dp)) } Tab(selected = selectedTab == 1, onClick = { selectedTab = 1 }) { Text("Oxy/Deoxy", modifier = Modifier.padding(if (isCompact) 8.dp else 12.dp)) }
} }
Column( Column(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(horizontal = 24.dp, vertical = 16.dp), .padding(horizontal = hPad, vertical = if (isCompact) 8.dp else 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(if (isCompact) 10.dp else 16.dp)
) { ) {
if (selectedTab == 0) { if (selectedTab == 0) {
// SpO2 tab // SpO2 tab
BladdyIdleRiveView( BladdyIdleRiveView(
size = 120.dp, size = if (isCompact) 80.dp else 120.dp,
modifier = Modifier.align(Alignment.CenterHorizontally) modifier = Modifier.align(Alignment.CenterHorizontally)
) )
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth()) { Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth()) {
Text(String.format("%.1f", currentOxy.stO2), fontSize = 56.sp, fontWeight = FontWeight.Bold, color = stO2Color) Text(String.format("%.1f", currentOxy.stO2), fontSize = if (isCompact) 40.sp else 56.sp, fontWeight = FontWeight.Bold, color = stO2Color)
Text("% StO2", fontSize = 18.sp, color = MlSecondaryText) Text("% StO2", fontSize = 18.sp, color = MlSecondaryText)
Text(stO2StatusText, fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = stO2Color, Text(stO2StatusText, fontSize = 14.sp, fontWeight = FontWeight.SemiBold, color = stO2Color,
modifier = Modifier.clip(RoundedCornerShape(8.dp)).background(stO2Color.copy(alpha = 0.15f)).padding(horizontal = 12.dp, vertical = 4.dp)) modifier = Modifier.clip(RoundedCornerShape(8.dp)).background(stO2Color.copy(alpha = 0.15f)).padding(horizontal = 12.dp, vertical = 4.dp))
@@ -320,7 +325,7 @@ fun VivamyoMonitoringView(appState: AppState) {
// Control bar // Control bar
Row( Row(
modifier = Modifier.padding(horizontal = 24.dp).padding(bottom = 32.dp), modifier = Modifier.padding(horizontal = hPad).padding(bottom = if (isCompact) 16.dp else 32.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp) horizontalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
Button( Button(