refactor(UI): Use animateFloatingActionButton for FAB animations (#2844)

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2025-08-25 20:56:53 -05:00 committed by GitHub
parent b2a8d7a934
commit 1c9e14e87c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 87 additions and 83 deletions

View file

@ -17,7 +17,6 @@
package com.geeksville.mesh.ui.contact
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
@ -37,6 +36,7 @@ import androidx.compose.material.icons.rounded.QrCode2
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@ -44,6 +44,7 @@ import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.animateFloatingActionButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
@ -67,6 +68,7 @@ import com.geeksville.mesh.model.Contact
import com.geeksville.mesh.model.UIViewModel
import java.util.concurrent.TimeUnit
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Suppress("LongMethod")
@Composable
fun ContactsScreen(
@ -153,13 +155,11 @@ fun ContactsScreen(
}
},
floatingActionButton = {
AnimatedVisibility(isConnected && !isSelectionModeActive) {
FloatingActionButton(onClick = onNavigateToShare) {
Icon(
Icons.Rounded.QrCode2,
contentDescription = null
)
}
FloatingActionButton(
modifier = Modifier.animateFloatingActionButton(visible = isConnected, alignment = Alignment.BottomEnd),
onClick = onNavigateToShare,
) {
Icon(Icons.Rounded.QrCode2, contentDescription = null)
}
},
) { paddingValues ->
@ -227,11 +227,8 @@ fun MuteNotificationsDialog(
val text = stringResource(stringRes)
Row(
modifier =
Modifier
.fillMaxWidth()
.selectable(
selected = isSelected,
onClick = { selectedOptionIndex = index })
Modifier.fillMaxWidth()
.selectable(selected = isSelected, onClick = { selectedOptionIndex = index })
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {

View file

@ -17,7 +17,6 @@
package com.geeksville.mesh.ui.node
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
@ -28,7 +27,10 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.animateFloatingActionButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
@ -54,7 +56,7 @@ import com.geeksville.mesh.ui.sharing.AddContactFAB
import com.geeksville.mesh.ui.sharing.SharedContactDialog
import com.geeksville.mesh.ui.sharing.supportsQrCodeSharing
@OptIn(ExperimentalFoundationApi::class)
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3ExpressiveApi::class)
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
fun NodeScreen(
@ -80,77 +82,82 @@ fun NodeScreen(
}
val isScrollInProgress by remember { derivedStateOf { listState.isScrollInProgress } }
Scaffold(
floatingActionButton = {
val firmwareVersion = DeviceVersion(ourNode?.metadata?.firmwareVersion ?: "0.0.0")
val shareCapable = firmwareVersion.supportsQrCodeSharing()
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(state = listState, modifier = Modifier.fillMaxSize()) {
stickyHeader {
val animatedAlpha by
animateFloatAsState(targetValue = if (!isScrollInProgress) 1.0f else 0f, label = "alpha")
NodeFilterTextField(
modifier =
Modifier.fillMaxWidth()
.graphicsLayer(alpha = animatedAlpha)
.background(MaterialTheme.colorScheme.surfaceDim)
.padding(8.dp),
filterText = state.filter,
onTextChange = model::setNodeFilterText,
currentSortOption = state.sort,
onSortSelect = model::setSortOption,
includeUnknown = state.includeUnknown,
onToggleIncludeUnknown = model::toggleIncludeUnknown,
onlyOnline = state.onlyOnline,
onToggleOnlyOnline = model::toggleOnlyOnline,
onlyDirect = state.onlyDirect,
onToggleOnlyDirect = model::toggleOnlyDirect,
showDetails = state.showDetails,
onToggleShowDetails = model::toggleShowDetails,
showIgnored = state.showIgnored,
onToggleShowIgnored = model::toggleShowIgnored,
ignoredNodeCount = ignoredNodeCount,
)
}
AddContactFAB(
modifier =
Modifier.animateFloatingActionButton(
visible = !isScrollInProgress && connectionState == ConnectionState.CONNECTED && shareCapable,
alignment = Alignment.BottomEnd,
),
model = model,
onSharedContactImport = { contact -> model.addSharedContact(contact) },
)
},
) { contentPadding ->
Box(modifier = Modifier.fillMaxSize().padding(contentPadding)) {
LazyColumn(state = listState, modifier = Modifier.fillMaxSize()) {
stickyHeader {
val animatedAlpha by
animateFloatAsState(targetValue = if (!isScrollInProgress) 1.0f else 0f, label = "alpha")
NodeFilterTextField(
modifier =
Modifier.fillMaxWidth()
.graphicsLayer(alpha = animatedAlpha)
.background(MaterialTheme.colorScheme.surfaceDim)
.padding(8.dp),
filterText = state.filter,
onTextChange = model::setNodeFilterText,
currentSortOption = state.sort,
onSortSelect = model::setSortOption,
includeUnknown = state.includeUnknown,
onToggleIncludeUnknown = model::toggleIncludeUnknown,
onlyOnline = state.onlyOnline,
onToggleOnlyOnline = model::toggleOnlyOnline,
onlyDirect = state.onlyDirect,
onToggleOnlyDirect = model::toggleOnlyDirect,
showDetails = state.showDetails,
onToggleShowDetails = model::toggleShowDetails,
showIgnored = state.showIgnored,
onToggleShowIgnored = model::toggleShowIgnored,
ignoredNodeCount = ignoredNodeCount,
)
}
items(nodes, key = { it.num }) { node ->
NodeItem(
modifier = Modifier.animateItem(),
thisNode = ourNode,
thatNode = node,
distanceUnits = state.distanceUnits,
tempInFahrenheit = state.tempInFahrenheit,
onAction = { menuItem ->
when (menuItem) {
is NodeMenuAction.Remove -> model.removeNode(node.num)
is NodeMenuAction.Ignore -> model.ignoreNode(node)
is NodeMenuAction.Favorite -> model.favoriteNode(node)
is NodeMenuAction.DirectMessage -> {
val hasPKC = model.ourNodeInfo.value?.hasPKC == true && node.hasPKC
val channel = if (hasPKC) DataPacket.PKC_CHANNEL_INDEX else node.channel
navigateToMessages("$channel${node.user.id}")
items(nodes, key = { it.num }) { node ->
NodeItem(
modifier = Modifier.animateItem(),
thisNode = ourNode,
thatNode = node,
distanceUnits = state.distanceUnits,
tempInFahrenheit = state.tempInFahrenheit,
onAction = { menuItem ->
when (menuItem) {
is NodeMenuAction.Remove -> model.removeNode(node.num)
is NodeMenuAction.Ignore -> model.ignoreNode(node)
is NodeMenuAction.Favorite -> model.favoriteNode(node)
is NodeMenuAction.DirectMessage -> {
val hasPKC = model.ourNodeInfo.value?.hasPKC == true && node.hasPKC
val channel = if (hasPKC) DataPacket.PKC_CHANNEL_INDEX else node.channel
navigateToMessages("$channel${node.user.id}")
}
is NodeMenuAction.RequestUserInfo -> model.requestUserInfo(node.num)
is NodeMenuAction.RequestPosition -> model.requestPosition(node.num)
is NodeMenuAction.TraceRoute -> model.requestTraceroute(node.num)
is NodeMenuAction.MoreDetails -> navigateToNodeDetails(node.num)
is NodeMenuAction.Share -> showSharedContact = node
}
is NodeMenuAction.RequestUserInfo -> model.requestUserInfo(node.num)
is NodeMenuAction.RequestPosition -> model.requestPosition(node.num)
is NodeMenuAction.TraceRoute -> model.requestTraceroute(node.num)
is NodeMenuAction.MoreDetails -> navigateToNodeDetails(node.num)
is NodeMenuAction.Share -> showSharedContact = node
}
},
expanded = state.showDetails,
currentTimeMillis = currentTimeMillis,
isConnected = connectionState.isConnected(),
)
},
expanded = state.showDetails,
currentTimeMillis = currentTimeMillis,
isConnected = connectionState.isConnected(),
)
}
}
}
val firmwareVersion = DeviceVersion(ourNode?.metadata?.firmwareVersion ?: "0.0.0")
val shareCapable = firmwareVersion.supportsQrCodeSharing()
AnimatedVisibility(
modifier = Modifier.align(Alignment.BottomEnd),
visible = !isScrollInProgress && connectionState == ConnectionState.CONNECTED && shareCapable,
) {
@Suppress("NewApi")
(AddContactFAB(model = model, onSharedContactImport = { contact -> model.addSharedContact(contact) }))
}
}
}

View file

@ -163,6 +163,7 @@ fun AddContactFAB(
}
FloatingActionButton(
modifier = modifier,
onClick = {
if (cameraPermissionState.status.isGranted) {
zxingScan()
@ -170,7 +171,6 @@ fun AddContactFAB(
cameraPermissionState.launchPermissionRequest()
}
},
modifier = modifier.padding(16.dp),
) {
Icon(imageVector = Icons.TwoTone.QrCodeScanner, contentDescription = stringResource(R.string.scan_qr_code))
}