очки: HUD (время+заряд) сверху по центру обоих глаз, всегда поверх

Панель перенесена из view-mate (StatusPanel) как часть активити, без виджета:
StatusHud + rememberStatusHudData в :app-glasses-vm/ui. Слой добавляется в
GlassesScreen поверх when(state) внутри Binocular, поэтому виден в любом
состоянии — ожидание/нет связи/запущенный гость — и дублируется в обе
половины (левый и правый глаз).
This commit is contained in:
2026-10-01 17:50:22 +03:00
parent 65782abfdb
commit 28d1c02062
2 changed files with 137 additions and 0 deletions
@@ -46,6 +46,7 @@ fun GlassesScreen(
val state by controller.state.collectAsState()
val controllerFrameSource by controller.frameSource.collectAsState()
val cursor by controller.cursor.collectAsState()
val hud = rememberStatusHudData()
val noOp = remember { NoOpFrameSource() }
val frameSource = controllerFrameSource ?: noOp
@@ -95,6 +96,17 @@ fun GlassesScreen(
)
}
}
// HUD (время + заряд) — всегда поверх, в обеих половинах (на обоих глазах),
// сверху по центру. Виден в любом состоянии: и в ожидании связи, и поверх гостя.
Binocular {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.TopCenter,
) {
StatusHud(hud, modifier = Modifier.padding(top = 6.dp))
}
}
}
}
@@ -0,0 +1,125 @@
package pw.binom.rayneovm.glasses.ui
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
/** Данные HUD: время + заряд + цвет индикатора заряда. */
data class StatusHudData(
val time: String,
val percent: Int,
val chargeColorArgb: Int,
)
private const val CHARGE_COLOR_CHARGING = 0xFF4CAF50.toInt() // зелёный — идёт заряд
private const val CHARGE_COLOR_DISCHARGING = 0xFFFFFFFF.toInt() // белый — разряд
private const val CHARGE_COLOR_PLUGGED_NOT_CHARGING = 0xFFE53935.toInt() // красный — подключён, но не заряжается
/** Цвет индикатора заряда: не подключён=белый, CHARGING/FULL=зелёный, иначе красный. */
private fun chargeColorArgb(plugged: Boolean, status: Int): Int = when {
!plugged -> CHARGE_COLOR_DISCHARGING
status == BatteryManager.BATTERY_STATUS_CHARGING ||
status == BatteryManager.BATTERY_STATUS_FULL -> CHARGE_COLOR_CHARGING
else -> CHARGE_COLOR_PLUGGED_NOT_CHARGING
}
/**
* HUD очков: время + заряд, компактной «пилюлей» на полупрозрачном тёмном фоне.
*
* Панель вызывается внутри [pw.binom.mercury.binocular.Binocular] (см. `GlassesScreen`),
* поэтому автоматически дублируется в обеих половинах — левом и правом глазу.
*/
@Composable
fun StatusHud(data: StatusHudData, modifier: Modifier = Modifier) {
Row(
modifier = modifier
.background(Color(0x99000000))
.padding(horizontal = 10.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(data.time, color = Color.White, fontSize = 14.sp)
Spacer(Modifier.width(6.dp))
// Процент — цветом состояния заряда, без отдельного квадратика.
Text("${data.percent}%", color = Color(data.chargeColorArgb), fontSize = 14.sp)
}
}
/**
* Провайдер данных HUD: время обновляется раз в секунду, заряд — из sticky
* `ACTION_BATTERY_CHANGED`. Держится один на весь экран; обе половины читают одни данные.
*/
@Composable
fun rememberStatusHudData(): StatusHudData {
val context = LocalContext.current
var data by remember {
mutableStateOf(
StatusHudData(
time = "--:--",
percent = 0,
chargeColorArgb = CHARGE_COLOR_DISCHARGING,
)
)
}
DisposableEffect(Unit) {
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
val timeFormat = SimpleDateFormat("HH:mm", Locale.getDefault())
var percent = 0
var plugged = false
var status = BatteryManager.BATTERY_STATUS_UNKNOWN
fun readBattery() {
val intent = context.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
?: return
val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
val scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
status = intent.getIntExtra(
BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_UNKNOWN
)
plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0
percent = if (scale > 0 && level >= 0) {
(level * 100f / scale).roundToInt().coerceIn(0, 100)
} else {
level.coerceIn(0, 100)
}
}
scope.launch {
while (isActive) {
readBattery()
data = StatusHudData(timeFormat.format(Date()), percent, chargeColorArgb(plugged, status))
delay(1_000)
}
}
onDispose { scope.cancel() }
}
return data
}