From 1c9e14e87c23a8076160976bc6d85966986fa650 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Mon, 25 Aug 2025 20:56:53 -0500 Subject: [PATCH] refactor(UI): Use animateFloatingActionButton for FAB animations (#2844) Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com> --- .../geeksville/mesh/ui/contact/Contacts.kt | 23 ++- .../com/geeksville/mesh/ui/node/NodeScreen.kt | 145 +++++++++--------- .../mesh/ui/sharing/ContactSharing.kt | 2 +- 3 files changed, 87 insertions(+), 83 deletions(-) diff --git a/app/src/main/java/com/geeksville/mesh/ui/contact/Contacts.kt b/app/src/main/java/com/geeksville/mesh/ui/contact/Contacts.kt index 5df627429..a7fe62130 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/contact/Contacts.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/contact/Contacts.kt @@ -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, ) { diff --git a/app/src/main/java/com/geeksville/mesh/ui/node/NodeScreen.kt b/app/src/main/java/com/geeksville/mesh/ui/node/NodeScreen.kt index 7bcee7b30..e4c375138 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/node/NodeScreen.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/node/NodeScreen.kt @@ -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) })) - } } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/sharing/ContactSharing.kt b/app/src/main/java/com/geeksville/mesh/ui/sharing/ContactSharing.kt index 54d238c07..7fbe97d49 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/sharing/ContactSharing.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/sharing/ContactSharing.kt @@ -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)) }