ux: Sensor Alignment 태블릿 레이아웃 재배치 + 글자 키움 (demo-final)

레이아웃 순서 변경:
  기존: 타이틀 → Step → 일러스트 → Hint
  변경: 타이틀 → Hint1 → 일러스트 → Step → Hint2 → 버튼 → Skip

Hint 두 영역 분리:
  Hint 1 (일러스트 위) — 시작 전 안내
    "Place VesiScan above the pubic bone, then press Start"
    waitingForStart=true 일 때만 표시.
  Hint 2 (일러스트 아래) — 측정 중 방향 안내
    "Slide up slightly ↑", "Centered, optimizing..." 등.
    waitingForStart=false 일 때만 표시.

태블릿 기준 글자 크기:
  타이틀  32 → 56sp (ExtraBold) — 화면폭 ≈ 60% 차지
  Hint1   신규 26sp Bold
  Hint2   22 → 28sp Bold
  Step    22 → 28sp + 3-dot indicator 12/8 → 16/10dp
  SWEEP/IMU 배지 10 → 13/12sp
  Skip    15 → 20sp

기존 버튼 ("Start Alignment" 28sp, "Start Scanning" 28sp) 그대로 유지.

영향:
  - 일반 사용자 (demo flavor): 태블릿 시연 화면 정렬 강화
  - dev/feature 브랜치: 동일 패턴 cherry-pick 가능 (별개 commit)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 09:44:21 +09:00
parent 5e39237a36
commit c1533a3422
@@ -199,10 +199,10 @@ fun PlacementGuideView(appState: AppState) {
}
}
}
// (B) 타이틀 — Box.Center로 화면 절대 가운데 (← 폭과 무관). 26→32sp 강조.
// (B) 타이틀 — 태블릿 기준 화면폭 ≈ 60% 차지 (32 → 56sp).
Text(
text = "Sensor Alignment",
fontSize = 32.sp,
fontSize = 56.sp,
fontWeight = FontWeight.ExtraBold,
color = Color.Black
)
@@ -221,76 +221,23 @@ fun PlacementGuideView(appState: AppState) {
.padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Step indicator + mode badge (이미지 위에 배치)
// Step progress — 텍스트 + 3-dot indicator (현재 단계 강조)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier.padding(vertical = 6.dp)
) {
val currentStepIdx = when (placementPhase) {
PlacementPhase.VERTICAL -> 0
PlacementPhase.LATERAL -> 1
PlacementPhase.GREEN -> 2
}
// ─── (1) Hint 1 — 시작 전 안내 ("pubic bone 위에 놓으세요") ───
// waitingForStart 일 때만 표시. 시작 후엔 빈 공간.
if (waitingForStart) {
val hint1Bg = Color(0xFFFFF3E0)
Text(
text = "Step ${currentStepIdx + 1}/3",
fontSize = 22.sp, fontWeight = FontWeight.Bold, color = Color.Black
)
// 3-dot indicator: 완료(○ → ●), 현재(큰 ●), 미완료(○)
Row(horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically) {
repeat(3) { i ->
val isDone = i < currentStepIdx
val isCurrent = i == currentStepIdx
val dotSize = if (isCurrent) 12.dp else 8.dp
val dotColor = when {
isCurrent -> MlPrimary
isDone -> MlPrimary.copy(alpha = 0.6f)
else -> Color.Gray.copy(alpha = 0.3f)
}
Box(
modifier = Modifier
.size(dotSize)
.clip(CircleShape)
.background(dotColor)
)
}
}
if (isSweepMode) {
Text(
"SWEEP",
fontSize = 10.sp,
text = "Place VesiScan above the pubic bone, then press Start",
fontSize = 26.sp, // 태블릿: 더 크게
fontWeight = FontWeight.Bold,
color = Color.White,
color = Color.Black,
textAlign = TextAlign.Center,
modifier = Modifier
.background(Color(0xFF7C3AED), RoundedCornerShape(4.dp))
.padding(horizontal = 6.dp, vertical = 1.dp)
.fillMaxWidth()
.background(hint1Bg.copy(alpha = 0.7f), RoundedCornerShape(16.dp))
.padding(horizontal = 20.dp, vertical = 14.dp)
)
Spacer(modifier = Modifier.height(8.dp))
}
// IMU 정보 (Dev 모드 한정) — 자세 + 흔들림 RMS
if (appState.isDevMode && lastImuSamples.isNotEmpty()) {
val posture = remember(lastImuSamples) {
com.example.medilightv2android.managers.ImuAnalysis.estimatePosture(lastImuSamples)
}
val shake = remember(lastImuSamples) {
com.example.medilightv2android.managers.ImuAnalysis.shakeRmsDps(lastImuSamples)
}
val tilt = remember(lastImuSamples) {
com.example.medilightv2android.managers.ImuAnalysis.tiltDegrees(lastImuSamples)
}
Text(
"IMU: $posture · tilt=${"%.0f".format(tilt)}° · shake=${"%.1f".format(shake)}dps",
fontSize = 10.sp,
fontWeight = FontWeight.SemiBold,
color = if (shake > 30f) Color.Red else MlSecondaryText,
modifier = Modifier
.background(Color.Gray.copy(alpha = 0.15f), RoundedCornerShape(4.dp))
.padding(horizontal = 6.dp, vertical = 1.dp)
)
}
}
// 타이틀은 top bar 로 이동 (← 옆 절대 가운데) — 본문 중복 제거.
// ── 가이드 텍스트 (Step 아래, 몸 위) — "pubic bone" 인라인 빨강 ExtraBold 강조 ──
@@ -473,18 +420,87 @@ fun PlacementGuideView(appState: AppState) {
Spacer(modifier = Modifier.height(8.dp))
// ── 가이드 hint — 일러스트 *아래* 로 이동 (사용자 피드백) ──
// ─── (3) Step indicator — 일러스트 아래로 이동 ───
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier.padding(vertical = 4.dp)
) {
val currentStepIdx = when (placementPhase) {
PlacementPhase.VERTICAL -> 0
PlacementPhase.LATERAL -> 1
PlacementPhase.GREEN -> 2
}
Text(
text = "Step ${currentStepIdx + 1}/3",
fontSize = 28.sp, fontWeight = FontWeight.Bold, color = Color.Black // 22 → 28
)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically) {
repeat(3) { i ->
val isDone = i < currentStepIdx
val isCurrent = i == currentStepIdx
val dotSize = if (isCurrent) 16.dp else 10.dp // 12/8 → 16/10
val dotColor = when {
isCurrent -> MlPrimary
isDone -> MlPrimary.copy(alpha = 0.6f)
else -> Color.Gray.copy(alpha = 0.3f)
}
Box(
modifier = Modifier
.size(dotSize)
.clip(CircleShape)
.background(dotColor)
)
}
}
if (isSweepMode) {
Text(
"SWEEP",
fontSize = 13.sp, fontWeight = FontWeight.Bold, color = Color.White,
modifier = Modifier
.background(Color(0xFF7C3AED), RoundedCornerShape(4.dp))
.padding(horizontal = 8.dp, vertical = 2.dp)
)
}
if (appState.isDevMode && lastImuSamples.isNotEmpty()) {
val posture = remember(lastImuSamples) {
com.example.medilightv2android.managers.ImuAnalysis.estimatePosture(lastImuSamples)
}
val shake = remember(lastImuSamples) {
com.example.medilightv2android.managers.ImuAnalysis.shakeRmsDps(lastImuSamples)
}
val tilt = remember(lastImuSamples) {
com.example.medilightv2android.managers.ImuAnalysis.tiltDegrees(lastImuSamples)
}
Text(
"IMU: $posture · tilt=${"%.0f".format(tilt)}° · shake=${"%.1f".format(shake)}dps",
fontSize = 12.sp, fontWeight = FontWeight.SemiBold,
color = if (shake > 30f) Color.Red else MlSecondaryText,
modifier = Modifier
.background(Color.Gray.copy(alpha = 0.15f), RoundedCornerShape(4.dp))
.padding(horizontal = 8.dp, vertical = 2.dp)
)
}
}
Spacer(modifier = Modifier.height(8.dp))
// ─── (5) Hint 2 — 측정 중 방향 안내 ("Slide up slightly ↑" 등) ───
// waitingForStart 가 아닐 때 (=scanning 또는 phaseHold) 만 표시.
if (!waitingForStart) {
Text(
text = displayHint,
fontSize = 22.sp, // 26 → 22 (그림이 메인, hint 보조)
fontSize = 28.sp, // 22 → 28 (태블릿)
fontWeight = FontWeight.Bold,
color = Color.Black,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.background(hintBg.copy(alpha = 0.7f), RoundedCornerShape(14.dp))
.padding(horizontal = 16.dp, vertical = 10.dp)
.background(hintBg.copy(alpha = 0.7f), RoundedCornerShape(16.dp))
.padding(horizontal = 20.dp, vertical = 14.dp)
)
}
Spacer(modifier = Modifier.height(8.dp))
@@ -940,12 +956,12 @@ fun PlacementGuideView(appState: AppState) {
Text("Start Scanning", fontSize = 28.sp, fontWeight = FontWeight.Bold, color = Color.White)
}
// Skip button
// Skip button (태블릿: 15 → 20sp)
Spacer(modifier = Modifier.height(8.dp))
TextButton(onClick = { isContinuousMode = false; appState.placementComplete() }) {
Text(
text = "Skip",
fontSize = 15.sp,
fontSize = 20.sp,
fontWeight = FontWeight.SemiBold,
color = MlSecondaryText
)