ux: Sensor Alignment 사용자 피드백 5건 (demo-final)
1) Clinical alignment session 진입 시 자동 V2, 그 외 자동 V1
PlacementGuide LaunchedEffect 에서 currentSession.mode==ALIGNMENT 면 V2,
아니면 V1 로 GreenZoneConstants.alignmentAlgo 강제.
사용자 토글 불필요 — 컨텍스트로 자동 결정.
2) V2 활성 시 step indicator 4-stage (기존 3)
- V1: Step N/3 (VERTICAL / LATERAL / GREEN — placementPhase)
- V2: Step N/4 (INITIAL_ACCUM / VERTICAL_CLIMB / CH3_STABILIZE / LR_BALANCE
— v2Aligner.phase)
3) Hint 2 waitingForStart 시 공백 — pubic bone 중복 안 나오게
Hint 2 가 displayHint 표시하던 것을 waitingForStart 시 AnnotatedString(" ")
로 교체. 칸 높이 유지, 텍스트만 사라짐.
4) Hint 1 "pubic bone" 빨강 ExtraBold 강조
기존 plain Text → AnnotatedString 으로 변경 + buildAnnotatedString 의
"pubic bone" 부분에 SpanStyle(color=Red, fontWeight=ExtraBold) 적용.
5) 화살표 alpha 1.0 ↔ 0.85 → 1.0 ↔ 0.0 (완전 사라짐)
색상 #E64A19 (deep orange) → Color.Red.
tween 220ms 그대로 — 빨강 화살표가 깜빡임으로 명확히 사라짐.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+54
-18
@@ -36,6 +36,7 @@ import androidx.compose.ui.layout.ContentScale
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.SpanStyle
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
@@ -120,8 +121,15 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
var sweepLastDetected by remember { mutableIntStateOf(0) }
|
var sweepLastDetected by remember { mutableIntStateOf(0) }
|
||||||
var sweepEverSawAll by remember { mutableStateOf(false) }
|
var sweepEverSawAll by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
// 화면 진입 시 전체 상태 리셋
|
// 화면 진입 시 전체 상태 리셋. clinical alignment session 이면 V2, 그 외 V1 자동 강제.
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
|
val sess = com.example.medilightv2android.services.ClinicalSessionStore.currentSession
|
||||||
|
val isClinicalAlign = sess != null &&
|
||||||
|
sess.mode == com.example.medilightv2android.models.SessionMode.ALIGNMENT
|
||||||
|
com.example.medilightv2android.managers.GreenZoneConstants.alignmentAlgo =
|
||||||
|
if (isClinicalAlign) com.example.medilightv2android.managers.AlignmentAlgo.V2
|
||||||
|
else com.example.medilightv2android.managers.AlignmentAlgo.V1
|
||||||
|
v2Aligner.reset()
|
||||||
isContinuousMode = false
|
isContinuousMode = false
|
||||||
waitingForStart = true
|
waitingForStart = true
|
||||||
placementPhase = PlacementPhase.VERTICAL
|
placementPhase = PlacementPhase.VERTICAL
|
||||||
@@ -224,13 +232,25 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
.padding(horizontal = 24.dp),
|
.padding(horizontal = 24.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
// ─── (1) Hint 1 — 시작 전 안내 (칸은 상시 유지, 텍스트는 Start 누르면 사라짐) ───
|
// ─── (1) Hint 1 — 시작 전 안내 (칸 상시 유지, "pubic bone" 빨강 강조) ───
|
||||||
val hint1Bg = Color(0xFFFFF3E0)
|
val hint1Bg = Color(0xFFFFF3E0)
|
||||||
|
val hint1Text = remember(waitingForStart) {
|
||||||
|
if (waitingForStart) buildAnnotatedString {
|
||||||
|
val full = "Place VesiScan above the pubic bone, then press Start"
|
||||||
|
val key = "pubic bone"
|
||||||
|
val idx = full.indexOf(key, ignoreCase = true)
|
||||||
|
if (idx >= 0) {
|
||||||
|
append(full.substring(0, idx))
|
||||||
|
withStyle(SpanStyle(color = Color.Red, fontWeight = FontWeight.ExtraBold)) {
|
||||||
|
append(full.substring(idx, idx + key.length))
|
||||||
|
}
|
||||||
|
append(full.substring(idx + key.length))
|
||||||
|
} else append(full)
|
||||||
|
} else AnnotatedString(" ") // 공백 — 칸 높이 유지
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
text = if (waitingForStart)
|
text = hint1Text,
|
||||||
"Place VesiScan above the pubic bone, then press Start"
|
fontSize = 26.sp,
|
||||||
else " ", // 공백 — 칸 높이 유지
|
|
||||||
fontSize = 26.sp, // 태블릿: 더 크게
|
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = Color.Black,
|
color = Color.Black,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
@@ -277,8 +297,10 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
|
|
||||||
// 화살표 깜빡임 — alpha 1.0 ↔ 0.85 (최솟값 0.85 → body 일러스트 살색 안 비침)
|
// 화살표 깜빡임 — alpha 1.0 ↔ 0.85 (최솟값 0.85 → body 일러스트 살색 안 비침)
|
||||||
// 이전 0.35 만큼 떨어지면 skin 톤이 비쳐 보이던 사용자 피드백 반영.
|
// 이전 0.35 만큼 떨어지면 skin 톤이 비쳐 보이던 사용자 피드백 반영.
|
||||||
|
// 화살표 깜빡임 — alpha 1.0 ↔ 0.0 (완전 사라짐), 빨강 색.
|
||||||
|
// 사용자 피드백: "붉은색/사라짐 으로 깜빡임"
|
||||||
val arrowAlpha by animateFloatAsState(
|
val arrowAlpha by animateFloatAsState(
|
||||||
targetValue = if (scanCount % 2 == 0) 1f else 0.85f,
|
targetValue = if (scanCount % 2 == 0) 1f else 0f,
|
||||||
animationSpec = tween(durationMillis = 220),
|
animationSpec = tween(durationMillis = 220),
|
||||||
label = "arrowBlink"
|
label = "arrowBlink"
|
||||||
)
|
)
|
||||||
@@ -397,7 +419,7 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
}
|
}
|
||||||
if (placement != null) {
|
if (placement != null) {
|
||||||
val tint = androidx.compose.ui.graphics.ColorFilter.tint(
|
val tint = androidx.compose.ui.graphics.ColorFilter.tint(
|
||||||
Color(0xFFE64A19).copy(alpha = arrowAlpha)
|
Color.Red.copy(alpha = arrowAlpha)
|
||||||
)
|
)
|
||||||
rotate(degrees = placement.rot, pivot = Offset(placement.cxa, placement.cya)) {
|
rotate(degrees = placement.rot, pivot = Offset(placement.cxa, placement.cya)) {
|
||||||
drawImage(
|
drawImage(
|
||||||
@@ -428,18 +450,32 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
modifier = Modifier.padding(vertical = 4.dp)
|
modifier = Modifier.padding(vertical = 4.dp)
|
||||||
) {
|
) {
|
||||||
val currentStepIdx = when (placementPhase) {
|
// V2 활성 시 4-stage step indicator, 아니면 V1 의 3-stage.
|
||||||
PlacementPhase.VERTICAL -> 0
|
val isV2Active = com.example.medilightv2android.managers.GreenZoneConstants.alignmentAlgo ==
|
||||||
PlacementPhase.LATERAL -> 1
|
com.example.medilightv2android.managers.AlignmentAlgo.V2
|
||||||
PlacementPhase.GREEN -> 2
|
val (currentStepIdx, totalSteps) = if (isV2Active) {
|
||||||
|
val idx = when (v2Aligner.phase) {
|
||||||
|
com.example.medilightv2android.managers.AlignGuide4Stage.Phase4.INITIAL_ACCUM -> 0
|
||||||
|
com.example.medilightv2android.managers.AlignGuide4Stage.Phase4.VERTICAL_CLIMB -> 1
|
||||||
|
com.example.medilightv2android.managers.AlignGuide4Stage.Phase4.CH3_STABILIZE -> 2
|
||||||
|
com.example.medilightv2android.managers.AlignGuide4Stage.Phase4.LR_BALANCE -> 3
|
||||||
|
}
|
||||||
|
idx to 4
|
||||||
|
} else {
|
||||||
|
val idx = when (placementPhase) {
|
||||||
|
PlacementPhase.VERTICAL -> 0
|
||||||
|
PlacementPhase.LATERAL -> 1
|
||||||
|
PlacementPhase.GREEN -> 2
|
||||||
|
}
|
||||||
|
idx to 3
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = "Step ${currentStepIdx + 1}/3",
|
text = "Step ${currentStepIdx + 1}/$totalSteps",
|
||||||
fontSize = 28.sp, fontWeight = FontWeight.Bold, color = Color.Black // 22 → 28
|
fontSize = 28.sp, fontWeight = FontWeight.Bold, color = Color.Black
|
||||||
)
|
)
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp),
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically) {
|
verticalAlignment = Alignment.CenterVertically) {
|
||||||
repeat(3) { i ->
|
repeat(totalSteps) { i ->
|
||||||
val isDone = i < currentStepIdx
|
val isDone = i < currentStepIdx
|
||||||
val isCurrent = i == currentStepIdx
|
val isCurrent = i == currentStepIdx
|
||||||
val dotSize = if (isCurrent) 16.dp else 10.dp // 12/8 → 16/10
|
val dotSize = if (isCurrent) 16.dp else 10.dp // 12/8 → 16/10
|
||||||
@@ -488,10 +524,10 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
// ─── (5) Hint 2 — 측정 중 방향 안내 (항상 표시, 레이아웃 안정) ───
|
// ─── (5) Hint 2 — 측정 중 방향 안내 (waitingForStart 시 공백, pubic bone 중복 X) ───
|
||||||
Text(
|
Text(
|
||||||
text = displayHint,
|
text = if (waitingForStart) AnnotatedString(" ") else displayHint,
|
||||||
fontSize = 28.sp, // 22 → 28 (태블릿)
|
fontSize = 28.sp,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
color = Color.Black,
|
color = Color.Black,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
|
|||||||
Reference in New Issue
Block a user