feat: add animation effects and visual feedback
This commit is contained in:
@@ -20,20 +20,31 @@ import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.filled.LocationOn
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -966,55 +977,165 @@ fun ConnectionDialog(
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text("连接设备") },
|
||||
title = {
|
||||
Text(
|
||||
text = "蓝牙设备",
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Button(
|
||||
onClick = onScan,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = if (isScanning) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary
|
||||
)
|
||||
) {
|
||||
Text(if (isScanning) "停止扫描" else "扫描设备")
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
if (isScanning) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(16.dp),
|
||||
strokeWidth = 2.dp,
|
||||
color = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Search,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = if (isScanning) "扫描中..." else "扫描设备",
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
if (isScanning) {
|
||||
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
if (devices.isEmpty()) {
|
||||
Text("未找到设备")
|
||||
} else {
|
||||
Column {
|
||||
devices.forEach { device ->
|
||||
if (devices.isNotEmpty()) {
|
||||
Text(
|
||||
text = "发现 ${devices.size} 个设备",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.heightIn(max = 200.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
items(devices) { device ->
|
||||
var isPressed by remember { mutableStateOf(false) }
|
||||
|
||||
val cardScale by animateFloatAsState(
|
||||
targetValue = if (isPressed) 0.98f else 1f,
|
||||
animationSpec = tween(
|
||||
durationMillis = 120,
|
||||
easing = LinearEasing
|
||||
)
|
||||
)
|
||||
|
||||
LaunchedEffect(isPressed) {
|
||||
if (isPressed) {
|
||||
delay(100)
|
||||
isPressed = false
|
||||
}
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp)
|
||||
.clickable { onConnect(device) }
|
||||
.graphicsLayer {
|
||||
scaleX = cardScale
|
||||
scaleY = cardScale
|
||||
},
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(8.dp)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = true)
|
||||
) {
|
||||
isPressed = true
|
||||
onConnect(device)
|
||||
}
|
||||
.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = device.name ?: "未知设备",
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
text = device.address,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bluetooth,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
text = device.name ?: "未知设备",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Text(
|
||||
text = device.address,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!isScanning) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 32.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.BluetoothSearching,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(48.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = "未发现设备",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "请确保设备已开启并处于可发现状态",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text("取消")
|
||||
Text("关闭")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
package org.noxylva.lbjconsole.ui.components
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Clear
|
||||
import androidx.compose.material.icons.filled.FilterList
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -21,6 +29,7 @@ import org.noxylva.lbjconsole.model.TrainRecord
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun TrainRecordsList(
|
||||
records: List<TrainRecord>,
|
||||
@@ -41,19 +50,52 @@ fun TrainRecordsList(
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(vertical = 4.dp, horizontal = 8.dp)
|
||||
contentPadding = PaddingValues(vertical = 4.dp, horizontal = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
items(records) { record ->
|
||||
itemsIndexed(records, key = { _, record -> record.uniqueId }) { index, record ->
|
||||
val animationDelay = (index * 30).coerceAtMost(200)
|
||||
var isPressed by remember { mutableStateOf(false) }
|
||||
|
||||
val scale by animateFloatAsState(
|
||||
targetValue = if (isPressed) 0.98f else 1f,
|
||||
animationSpec = tween(durationMillis = 120)
|
||||
)
|
||||
|
||||
val elevation by animateDpAsState(
|
||||
targetValue = if (isPressed) 6.dp else 2.dp,
|
||||
animationSpec = tween(durationMillis = 120)
|
||||
)
|
||||
|
||||
LaunchedEffect(isPressed) {
|
||||
if (isPressed) {
|
||||
kotlinx.coroutines.delay(150)
|
||||
isPressed = false
|
||||
}
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 2.dp)
|
||||
.clickable { onRecordClick(record) },
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
|
||||
.graphicsLayer {
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
}
|
||||
.animateItemPlacement(
|
||||
animationSpec = tween(durationMillis = 200)
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = elevation)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = true)
|
||||
) {
|
||||
isPressed = true
|
||||
onRecordClick(record)
|
||||
}
|
||||
.padding(8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
|
||||
@@ -9,6 +9,9 @@ import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
@@ -19,6 +22,7 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -53,14 +57,41 @@ fun TrainRecordItem(
|
||||
onToggleSelection: (TrainRecord) -> Unit,
|
||||
onLongClick: (TrainRecord) -> Unit
|
||||
) {
|
||||
val cardColor = when {
|
||||
isSelected -> MaterialTheme.colorScheme.primaryContainer
|
||||
else -> MaterialTheme.colorScheme.surface
|
||||
}
|
||||
val recordId = record.uniqueId
|
||||
val isExpanded = expandedStatesMap[recordId] == true
|
||||
|
||||
val cardColor by animateColorAsState(
|
||||
targetValue = when {
|
||||
isSelected -> MaterialTheme.colorScheme.primaryContainer
|
||||
else -> MaterialTheme.colorScheme.surface
|
||||
},
|
||||
animationSpec = tween(durationMillis = 200),
|
||||
label = "cardColor"
|
||||
)
|
||||
|
||||
val cardScale by animateFloatAsState(
|
||||
targetValue = if (isSelected) 0.98f else 1f,
|
||||
animationSpec = tween(durationMillis = 150),
|
||||
label = "cardScale"
|
||||
)
|
||||
|
||||
val cardElevation by animateDpAsState(
|
||||
targetValue = if (isSelected) 6.dp else 2.dp,
|
||||
animationSpec = tween(durationMillis = 200),
|
||||
label = "cardElevation"
|
||||
)
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.graphicsLayer {
|
||||
scaleX = cardScale
|
||||
scaleY = cardScale
|
||||
}
|
||||
.animateContentSize(
|
||||
animationSpec = tween(durationMillis = 150, easing = LinearEasing)
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = cardColor
|
||||
),
|
||||
@@ -260,106 +291,112 @@ fun TrainRecordItem(
|
||||
}
|
||||
}
|
||||
|
||||
if (isExpanded) {
|
||||
val coordinates = remember { record.getCoordinates() }
|
||||
AnimatedVisibility(
|
||||
visible = isExpanded,
|
||||
enter = expandVertically(animationSpec = tween(durationMillis = 300)) + fadeIn(animationSpec = tween(durationMillis = 300)),
|
||||
exit = shrinkVertically(animationSpec = tween(durationMillis = 300)) + fadeOut(animationSpec = tween(durationMillis = 300))
|
||||
) {
|
||||
Column {
|
||||
val coordinates = remember { record.getCoordinates() }
|
||||
|
||||
if (coordinates != null) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
if (coordinates != null) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
if (coordinates != null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(220.dp)
|
||||
.padding(vertical = 4.dp)
|
||||
.clip(RoundedCornerShape(8.dp)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
AndroidView(
|
||||
modifier = Modifier.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {},
|
||||
factory = { context ->
|
||||
MapView(context).apply {
|
||||
setTileSource(TileSourceFactory.MAPNIK)
|
||||
setMultiTouchControls(true)
|
||||
zoomController.setVisibility(org.osmdroid.views.CustomZoomButtonsController.Visibility.NEVER)
|
||||
isHorizontalMapRepetitionEnabled = false
|
||||
isVerticalMapRepetitionEnabled = false
|
||||
setHasTransientState(true)
|
||||
setOnTouchListener { v, event ->
|
||||
v.parent?.requestDisallowInterceptTouchEvent(true)
|
||||
false
|
||||
}
|
||||
controller.setZoom(10.0)
|
||||
controller.setCenter(coordinates)
|
||||
this.isTilesScaledToDpi = true
|
||||
this.setUseDataConnection(true)
|
||||
if (coordinates != null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(220.dp)
|
||||
.padding(vertical = 4.dp)
|
||||
.clip(RoundedCornerShape(8.dp)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
AndroidView(
|
||||
modifier = Modifier.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {},
|
||||
factory = { context ->
|
||||
MapView(context).apply {
|
||||
setTileSource(TileSourceFactory.MAPNIK)
|
||||
setMultiTouchControls(true)
|
||||
zoomController.setVisibility(org.osmdroid.views.CustomZoomButtonsController.Visibility.NEVER)
|
||||
isHorizontalMapRepetitionEnabled = false
|
||||
isVerticalMapRepetitionEnabled = false
|
||||
setHasTransientState(true)
|
||||
setOnTouchListener { v, event ->
|
||||
v.parent?.requestDisallowInterceptTouchEvent(true)
|
||||
false
|
||||
}
|
||||
controller.setZoom(10.0)
|
||||
controller.setCenter(coordinates)
|
||||
this.isTilesScaledToDpi = true
|
||||
this.setUseDataConnection(true)
|
||||
|
||||
try {
|
||||
val railwayTileSource = XYTileSource(
|
||||
"OpenRailwayMap", 8, 16, 256, ".png",
|
||||
arrayOf(
|
||||
"https://a.tiles.openrailwaymap.org/standard/",
|
||||
"https://b.tiles.openrailwaymap.org/standard/",
|
||||
"https://c.tiles.openrailwaymap.org/standard/"
|
||||
),
|
||||
"© OpenRailwayMap contributors, © OpenStreetMap contributors"
|
||||
)
|
||||
try {
|
||||
val railwayTileSource = XYTileSource(
|
||||
"OpenRailwayMap", 8, 16, 256, ".png",
|
||||
arrayOf(
|
||||
"https://a.tiles.openrailwaymap.org/standard/",
|
||||
"https://b.tiles.openrailwaymap.org/standard/",
|
||||
"https://c.tiles.openrailwaymap.org/standard/"
|
||||
),
|
||||
"© OpenRailwayMap contributors, © OpenStreetMap contributors"
|
||||
)
|
||||
|
||||
val railwayProvider = MapTileProviderBasic(context)
|
||||
railwayProvider.tileSource = railwayTileSource
|
||||
val railwayProvider = MapTileProviderBasic(context)
|
||||
railwayProvider.tileSource = railwayTileSource
|
||||
|
||||
val railwayOverlay = TilesOverlay(railwayProvider, context)
|
||||
railwayOverlay.loadingBackgroundColor = android.graphics.Color.TRANSPARENT
|
||||
railwayOverlay.loadingLineColor = android.graphics.Color.TRANSPARENT
|
||||
val railwayOverlay = TilesOverlay(railwayProvider, context)
|
||||
railwayOverlay.loadingBackgroundColor = android.graphics.Color.TRANSPARENT
|
||||
railwayOverlay.loadingLineColor = android.graphics.Color.TRANSPARENT
|
||||
|
||||
overlays.add(railwayOverlay)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
try {
|
||||
val locationProvider = GpsMyLocationProvider(context).apply {
|
||||
locationUpdateMinDistance = 10f
|
||||
locationUpdateMinTime = 1000
|
||||
overlays.add(railwayOverlay)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
MyLocationNewOverlay(locationProvider, this).apply {
|
||||
enableMyLocation()
|
||||
}.also { overlays.add(it) }
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
try {
|
||||
val locationProvider = GpsMyLocationProvider(context).apply {
|
||||
locationUpdateMinDistance = 10f
|
||||
locationUpdateMinTime = 1000
|
||||
}
|
||||
|
||||
MyLocationNewOverlay(locationProvider, this).apply {
|
||||
enableMyLocation()
|
||||
}.also { overlays.add(it) }
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
val marker = Marker(this)
|
||||
marker.position = coordinates
|
||||
|
||||
val latStr = String.format("%.4f", coordinates.latitude)
|
||||
val lonStr = String.format("%.4f", coordinates.longitude)
|
||||
val coordStr = "${latStr}°N, ${lonStr}°E"
|
||||
marker.title = recordMap["train"]?.toString() ?: "列车"
|
||||
marker.snippet = coordStr
|
||||
marker.setInfoWindowAnchor(Marker.ANCHOR_CENTER, 0f)
|
||||
|
||||
overlays.add(marker)
|
||||
marker.showInfoWindow()
|
||||
}
|
||||
|
||||
val marker = Marker(this)
|
||||
marker.position = coordinates
|
||||
|
||||
val latStr = String.format("%.4f", coordinates.latitude)
|
||||
val lonStr = String.format("%.4f", coordinates.longitude)
|
||||
val coordStr = "${latStr}°N, ${lonStr}°E"
|
||||
marker.title = recordMap["train"]?.toString() ?: "列车"
|
||||
marker.snippet = coordStr
|
||||
marker.setInfoWindowAnchor(Marker.ANCHOR_CENTER, 0f)
|
||||
|
||||
overlays.add(marker)
|
||||
marker.showInfoWindow()
|
||||
}
|
||||
},
|
||||
update = { mapView -> mapView.invalidate() }
|
||||
},
|
||||
update = { mapView -> mapView.invalidate() }
|
||||
)
|
||||
}
|
||||
}
|
||||
if (recordMap.containsKey("position_info")) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = recordMap["position_info"] ?: "",
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
if (recordMap.containsKey("position_info")) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = recordMap["position_info"] ?: "",
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -383,14 +420,39 @@ fun MergedTrainRecordItem(
|
||||
val latestRecord = mergedRecord.latestRecord
|
||||
|
||||
val hasSelectedRecords = mergedRecord.records.any { selectedRecords.contains(it) }
|
||||
val cardColor = when {
|
||||
hasSelectedRecords -> MaterialTheme.colorScheme.primaryContainer
|
||||
else -> MaterialTheme.colorScheme.surface
|
||||
}
|
||||
|
||||
val cardColor by animateColorAsState(
|
||||
targetValue = when {
|
||||
hasSelectedRecords -> MaterialTheme.colorScheme.primaryContainer
|
||||
else -> MaterialTheme.colorScheme.surface
|
||||
},
|
||||
animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing),
|
||||
label = "mergedCardColor"
|
||||
)
|
||||
|
||||
val cardScale by animateFloatAsState(
|
||||
targetValue = if (hasSelectedRecords) 0.98f else 1f,
|
||||
animationSpec = tween(durationMillis = 150, easing = LinearEasing),
|
||||
label = "mergedCardScale"
|
||||
)
|
||||
|
||||
val cardElevation by animateDpAsState(
|
||||
targetValue = if (hasSelectedRecords) 6.dp else 2.dp,
|
||||
animationSpec = tween(durationMillis = 200, easing = LinearEasing),
|
||||
label = "mergedCardElevation"
|
||||
)
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.graphicsLayer {
|
||||
scaleX = cardScale
|
||||
scaleY = cardScale
|
||||
}
|
||||
.animateContentSize(
|
||||
animationSpec = tween(durationMillis = 150, easing = LinearEasing)
|
||||
),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = cardColor
|
||||
),
|
||||
@@ -948,7 +1010,13 @@ fun HistoryScreen(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 16.dp)
|
||||
) {
|
||||
items(filteredRecords) { item ->
|
||||
itemsIndexed(filteredRecords, key = { _, item ->
|
||||
when (item) {
|
||||
is TrainRecord -> item.uniqueId
|
||||
is MergedTrainRecord -> item.groupKey
|
||||
else -> item.hashCode()
|
||||
}
|
||||
}) { index, item ->
|
||||
when (item) {
|
||||
is TrainRecord -> {
|
||||
TrainRecordItem(
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
package org.noxylva.lbjconsole.ui.screens
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.clickable
|
||||
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -28,6 +33,20 @@ fun MonitorScreen(
|
||||
) {
|
||||
var showDetailDialog by remember { mutableStateOf(false) }
|
||||
var selectedRecord by remember { mutableStateOf<TrainRecord?>(null) }
|
||||
var isPressed by remember { mutableStateOf(false) }
|
||||
|
||||
val scale by animateFloatAsState(
|
||||
targetValue = if (isPressed) 0.98f else 1f,
|
||||
animationSpec = tween(durationMillis = 120),
|
||||
label = "content_scale"
|
||||
)
|
||||
|
||||
LaunchedEffect(isPressed) {
|
||||
if (isPressed) {
|
||||
delay(100)
|
||||
isPressed = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val timeSinceLastUpdate = remember { mutableStateOf<String?>(null) }
|
||||
@@ -76,20 +95,57 @@ fun MonitorScreen(
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
) {
|
||||
if (latestRecord != null) {
|
||||
AnimatedContent(
|
||||
targetState = latestRecord,
|
||||
transitionSpec = {
|
||||
fadeIn(
|
||||
animationSpec = tween(
|
||||
durationMillis = 300,
|
||||
easing = FastOutSlowInEasing
|
||||
)
|
||||
) + slideInVertically(
|
||||
initialOffsetY = { it / 4 },
|
||||
animationSpec = tween(
|
||||
durationMillis = 300,
|
||||
easing = FastOutSlowInEasing
|
||||
)
|
||||
) togetherWith fadeOut(
|
||||
animationSpec = tween(
|
||||
durationMillis = 150,
|
||||
easing = FastOutLinearInEasing
|
||||
)
|
||||
) + slideOutVertically(
|
||||
targetOffsetY = { -it / 4 },
|
||||
animationSpec = tween(
|
||||
durationMillis = 150,
|
||||
easing = FastOutLinearInEasing
|
||||
)
|
||||
)
|
||||
},
|
||||
label = "content_animation"
|
||||
) { record ->
|
||||
if (record != null) {
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp)
|
||||
.clickable {
|
||||
selectedRecord = latestRecord
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = true)
|
||||
) {
|
||||
isPressed = true
|
||||
selectedRecord = record
|
||||
showDetailDialog = true
|
||||
onRecordClick(latestRecord)
|
||||
onRecordClick(record)
|
||||
}
|
||||
.padding(8.dp)
|
||||
.graphicsLayer {
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
}
|
||||
) {
|
||||
|
||||
val recordMap = latestRecord.toMap()
|
||||
val recordMap = record.toMap()
|
||||
|
||||
|
||||
Row(
|
||||
@@ -209,6 +265,7 @@ fun MonitorScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user