feat: add animation effects and visual feedback

This commit is contained in:
Nedifinita
2025-07-22 23:18:50 +08:00
parent 799410eeb2
commit 3edc8632be
4 changed files with 428 additions and 140 deletions

View File

@@ -20,20 +20,31 @@ import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.* 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.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.filled.LocationOn import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp 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.ContextCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -966,55 +977,165 @@ fun ConnectionDialog(
) { ) {
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
title = { Text("连接设备") }, title = {
Text(
text = "蓝牙设备",
style = MaterialTheme.typography.headlineSmall
)
},
text = { text = {
Column(modifier = Modifier.fillMaxWidth()) { Column(
modifier = Modifier.fillMaxWidth()
) {
Button( Button(
onClick = onScan, 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) { if (devices.isNotEmpty()) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) Text(
Spacer(modifier = Modifier.height(8.dp)) 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
}
}
if (devices.isEmpty()) {
Text("未找到设备")
} else {
Column {
devices.forEach { device ->
Card( Card(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 4.dp) .graphicsLayer {
.clickable { onConnect(device) } scaleX = cardScale
scaleY = cardScale
},
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) { ) {
Column( Row(
modifier = Modifier.padding(8.dp) 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( Icon(
text = device.name ?: "未知设备", imageVector = Icons.Default.Bluetooth,
fontWeight = FontWeight.Bold contentDescription = null,
) tint = MaterialTheme.colorScheme.primary,
Text( modifier = Modifier.size(20.dp)
text = device.address,
style = MaterialTheme.typography.bodySmall
) )
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 = { confirmButton = {
TextButton(onClick = onDismiss) { TextButton(onClick = onDismiss) {
Text("取消") Text("关闭")
} }
} }
) )

View File

@@ -1,18 +1,26 @@
package org.noxylva.lbjconsole.ui.components 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.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.FilterList import androidx.compose.material.icons.filled.FilterList
import androidx.compose.material.icons.filled.Share import androidx.compose.material.icons.filled.Share
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -21,6 +29,7 @@ import org.noxylva.lbjconsole.model.TrainRecord
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun TrainRecordsList( fun TrainRecordsList(
records: List<TrainRecord>, records: List<TrainRecord>,
@@ -41,19 +50,52 @@ fun TrainRecordsList(
} else { } else {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), 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( Card(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 2.dp) .graphicsLayer {
.clickable { onRecordClick(record) }, scaleX = scale
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp) scaleY = scale
}
.animateItemPlacement(
animationSpec = tween(durationMillis = 200)
),
elevation = CardDefaults.cardElevation(defaultElevation = elevation)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = true)
) {
isPressed = true
onRecordClick(record)
}
.padding(8.dp), .padding(8.dp),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically

View File

@@ -9,6 +9,9 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items 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.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ripple.rememberRipple import androidx.compose.material.ripple.rememberRipple
@@ -19,6 +22,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -53,14 +57,41 @@ fun TrainRecordItem(
onToggleSelection: (TrainRecord) -> Unit, onToggleSelection: (TrainRecord) -> Unit,
onLongClick: (TrainRecord) -> Unit onLongClick: (TrainRecord) -> Unit
) { ) {
val cardColor = when { val recordId = record.uniqueId
isSelected -> MaterialTheme.colorScheme.primaryContainer val isExpanded = expandedStatesMap[recordId] == true
else -> MaterialTheme.colorScheme.surface
} 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( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), .fillMaxWidth()
.graphicsLayer {
scaleX = cardScale
scaleY = cardScale
}
.animateContentSize(
animationSpec = tween(durationMillis = 150, easing = LinearEasing)
),
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
containerColor = cardColor containerColor = cardColor
), ),
@@ -260,106 +291,112 @@ fun TrainRecordItem(
} }
} }
if (isExpanded) { AnimatedVisibility(
val coordinates = remember { record.getCoordinates() } 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) { if (coordinates != null) {
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
} }
if (coordinates != null) { if (coordinates != null) {
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(220.dp) .height(220.dp)
.padding(vertical = 4.dp) .padding(vertical = 4.dp)
.clip(RoundedCornerShape(8.dp)), .clip(RoundedCornerShape(8.dp)),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
AndroidView( AndroidView(
modifier = Modifier.clickable( modifier = Modifier.clickable(
indication = null, indication = null,
interactionSource = remember { MutableInteractionSource() } interactionSource = remember { MutableInteractionSource() }
) {}, ) {},
factory = { context -> factory = { context ->
MapView(context).apply { MapView(context).apply {
setTileSource(TileSourceFactory.MAPNIK) setTileSource(TileSourceFactory.MAPNIK)
setMultiTouchControls(true) setMultiTouchControls(true)
zoomController.setVisibility(org.osmdroid.views.CustomZoomButtonsController.Visibility.NEVER) zoomController.setVisibility(org.osmdroid.views.CustomZoomButtonsController.Visibility.NEVER)
isHorizontalMapRepetitionEnabled = false isHorizontalMapRepetitionEnabled = false
isVerticalMapRepetitionEnabled = false isVerticalMapRepetitionEnabled = false
setHasTransientState(true) setHasTransientState(true)
setOnTouchListener { v, event -> setOnTouchListener { v, event ->
v.parent?.requestDisallowInterceptTouchEvent(true) v.parent?.requestDisallowInterceptTouchEvent(true)
false false
} }
controller.setZoom(10.0) controller.setZoom(10.0)
controller.setCenter(coordinates) controller.setCenter(coordinates)
this.isTilesScaledToDpi = true this.isTilesScaledToDpi = true
this.setUseDataConnection(true) this.setUseDataConnection(true)
try { try {
val railwayTileSource = XYTileSource( val railwayTileSource = XYTileSource(
"OpenRailwayMap", 8, 16, 256, ".png", "OpenRailwayMap", 8, 16, 256, ".png",
arrayOf( arrayOf(
"https://a.tiles.openrailwaymap.org/standard/", "https://a.tiles.openrailwaymap.org/standard/",
"https://b.tiles.openrailwaymap.org/standard/", "https://b.tiles.openrailwaymap.org/standard/",
"https://c.tiles.openrailwaymap.org/standard/" "https://c.tiles.openrailwaymap.org/standard/"
), ),
"© OpenRailwayMap contributors, © OpenStreetMap contributors" "© OpenRailwayMap contributors, © OpenStreetMap contributors"
) )
val railwayProvider = MapTileProviderBasic(context) val railwayProvider = MapTileProviderBasic(context)
railwayProvider.tileSource = railwayTileSource railwayProvider.tileSource = railwayTileSource
val railwayOverlay = TilesOverlay(railwayProvider, context) val railwayOverlay = TilesOverlay(railwayProvider, context)
railwayOverlay.loadingBackgroundColor = android.graphics.Color.TRANSPARENT railwayOverlay.loadingBackgroundColor = android.graphics.Color.TRANSPARENT
railwayOverlay.loadingLineColor = android.graphics.Color.TRANSPARENT railwayOverlay.loadingLineColor = android.graphics.Color.TRANSPARENT
overlays.add(railwayOverlay) overlays.add(railwayOverlay)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
}
try {
val locationProvider = GpsMyLocationProvider(context).apply {
locationUpdateMinDistance = 10f
locationUpdateMinTime = 1000
} }
MyLocationNewOverlay(locationProvider, this).apply { try {
enableMyLocation() val locationProvider = GpsMyLocationProvider(context).apply {
}.also { overlays.add(it) } locationUpdateMinDistance = 10f
} catch (e: Exception) { locationUpdateMinTime = 1000
e.printStackTrace() }
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) update = { mapView -> mapView.invalidate() }
marker.position = coordinates )
}
val latStr = String.format("%.4f", coordinates.latitude) }
val lonStr = String.format("%.4f", coordinates.longitude) if (recordMap.containsKey("position_info")) {
val coordStr = "${latStr}°N, ${lonStr}°E" Spacer(modifier = Modifier.height(8.dp))
marker.title = recordMap["train"]?.toString() ?: "列车" Text(
marker.snippet = coordStr text = recordMap["position_info"] ?: "",
marker.setInfoWindowAnchor(Marker.ANCHOR_CENTER, 0f) fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurface
overlays.add(marker)
marker.showInfoWindow()
}
},
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
)
}
} }
} }
} }
@@ -383,14 +420,39 @@ fun MergedTrainRecordItem(
val latestRecord = mergedRecord.latestRecord val latestRecord = mergedRecord.latestRecord
val hasSelectedRecords = mergedRecord.records.any { selectedRecords.contains(it) } val hasSelectedRecords = mergedRecord.records.any { selectedRecords.contains(it) }
val cardColor = when {
hasSelectedRecords -> MaterialTheme.colorScheme.primaryContainer val cardColor by animateColorAsState(
else -> MaterialTheme.colorScheme.surface 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( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), .fillMaxWidth()
.graphicsLayer {
scaleX = cardScale
scaleY = cardScale
}
.animateContentSize(
animationSpec = tween(durationMillis = 150, easing = LinearEasing)
),
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
containerColor = cardColor containerColor = cardColor
), ),
@@ -948,7 +1010,13 @@ fun HistoryScreen(
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 16.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) { when (item) {
is TrainRecord -> { is TrainRecord -> {
TrainRecordItem( TrainRecordItem(

View File

@@ -1,13 +1,18 @@
package org.noxylva.lbjconsole.ui.screens package org.noxylva.lbjconsole.ui.screens
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -28,6 +33,20 @@ fun MonitorScreen(
) { ) {
var showDetailDialog by remember { mutableStateOf(false) } var showDetailDialog by remember { mutableStateOf(false) }
var selectedRecord by remember { mutableStateOf<TrainRecord?>(null) } 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) } val timeSinceLastUpdate = remember { mutableStateOf<String?>(null) }
@@ -76,20 +95,57 @@ fun MonitorScreen(
.fillMaxWidth() .fillMaxWidth()
.weight(1f) .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( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(8.dp) .clickable(
.clickable { interactionSource = remember { MutableInteractionSource() },
selectedRecord = latestRecord indication = rememberRipple(bounded = true)
) {
isPressed = true
selectedRecord = record
showDetailDialog = true showDetailDialog = true
onRecordClick(latestRecord) onRecordClick(record)
}
.padding(8.dp)
.graphicsLayer {
scaleX = scale
scaleY = scale
} }
) { ) {
val recordMap = latestRecord.toMap() val recordMap = record.toMap()
Row( Row(
@@ -209,6 +265,7 @@ fun MonitorScreen(
} }
} }
} }
}
} }
} }