feat: debug window

This commit is contained in:
玖叁 2024-12-25 17:27:59 +08:00
parent 4ab4487197
commit 54bd378bf7
7 changed files with 348 additions and 12 deletions

View File

@ -73,6 +73,8 @@ dependencies {
implementation(libs.androidx.datastore.preferences)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.accompanist.systemuicontroller)
implementation(libs.lifecycle.runtime.ktx)
implementation(libs.androidx.savedstate.ktx)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)

View File

@ -1,5 +1,6 @@
package icu.fur93.esp32_car.page
import DebugWindowManager
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
@ -18,6 +19,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.vectorResource
import icu.fur93.esp32_car.BuildConfig
import icu.fur93.esp32_car.R
@ -45,17 +47,33 @@ fun SettingsPage(viewModel: CarViewModel) {
}
SettingsList(viewModel)
TipText("GitHub: colour93/esp32-car-android")
TipText("Build: ${
android.text.format.DateFormat.format(
"yyyy/MM/dd HH:mm:ss",
BuildConfig.BUILD_TIME.toLong()
)
}")
TipText(
"Build: ${
android.text.format.DateFormat.format(
"yyyy/MM/dd HH:mm:ss",
BuildConfig.BUILD_TIME.toLong()
)
}"
)
}
}
@Composable
fun SettingsList(viewModel: CarViewModel) {
val context = LocalContext.current
var debugMode by remember { mutableStateOf(DebugWindowManager.isShowing()) }
val onDebugModeChange = { enable: Boolean ->
if (enable) {
DebugWindowManager.show(context, viewModel)
} else {
DebugWindowManager.hide(context)
}
debugMode = enable
}
// 使用 LazyColumn 显示设置列表
LazyColumn {
item {
SettingConnectDeviceItem(viewModel)
@ -65,7 +83,10 @@ fun SettingsList(viewModel: CarViewModel) {
headlineContent = { Text("设备名称") },
supportingContent = { Text("当前名称: Name") },
trailingContent = {
Icon(ImageVector.vectorResource(R.drawable.arrow_right_24), "设备名称")
Icon(
ImageVector.vectorResource(R.drawable.arrow_right_24),
contentDescription = "设备名称"
)
}
)
}
@ -74,10 +95,8 @@ fun SettingsList(viewModel: CarViewModel) {
headlineContent = { Text("调试模式") },
trailingContent = {
Switch(
checked = false,
onCheckedChange = {
}
checked = debugMode,
onCheckedChange = onDebugModeChange
)
}
)
@ -86,13 +105,17 @@ fun SettingsList(viewModel: CarViewModel) {
ListItem(
headlineContent = { Text("PID 参数") },
trailingContent = {
Icon(ImageVector.vectorResource(R.drawable.arrow_right_24), "PID 参数")
Icon(
ImageVector.vectorResource(R.drawable.arrow_right_24),
contentDescription = "PID 参数"
)
}
)
}
}
}
@Composable
fun SettingConnectDeviceItem(viewModel: CarViewModel) {
var showScanDialog by remember { mutableStateOf(false) }

View File

@ -3,6 +3,7 @@ package icu.fur93.esp32_car.viewmodel
import android.annotation.SuppressLint
import android.bluetooth.BluetoothDevice
import android.util.Log
import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import icu.fur93.esp32_car.const.CarCommands
@ -19,6 +20,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import android.view.WindowManager
// 用例

View File

@ -0,0 +1,198 @@
package icu.fur93.esp32_car.window
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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.graphics.vector.ImageVector
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import icu.fur93.esp32_car.R
import icu.fur93.esp32_car.entity.LogDirection
import icu.fur93.esp32_car.entity.LogEntry
import java.text.SimpleDateFormat
import java.util.Locale
@Composable
fun DebugWindow(
onDrag: (Float, Float) -> Unit,
onResize: (Float, Float) -> Unit,
logs: List<LogEntry>
) {
var logFilter by remember { mutableStateOf(LogFilter.ALL) }
Box(
modifier = Modifier
.width(280.dp)
.height(200.dp)
.background(
color = Color.Black.copy(alpha = 0.8f),
shape = RoundedCornerShape(8.dp)
)
) {
Column {
// 顶部拖动条
Box(
modifier = Modifier
.fillMaxWidth()
.height(32.dp)
.background(
color = Color.DarkGray,
shape = RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp)
)
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
change.consume()
onDrag(dragAmount.x, dragAmount.y)
}
}
) {
Text(
"Debug Window",
color = Color.White,
style = MaterialTheme.typography.titleSmall,
modifier = Modifier
.padding(horizontal = 12.dp, vertical = 6.dp)
.align(Alignment.CenterStart)
)
}
// 过滤选项
Row(
modifier = Modifier
.fillMaxWidth()
.background(Color.DarkGray.copy(alpha = 0.3f))
.padding(horizontal = 8.dp, vertical = 4.dp),
horizontalArrangement = Arrangement.SpaceEvenly
) {
FilterChip(
selected = logFilter == LogFilter.ALL,
onClick = { logFilter = LogFilter.ALL },
label = { Text("全部", color = Color.White) },
colors = FilterChipDefaults.filterChipColors(
selectedContainerColor = MaterialTheme.colorScheme.primary
)
)
FilterChip(
selected = logFilter == LogFilter.SEND,
onClick = { logFilter = LogFilter.SEND },
label = { Text("发送", color = Color.White) },
colors = FilterChipDefaults.filterChipColors(
selectedContainerColor = MaterialTheme.colorScheme.primary
)
)
FilterChip(
selected = logFilter == LogFilter.RECEIVE,
onClick = { logFilter = LogFilter.RECEIVE },
label = { Text("接收", color = Color.White) },
colors = FilterChipDefaults.filterChipColors(
selectedContainerColor = MaterialTheme.colorScheme.primary
)
)
}
// 日志列表
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 8.dp),
reverseLayout = true // 最新的日志显示在顶部
) {
items(
logs.filter {
when (logFilter) {
LogFilter.ALL -> true
LogFilter.SEND -> it.direction == LogDirection.SEND
LogFilter.RECEIVE -> it.direction == LogDirection.RECEIVE
}
}.reversed()
) { log ->
LogItem(log)
}
}
}
// 右下角调整大小的手柄
Box(
modifier = Modifier
.size(24.dp)
.align(Alignment.BottomEnd)
.padding(4.dp)
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
change.consume()
onResize(dragAmount.x, dragAmount.y)
}
}
) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.resize_24),
contentDescription = "Resize",
tint = Color.White.copy(alpha = 0.6f),
modifier = Modifier.size(20.dp)
)
}
}
}
@Composable
private fun LogItem(log: LogEntry) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 2.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
// 方向指示
Text(
text = if (log.direction == LogDirection.SEND) "" else "",
color = if (log.direction == LogDirection.SEND)
Color(0xFF4CAF50) else Color(0xFF2196F3),
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.width(16.dp)
)
// 日志内容
Text(
text = log.data,
color = Color.White,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.weight(1f)
)
// 时间戳
Text(
text = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
.format(log.timestamp),
color = Color.Gray,
style = MaterialTheme.typography.bodySmall
)
}
}
private enum class LogFilter {
ALL, SEND, RECEIVE
}

View File

@ -0,0 +1,98 @@
import android.content.Context
import android.graphics.PixelFormat
import android.view.Gravity
import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.setViewTreeLifecycleOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import icu.fur93.esp32_car.window.DebugWindow
import icu.fur93.esp32_car.viewmodel.CarViewModel
object DebugWindowManager {
private var debugWindow: ComposeView? = null
private var layoutParams: WindowManager.LayoutParams? = null
// 添加最小尺寸限制
private const val MIN_WIDTH_DP = 200
private const val MIN_HEIGHT_DP = 150
fun show(context: Context, viewModel: CarViewModel) {
if (debugWindow != null) return
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
val activity = context as? ComponentActivity ?: return
val density = context.resources.displayMetrics.density
val width = (280 * density).toInt()
val height = (200 * density).toInt()
val params = WindowManager.LayoutParams(
width,
height,
WindowManager.LayoutParams.TYPE_APPLICATION,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT
).apply {
gravity = Gravity.TOP or Gravity.START
x = 100
y = 100
}
val window = ComposeView(context).apply {
setViewTreeLifecycleOwner(activity)
setViewTreeSavedStateRegistryOwner(activity)
setContent {
val logs by viewModel.logs.collectAsState()
DebugWindow(
onDrag = { offsetX, offsetY ->
params.x += offsetX.toInt()
params.y += offsetY.toInt()
windowManager.updateViewLayout(this, params)
},
onResize = { offsetX, offsetY ->
val density = context.resources.displayMetrics.density
val minWidth = (MIN_WIDTH_DP * density).toInt()
val minHeight = (MIN_HEIGHT_DP * density).toInt()
params.width = (params.width + offsetX.toInt()).coerceAtLeast(minWidth)
params.height = (params.height + offsetY.toInt()).coerceAtLeast(minHeight)
windowManager.updateViewLayout(this, params)
},
logs = logs
)
}
}
windowManager.addView(window, params)
debugWindow = window
layoutParams = params
}
fun hide(context: Context) {
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
debugWindow?.let {
windowManager.removeView(it)
debugWindow = null
layoutParams = null
}
}
fun isShowing(): Boolean {
return debugWindow != null
}
fun updatePosition(context: Context, x: Int, y: Int) {
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
layoutParams?.let { params ->
params.x = x
params.y = y
debugWindow?.let {
windowManager.updateViewLayout(it, params)
}
}
}
}

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M22,22H20V20H22V22M22,18H20V16H22V18M18,22H16V20H18V22M18,18H16V16H18V18M14,22H12V20H14V22M22,14H20V12H22V14Z"/>
</vector>

View File

@ -15,6 +15,8 @@ datastorePreferencesCoreJvm = "1.1.1"
datastorePreferences = "1.1.1"
lifecycleViewmodelCompose = "2.9.0-alpha08"
accompanistSystemuicontroller = "0.36.0"
lifecycleRuntimeKtxVersion = "2.7.0"
savedstateKtx = "1.2.1"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@ -38,6 +40,8 @@ androidx-datastore-preferences-core-jvm = { group = "androidx.datastore", name =
androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastorePreferences" }
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version = "2.7.0" }
accompanist-systemuicontroller = { group = "com.google.accompanist", name = "accompanist-systemuicontroller", version.ref = "accompanistSystemuicontroller" }
lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtxVersion" }
androidx-savedstate-ktx = { group = "androidx.savedstate", name = "savedstate-ktx", version.ref = "savedstateKtx" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }