fix(#28): add scrim when ExpandedFab is open

Add ExpandedFabScrim composable that renders a Material 3 scrim
overlay fading in/out in sync with the mini-FABs. Tapping the scrim
dismisses the FAB.

Move the ExpandedFab out of Scaffold's floatingActionButton slot and
into the body inside a Box, so the scrim can match the body size via
matchParentSize() and stack above the list but below the FAB. Add a
BackHandler that dismisses the FAB on system back while it is open.
This commit is contained in:
Achmad Setyabudi Susilo
2026-06-28 21:21:36 +07:00
parent ba99eac4be
commit b698f5084f
2 changed files with 91 additions and 46 deletions
@@ -5,10 +5,15 @@ import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Add
@@ -20,6 +25,7 @@ import androidx.compose.material3.SmallFloatingActionButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
@@ -57,6 +63,32 @@ fun ExpandedFab(
}
}
@Composable
fun ExpandedFabScrim(
visible: Boolean,
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
) {
val interactionSource = remember { MutableInteractionSource() }
AnimatedVisibility(
visible = visible,
enter = fadeIn(),
exit = fadeOut(),
modifier = modifier,
) {
Box(
modifier = Modifier
.fillMaxSize()
.clickable(
interactionSource = interactionSource,
indication = null,
onClick = onDismiss,
)
.background(MaterialTheme.colorScheme.scrim.copy(alpha = 0.32f)),
)
}
}
@Composable
fun MiniFab(
label: String,
@@ -1,5 +1,6 @@
package dev.achmad.ledgerr.ui.screens.expenses
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
@@ -63,6 +64,7 @@ import dev.achmad.ledgerr.domain.recurring.model.RecurringExpenseWithCategory
import dev.achmad.ledgerr.domain.recurring.model.RecurringInterval
import dev.achmad.ledgerr.ui.components.AppBar
import dev.achmad.ledgerr.ui.components.ExpandedFab
import dev.achmad.ledgerr.ui.components.ExpandedFabScrim
import dev.achmad.ledgerr.ui.components.ExportAction
import dev.achmad.ledgerr.ui.components.MiniFab
import dev.achmad.ledgerr.ui.components.SingleSelectFilterChipGroup
@@ -99,6 +101,9 @@ object ExpenseListScreen : Screen {
LaunchedEffect(selectedTab) {
isFabExpanded = false
}
BackHandler(enabled = isFabExpanded) {
isFabExpanded = false
}
Scaffold(
topBar = {
@@ -123,46 +128,13 @@ object ExpenseListScreen : Screen {
)
},
snackbarHost = { SnackbarHost(snackbarHostState) },
floatingActionButton = {
ExpandedFab(
expanded = isFabExpanded,
onToggle = { isFabExpanded = !isFabExpanded },
) {
if (selectedTab == 0) {
MiniFab(
label = stringResource(R.string.fab_manual),
icon = Icons.Outlined.Edit,
onClick = {
isFabExpanded = false
navigator.push(AddEditExpenseScreen(expenseId = null))
},
)
MiniFab(
label = stringResource(R.string.fab_import),
icon = Icons.Outlined.UploadFile,
onClick = {
isFabExpanded = false
navigator.push(ImportBankStatementScreen)
},
)
} else {
MiniFab(
label = stringResource(R.string.expense_list_fab_add_recurring),
icon = Icons.Outlined.Repeat,
onClick = {
isFabExpanded = false
navigator.push(AddEditRecurringScreen(recurringId = null))
},
)
}
}
},
) { padding ->
Column(
Box(
modifier = Modifier
.fillMaxSize()
.padding(padding),
) {
Column(modifier = Modifier.fillMaxSize()) {
PrimaryTabRow(selectedTabIndex = selectedTab) {
listOf(
stringResource(R.string.expense_list_tab_expenses),
@@ -202,6 +174,47 @@ object ExpenseListScreen : Screen {
}
}
}
ExpandedFabScrim(
visible = isFabExpanded,
onDismiss = { isFabExpanded = false },
modifier = Modifier.matchParentSize(),
)
ExpandedFab(
expanded = isFabExpanded,
onToggle = { isFabExpanded = !isFabExpanded },
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(16.dp),
) {
if (selectedTab == 0) {
MiniFab(
label = stringResource(R.string.fab_manual),
icon = Icons.Outlined.Edit,
onClick = {
isFabExpanded = false
navigator.push(AddEditExpenseScreen(expenseId = null))
},
)
MiniFab(
label = stringResource(R.string.fab_import),
icon = Icons.Outlined.UploadFile,
onClick = {
isFabExpanded = false
navigator.push(ImportBankStatementScreen)
},
)
} else {
MiniFab(
label = stringResource(R.string.expense_list_fab_add_recurring),
icon = Icons.Outlined.Repeat,
onClick = {
isFabExpanded = false
navigator.push(AddEditRecurringScreen(recurringId = null))
},
)
}
}
}
}
pendingDeleteId?.let { id ->