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.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically 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.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Add 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.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
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.vector.ImageVector 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 @Composable
fun MiniFab( fun MiniFab(
label: String, label: String,
@@ -1,5 +1,6 @@
package dev.achmad.ledgerr.ui.screens.expenses package dev.achmad.ledgerr.ui.screens.expenses
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable 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.domain.recurring.model.RecurringInterval
import dev.achmad.ledgerr.ui.components.AppBar import dev.achmad.ledgerr.ui.components.AppBar
import dev.achmad.ledgerr.ui.components.ExpandedFab 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.ExportAction
import dev.achmad.ledgerr.ui.components.MiniFab import dev.achmad.ledgerr.ui.components.MiniFab
import dev.achmad.ledgerr.ui.components.SingleSelectFilterChipGroup import dev.achmad.ledgerr.ui.components.SingleSelectFilterChipGroup
@@ -99,6 +101,9 @@ object ExpenseListScreen : Screen {
LaunchedEffect(selectedTab) { LaunchedEffect(selectedTab) {
isFabExpanded = false isFabExpanded = false
} }
BackHandler(enabled = isFabExpanded) {
isFabExpanded = false
}
Scaffold( Scaffold(
topBar = { topBar = {
@@ -123,10 +128,63 @@ object ExpenseListScreen : Screen {
) )
}, },
snackbarHost = { SnackbarHost(snackbarHostState) }, snackbarHost = { SnackbarHost(snackbarHostState) },
floatingActionButton = { ) { padding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(padding),
) {
Column(modifier = Modifier.fillMaxSize()) {
PrimaryTabRow(selectedTabIndex = selectedTab) {
listOf(
stringResource(R.string.expense_list_tab_expenses),
stringResource(R.string.expense_list_tab_recurring),
).forEachIndexed { index, title ->
Tab(
selected = selectedTab == index,
onClick = {
coroutineScope.launch {
pagerState.animateScrollToPage(index)
}
},
text = { Text(text = title) },
)
}
}
HorizontalPager(
state = pagerState,
modifier = Modifier.fillMaxSize(),
) { page ->
when (page) {
0 -> ExpensesTabContent(
screenModel = screenModel,
expenses = expenses,
searchQuery = searchQuery,
dateRangeFilter = dateRangeFilter,
contentPadding = PaddingValues(bottom = 88.dp),
onExpenseClick = { id -> navigator.push(AddEditExpenseScreen(expenseId = id)) },
onExpenseLongClick = { id -> pendingDeleteId = id },
)
1 -> RecurringTabContent(
screenModel = screenModel,
recurring = recurring,
contentPadding = PaddingValues(bottom = 88.dp),
onRecurringClick = { id -> navigator.push(AddEditRecurringScreen(recurringId = id)) },
)
}
}
}
ExpandedFabScrim(
visible = isFabExpanded,
onDismiss = { isFabExpanded = false },
modifier = Modifier.matchParentSize(),
)
ExpandedFab( ExpandedFab(
expanded = isFabExpanded, expanded = isFabExpanded,
onToggle = { isFabExpanded = !isFabExpanded }, onToggle = { isFabExpanded = !isFabExpanded },
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(16.dp),
) { ) {
if (selectedTab == 0) { if (selectedTab == 0) {
MiniFab( MiniFab(
@@ -156,51 +214,6 @@ object ExpenseListScreen : Screen {
) )
} }
} }
},
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding),
) {
PrimaryTabRow(selectedTabIndex = selectedTab) {
listOf(
stringResource(R.string.expense_list_tab_expenses),
stringResource(R.string.expense_list_tab_recurring),
).forEachIndexed { index, title ->
Tab(
selected = selectedTab == index,
onClick = {
coroutineScope.launch {
pagerState.animateScrollToPage(index)
}
},
text = { Text(text = title) },
)
}
}
HorizontalPager(
state = pagerState,
modifier = Modifier.fillMaxSize(),
) { page ->
when (page) {
0 -> ExpensesTabContent(
screenModel = screenModel,
expenses = expenses,
searchQuery = searchQuery,
dateRangeFilter = dateRangeFilter,
contentPadding = PaddingValues(bottom = 88.dp),
onExpenseClick = { id -> navigator.push(AddEditExpenseScreen(expenseId = id)) },
onExpenseLongClick = { id -> pendingDeleteId = id },
)
1 -> RecurringTabContent(
screenModel = screenModel,
recurring = recurring,
contentPadding = PaddingValues(bottom = 88.dp),
onRecurringClick = { id -> navigator.push(AddEditRecurringScreen(recurringId = id)) },
)
}
}
} }
} }