очки: HUD (время+заряд) сверху по центру обоих глаз, всегда поверх
Панель перенесена из view-mate (StatusPanel) как часть активити, без виджета: StatusHud + rememberStatusHudData в :app-glasses-vm/ui. Слой добавляется в GlassesScreen поверх when(state) внутри Binocular, поэтому виден в любом состоянии — ожидание/нет связи/запущенный гость — и дублируется в обе половины (левый и правый глаз).
This commit is contained in:
@@ -46,6 +46,7 @@ fun GlassesScreen(
|
|||||||
val state by controller.state.collectAsState()
|
val state by controller.state.collectAsState()
|
||||||
val controllerFrameSource by controller.frameSource.collectAsState()
|
val controllerFrameSource by controller.frameSource.collectAsState()
|
||||||
val cursor by controller.cursor.collectAsState()
|
val cursor by controller.cursor.collectAsState()
|
||||||
|
val hud = rememberStatusHudData()
|
||||||
val noOp = remember { NoOpFrameSource() }
|
val noOp = remember { NoOpFrameSource() }
|
||||||
val frameSource = controllerFrameSource ?: noOp
|
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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user