feat(posture): Walking detector (flowchart Path B) + 2단계 알람
새 flowchart 의 walking 검출 추가:
1) Posture enum: UNKNOWN/LYING/NON_LYING + **WALKING** (4-state)
2) WalkingDetector.kt 신규 (flowchart Path B 구현)
- Sliding window (default 1.5초) buffer 에 gyro magnitude 누적
- 매 cycle 마다 wmean (이동평균) + wstd (이동표준편차) 계산
- State machine:
IDLE → WALKING:
|pitch| > 45° AND wmean > 4 AND wstd > 2
→ 1초 이상 연속 만족 시 전이
WALKING → IDLE:
|pitch| > 45° AND wmean > 2
→ 미달 후 2초 grace 지나면 전이
3) GreenZoneConstants 에 walking 임계 7개 추가 (모두 dev 조정 가능):
- walkingMinPitchDeg=45, walkingStartWmean=4, walkingStartWstd=2,
walkingStartHoldMs=1000, walkingKeepWmean=2, walkingKeepHoldMs=2000,
walkingWindowMs=1500
4) PiezoMonitoring:
- walkingDetector 인스턴스
- imuCollector.onComplete: classifier + detector 동시 호출,
composite posture (WALKING > NON_LYING > LYING) 결정
- NON_WALKING → WALKING 전이 시 critical 알람 trigger
5) UI:
- 상단 status bar: Walking 아이콘 (DirectionsWalk, 빨강) 추가
- 기존 wakeup 팝업 색: 빨강 → 주황 (warning level)
- 신규 walking 팝업: 빨강 + DirectionsWalk + "환자가 걷고 있습니다 — fall risk"
6) Dev settings: walking 임계 슬라이더 4개 추가
(Min Pitch / Start Wmean / Start Wstd / Keep Wmean)
sliding window / hold 시간은 default 유지 (필요 시 추가)
2단계 알람 체계 (의료적):
- 주황 (warning): LYING N초 → NON_LYING/WALKING 전이 (일어남 감지)
- 빨강 (critical): NON_WALKING → WALKING 전이 (보행 시작 — fall risk 최고)
Tag phase2-pre-walking (28a8769) 이전 상태로 복원 가능.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,22 @@ object GreenZoneConstants {
|
||||
/** Posture polling 간격 (ms). PiezoMonitoring 에서 mtb 보내는 주기. dev 모드에서 조정 가능. */
|
||||
@Volatile var posturePollIntervalMs: Long = 1000L
|
||||
|
||||
// ── Walking detector 임계값 (flowchart 4-2 / 5-2) ─────────────────
|
||||
/** Walking 인정 최소 pitch (deg). |pitch| > 이 값 이라야 walking 후보. */
|
||||
@Volatile var walkingMinPitchDeg: Float = 45f
|
||||
/** Walking 시작 — gyro 이동평균 (wmean) 임계 (dps). */
|
||||
@Volatile var walkingStartWmean: Float = 4f
|
||||
/** Walking 시작 — gyro 이동표준편차 (wstd) 임계 (dps). */
|
||||
@Volatile var walkingStartWstd: Float = 2f
|
||||
/** Walking 시작 — 두 임계 모두 만족한 채 지속해야 하는 최소 시간 (ms). */
|
||||
@Volatile var walkingStartHoldMs: Long = 1000L
|
||||
/** Walking 유지 — gyro wmean 임계 (dps). 시작보다 낮음. */
|
||||
@Volatile var walkingKeepWmean: Float = 2f
|
||||
/** Walking 유지 — wmean 임계 미달 후 종료까지 grace 시간 (ms). 미만 지속이 이 값 넘으면 walking 종료. */
|
||||
@Volatile var walkingKeepHoldMs: Long = 2000L
|
||||
/** Walking detector sliding window (ms). 이 시간 분량 sample 평균/표준편차. */
|
||||
@Volatile var walkingWindowMs: Long = 1500L
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 신호 범위
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
@@ -20,7 +20,7 @@ import kotlin.math.abs
|
||||
import kotlin.math.atan
|
||||
import kotlin.math.sqrt
|
||||
|
||||
enum class Posture { UNKNOWN, LYING, NON_LYING }
|
||||
enum class Posture { UNKNOWN, LYING, NON_LYING, WALKING }
|
||||
|
||||
object ImuPostureClassifier {
|
||||
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* WalkingDetector — flowchart Path B (4-2 / 5-2) 구현.
|
||||
*
|
||||
* 알고리즘:
|
||||
* 1. Sliding window (walkingWindowMs, default 1.5초) 동안 IMU gyro sample 누적
|
||||
* 2. window 의 gyro magnitude 평균 (wmean) 과 표준편차 (wstd) 계산
|
||||
* 3. 상태 머신:
|
||||
* IDLE (not walking):
|
||||
* 조건: |pitch| > walkingMinPitchDeg
|
||||
* AND wmean > walkingStartWmean (default 4)
|
||||
* AND wstd > walkingStartWstd (default 2)
|
||||
* → 조건 만족이 walkingStartHoldMs (default 1초) 이상 지속 → WALKING
|
||||
* WALKING:
|
||||
* 조건: |pitch| > walkingMinPitchDeg
|
||||
* AND wmean > walkingKeepWmean (default 2)
|
||||
* → 조건 미달이 walkingKeepHoldMs (default 2초) 이상 지속 → IDLE
|
||||
*
|
||||
* 입력: 매 mtb cycle (≈1초 주기) 마다 IMU sample 리스트 + 현재 pitch + 시각
|
||||
* 출력: 현재 walking 상태 (Boolean) + 진단 정보
|
||||
*/
|
||||
package com.example.medilightv2android.managers
|
||||
|
||||
import com.example.medilightv2android.ble.ImuSample
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.sqrt
|
||||
|
||||
class WalkingDetector {
|
||||
|
||||
/** 진단 결과. UI/log 용. */
|
||||
data class Snapshot(
|
||||
val isWalking: Boolean,
|
||||
val wmean: Float,
|
||||
val wstd: Float,
|
||||
val pitchOk: Boolean,
|
||||
val samplesInWindow: Int,
|
||||
val reason: String,
|
||||
)
|
||||
|
||||
private data class TsGyro(val tsMs: Long, val magDps: Float)
|
||||
|
||||
private val buffer = ArrayDeque<TsGyro>()
|
||||
private var walking = false
|
||||
/** IDLE 에서 시작 조건 처음 만족된 시각. null 이면 미만족 / 끊김. */
|
||||
private var startCondSinceMs: Long? = null
|
||||
/** WALKING 에서 유지 조건 마지막 만족 시각. */
|
||||
private var keepCondLastOkMs: Long? = null
|
||||
|
||||
val isWalking: Boolean get() = walking
|
||||
|
||||
fun reset() {
|
||||
buffer.clear()
|
||||
walking = false
|
||||
startCondSinceMs = null
|
||||
keepCondLastOkMs = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 매 cycle 마다 호출. samples = 이번 cycle 에 들어온 IMU sample 리스트.
|
||||
* pitchDeg = 같은 cycle 의 pitch (ImuPostureClassifier 가 계산한 값).
|
||||
*/
|
||||
fun update(samples: List<ImuSample>, pitchDeg: Float, nowMs: Long): Snapshot {
|
||||
// 1) sample buffer 에 추가 (timestamp 단순 분배 — cycle 내 균등 간격 가정)
|
||||
val windowMs = GreenZoneConstants.walkingWindowMs
|
||||
if (samples.isNotEmpty()) {
|
||||
// cycle 내 각 sample 의 timestamp 를 nowMs 기준으로 역산 (≈ ICM ODR 50Hz → 20ms 간격)
|
||||
val sampleGapMs = 20L
|
||||
val startTs = nowMs - (samples.size - 1) * sampleGapMs
|
||||
for ((i, s) in samples.withIndex()) {
|
||||
val mag = sqrt(s.gx * s.gx + s.gy * s.gy + s.gz * s.gz)
|
||||
buffer.addLast(TsGyro(startTs + i * sampleGapMs, mag))
|
||||
}
|
||||
}
|
||||
// 2) window 밖 sample 제거
|
||||
val cutoff = nowMs - windowMs
|
||||
while (buffer.isNotEmpty() && buffer.first().tsMs < cutoff) buffer.removeFirst()
|
||||
|
||||
// 3) wmean, wstd 계산
|
||||
val n = buffer.size
|
||||
if (n < 2) {
|
||||
return Snapshot(walking, 0f, 0f, false, n, "buffer too small")
|
||||
}
|
||||
var sum = 0f
|
||||
for (b in buffer) sum += b.magDps
|
||||
val wmean = sum / n
|
||||
var sqSum = 0f
|
||||
for (b in buffer) {
|
||||
val d = b.magDps - wmean
|
||||
sqSum += d * d
|
||||
}
|
||||
val wstd = sqrt(sqSum / n)
|
||||
|
||||
// 4) state machine
|
||||
val minPitch = GreenZoneConstants.walkingMinPitchDeg
|
||||
val pitchOk = abs(pitchDeg) > minPitch
|
||||
|
||||
if (!walking) {
|
||||
// IDLE → WALKING 검사 (4-2)
|
||||
val cond = pitchOk &&
|
||||
wmean > GreenZoneConstants.walkingStartWmean &&
|
||||
wstd > GreenZoneConstants.walkingStartWstd
|
||||
if (cond) {
|
||||
if (startCondSinceMs == null) startCondSinceMs = nowMs
|
||||
val held = nowMs - startCondSinceMs!!
|
||||
if (held >= GreenZoneConstants.walkingStartHoldMs) {
|
||||
walking = true
|
||||
keepCondLastOkMs = nowMs
|
||||
return Snapshot(true, wmean, wstd, pitchOk, n,
|
||||
"WALKING start (held ${held}ms)")
|
||||
}
|
||||
return Snapshot(false, wmean, wstd, pitchOk, n,
|
||||
"walking start cond met for ${held}ms")
|
||||
} else {
|
||||
startCondSinceMs = null
|
||||
return Snapshot(false, wmean, wstd, pitchOk, n,
|
||||
"idle (pitchOk=$pitchOk wmean=${"%.1f".format(wmean)} wstd=${"%.1f".format(wstd)})")
|
||||
}
|
||||
} else {
|
||||
// WALKING → IDLE 검사 (5-2)
|
||||
val cond = pitchOk && wmean > GreenZoneConstants.walkingKeepWmean
|
||||
if (cond) {
|
||||
keepCondLastOkMs = nowMs
|
||||
return Snapshot(true, wmean, wstd, pitchOk, n,
|
||||
"WALKING keep (wmean=${"%.1f".format(wmean)})")
|
||||
} else {
|
||||
// 조건 미달 — grace 시간 체크
|
||||
val sinceOk = nowMs - (keepCondLastOkMs ?: nowMs)
|
||||
if (sinceOk >= GreenZoneConstants.walkingKeepHoldMs) {
|
||||
walking = false
|
||||
startCondSinceMs = null
|
||||
keepCondLastOkMs = null
|
||||
return Snapshot(false, wmean, wstd, pitchOk, n,
|
||||
"WALKING end (grace ${sinceOk}ms exceeded)")
|
||||
}
|
||||
return Snapshot(true, wmean, wstd, pitchOk, n,
|
||||
"WALKING grace (${sinceOk}ms / ${GreenZoneConstants.walkingKeepHoldMs}ms)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+133
-10
@@ -130,10 +130,12 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
var lastAllWalls by remember { mutableStateOf<List<Pair<Int, Int>?>>(List(6) { null }) }
|
||||
val analyzer = remember { PiezoEchoAnalyzer.shared }
|
||||
|
||||
// Posture (flowchart-based, IMU polling — Phase 1) + Wakeup detector (Phase 2)
|
||||
// Posture (flowchart-based, IMU polling — Phase 1) + Wakeup/Walking detector (Phase 2)
|
||||
var currentPosture by remember { mutableStateOf(com.example.medilightv2android.managers.Posture.UNKNOWN) }
|
||||
var showWakeupAlarm by remember { mutableStateOf(false) }
|
||||
var showWakeupAlarm by remember { mutableStateOf(false) } // 주황 — LYING → NON_LYING/WALKING
|
||||
var wakeupAlarmElapsedSec by remember { mutableStateOf(0L) }
|
||||
var showWalkingAlarm by remember { mutableStateOf(false) } // 빨강 — NON_LYING → WALKING
|
||||
val walkingDetector = remember { com.example.medilightv2android.managers.WalkingDetector() }
|
||||
val wakeupDetector = remember {
|
||||
com.example.medilightv2android.managers.WakeupDetector(
|
||||
lyingDurationSec = com.example.medilightv2android.services.WakeupAlarmPrefs.lyingDurationSec(context)
|
||||
@@ -146,10 +148,12 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Wakeup alarm 빨간 팝업 — 사용자 dismiss 필요 (자동 닫힘 X)
|
||||
// 직전 walking 상태 추적 — false → true 전이 시 critical 알람
|
||||
var prevIsWalking by remember { mutableStateOf(false) }
|
||||
// Wakeup alarm (warning, 주황) — LYING → NON_LYING/WALKING 전이
|
||||
if (showWakeupAlarm) {
|
||||
androidx.compose.ui.window.Dialog(
|
||||
onDismissRequest = { /* not dismissible by outside tap */ },
|
||||
onDismissRequest = { },
|
||||
properties = androidx.compose.ui.window.DialogProperties(
|
||||
dismissOnBackPress = false, dismissOnClickOutside = false
|
||||
)
|
||||
@@ -158,7 +162,7 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.92f)
|
||||
.clip(RoundedCornerShape(24.dp))
|
||||
.background(Color(0xFFD32F2F))
|
||||
.background(Color(0xFFFF9800)) // Material Orange 500
|
||||
.padding(horizontal = 28.dp, vertical = 36.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
@@ -181,6 +185,54 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Button(
|
||||
onClick = { showWakeupAlarm = false },
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.White, contentColor = Color(0xFFFF9800)
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth().height(56.dp),
|
||||
shape = RoundedCornerShape(14.dp)
|
||||
) {
|
||||
Text("확인", fontSize = 20.sp, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Walking alarm (critical, 빨강) — NON_LYING/LYING → WALKING 전이 (보행 시작)
|
||||
if (showWalkingAlarm) {
|
||||
androidx.compose.ui.window.Dialog(
|
||||
onDismissRequest = { },
|
||||
properties = androidx.compose.ui.window.DialogProperties(
|
||||
dismissOnBackPress = false, dismissOnClickOutside = false
|
||||
)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.94f)
|
||||
.clip(RoundedCornerShape(24.dp))
|
||||
.background(Color(0xFFD32F2F))
|
||||
.padding(horizontal = 28.dp, vertical = 36.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Icon(Icons.Default.DirectionsWalk, contentDescription = null,
|
||||
tint = Color.White, modifier = Modifier.size(64.dp))
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text("환자가 걷고 있습니다", fontSize = 30.sp,
|
||||
fontWeight = FontWeight.ExtraBold, color = Color.White,
|
||||
textAlign = TextAlign.Center)
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
Text("Patient is walking — fall risk",
|
||||
fontSize = 16.sp, fontWeight = FontWeight.Medium,
|
||||
color = Color.White.copy(alpha = 0.92f),
|
||||
textAlign = TextAlign.Center)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text("즉시 환자 상태 확인 필요",
|
||||
fontSize = 14.sp, color = Color.White.copy(alpha = 0.9f),
|
||||
textAlign = TextAlign.Center)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Button(
|
||||
onClick = { showWalkingAlarm = false },
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.White, contentColor = Color(0xFFD32F2F)
|
||||
),
|
||||
@@ -668,16 +720,39 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
var unknownStreak = 0
|
||||
bleManager.imuCollector.onComplete = { samples ->
|
||||
val r = com.example.medilightv2android.managers.ImuPostureClassifier.classify(samples)
|
||||
if (r.posture != com.example.medilightv2android.managers.Posture.UNKNOWN) {
|
||||
currentPosture = r.posture
|
||||
wakeupDetector.update(r.posture)
|
||||
val nowMs = System.currentTimeMillis()
|
||||
// Walking detector — pitch 가 valid 일 때만 의미. acc gate fail 시 그대로 update (wmean/wstd 계산 + 시간 grace 처리).
|
||||
val walkSnap = walkingDetector.update(samples, r.pitchDeg, nowMs)
|
||||
val isWalkingNow = walkSnap.isWalking
|
||||
|
||||
// Composite posture — WALKING 이 NON_LYING 보다 우선
|
||||
val composite = when {
|
||||
r.posture == com.example.medilightv2android.managers.Posture.UNKNOWN -> com.example.medilightv2android.managers.Posture.UNKNOWN
|
||||
isWalkingNow -> com.example.medilightv2android.managers.Posture.WALKING
|
||||
else -> r.posture // LYING or NON_LYING
|
||||
}
|
||||
|
||||
if (composite != com.example.medilightv2android.managers.Posture.UNKNOWN) {
|
||||
currentPosture = composite
|
||||
wakeupDetector.update(composite)
|
||||
unknownStreak = 0
|
||||
} else {
|
||||
unknownStreak++
|
||||
// 3회 연속 UNKNOWN 이면 화면에 표시 (silent stuck 방지)
|
||||
if (unknownStreak >= 3) currentPosture = com.example.medilightv2android.managers.Posture.UNKNOWN
|
||||
}
|
||||
Log.d("ImuDbg", "POSTURE ${r.posture} pitch=${"%.1f".format(r.pitchDeg)}° accMag=${"%.2f".format(r.accMag)}g gyro=${"%.1f".format(r.gyroMagDps)}dps (${r.reason}) samples=${samples.size} unknownStreak=$unknownStreak")
|
||||
|
||||
// Walking 시작 전이 (NON_WALKING → WALKING) — critical 알람 (alarm enabled 시)
|
||||
if (isWalkingNow && !prevIsWalking) {
|
||||
if (com.example.medilightv2android.services.WakeupAlarmPrefs.isEnabled(context)) {
|
||||
showWalkingAlarm = true
|
||||
}
|
||||
}
|
||||
prevIsWalking = isWalkingNow
|
||||
|
||||
Log.d("ImuDbg", "POSTURE ${r.posture}→$composite pitch=${"%.1f".format(r.pitchDeg)}° " +
|
||||
"accMag=${"%.2f".format(r.accMag)}g gyro=${"%.1f".format(r.gyroMagDps)}dps | " +
|
||||
"WALK wmean=${"%.1f".format(walkSnap.wmean)} wstd=${"%.1f".format(walkSnap.wstd)} " +
|
||||
"n=${walkSnap.samplesInWindow} (${walkSnap.reason})")
|
||||
}
|
||||
bleManager.imuCollector.onLog = { msg ->
|
||||
Log.d("ImuDbg", "imuCollector log: $msg")
|
||||
@@ -1024,16 +1099,19 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
val postureIcon = when (currentPosture) {
|
||||
com.example.medilightv2android.managers.Posture.LYING -> Icons.Default.Hotel
|
||||
com.example.medilightv2android.managers.Posture.NON_LYING -> Icons.Default.AccessibilityNew
|
||||
com.example.medilightv2android.managers.Posture.WALKING -> Icons.Default.DirectionsWalk
|
||||
com.example.medilightv2android.managers.Posture.UNKNOWN -> Icons.Default.HelpOutline
|
||||
}
|
||||
val postureTint = when (currentPosture) {
|
||||
com.example.medilightv2android.managers.Posture.LYING -> Color(0xFF2196F3)
|
||||
com.example.medilightv2android.managers.Posture.NON_LYING -> Color(0xFF4CAF50)
|
||||
com.example.medilightv2android.managers.Posture.WALKING -> Color(0xFFD32F2F)
|
||||
com.example.medilightv2android.managers.Posture.UNKNOWN -> Color.Gray.copy(alpha = 0.5f)
|
||||
}
|
||||
val postureLabel = when (currentPosture) {
|
||||
com.example.medilightv2android.managers.Posture.LYING -> "Lying"
|
||||
com.example.medilightv2android.managers.Posture.NON_LYING -> "Upright"
|
||||
com.example.medilightv2android.managers.Posture.WALKING -> "Walking"
|
||||
com.example.medilightv2android.managers.Posture.UNKNOWN -> "—"
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
@@ -1876,6 +1954,51 @@ fun PiezoMonitoringView(appState: AppState) {
|
||||
steps = 0,
|
||||
colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary)
|
||||
)
|
||||
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = dividerPadding))
|
||||
|
||||
// Walking detector 임계값 (4개)
|
||||
Text("Walking Thresholds", fontSize = labelSize, fontWeight = FontWeight.SemiBold, color = MlPrimary)
|
||||
|
||||
var walkPitchVal by remember { mutableStateOf(GreenZoneConstants.walkingMinPitchDeg) }
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(" Min Pitch", fontSize = 12.sp)
|
||||
Spacer(Modifier.weight(1f))
|
||||
Text("${walkPitchVal.toInt()}°", fontSize = 12.sp, fontWeight = FontWeight.Bold, color = MlPrimary)
|
||||
}
|
||||
Slider(value = walkPitchVal, onValueChange = {
|
||||
walkPitchVal = it; GreenZoneConstants.walkingMinPitchDeg = it
|
||||
}, valueRange = 20f..70f, colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary))
|
||||
|
||||
var walkStartWmean by remember { mutableStateOf(GreenZoneConstants.walkingStartWmean) }
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(" Start Wmean", fontSize = 12.sp)
|
||||
Spacer(Modifier.weight(1f))
|
||||
Text("%.1f".format(walkStartWmean), fontSize = 12.sp, fontWeight = FontWeight.Bold, color = MlPrimary)
|
||||
}
|
||||
Slider(value = walkStartWmean, onValueChange = {
|
||||
walkStartWmean = it; GreenZoneConstants.walkingStartWmean = it
|
||||
}, valueRange = 1f..20f, colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary))
|
||||
|
||||
var walkStartWstd by remember { mutableStateOf(GreenZoneConstants.walkingStartWstd) }
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(" Start Wstd", fontSize = 12.sp)
|
||||
Spacer(Modifier.weight(1f))
|
||||
Text("%.1f".format(walkStartWstd), fontSize = 12.sp, fontWeight = FontWeight.Bold, color = MlPrimary)
|
||||
}
|
||||
Slider(value = walkStartWstd, onValueChange = {
|
||||
walkStartWstd = it; GreenZoneConstants.walkingStartWstd = it
|
||||
}, valueRange = 0.5f..10f, colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary))
|
||||
|
||||
var walkKeepWmean by remember { mutableStateOf(GreenZoneConstants.walkingKeepWmean) }
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(" Keep Wmean", fontSize = 12.sp)
|
||||
Spacer(Modifier.weight(1f))
|
||||
Text("%.1f".format(walkKeepWmean), fontSize = 12.sp, fontWeight = FontWeight.Bold, color = MlPrimary)
|
||||
}
|
||||
Slider(value = walkKeepWmean, onValueChange = {
|
||||
walkKeepWmean = it; GreenZoneConstants.walkingKeepWmean = it
|
||||
}, valueRange = 0.5f..10f, colors = SliderDefaults.colors(thumbColor = MlPrimary, activeTrackColor = MlPrimary))
|
||||
}
|
||||
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = dividerPadding))
|
||||
|
||||
Reference in New Issue
Block a user