From b698f5084f2b62984bf8f64a8a80686de6ccd416 Mon Sep 17 00:00:00 2001 From: Achmad Setyabudi Susilo Date: Sun, 28 Jun 2026 21:21:36 +0700 Subject: [PATCH] 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. --- .../ledgerr/ui/components/ExpandedFab.kt | 32 ++++++ .../ui/screens/expenses/ExpenseListScreen.kt | 105 ++++++++++-------- 2 files changed, 91 insertions(+), 46 deletions(-) diff --git a/app/src/main/java/dev/achmad/ledgerr/ui/components/ExpandedFab.kt b/app/src/main/java/dev/achmad/ledgerr/ui/components/ExpandedFab.kt index c43bbbf..dcac0fb 100644 --- a/app/src/main/java/dev/achmad/ledgerr/ui/components/ExpandedFab.kt +++ b/app/src/main/java/dev/achmad/ledgerr/ui/components/ExpandedFab.kt @@ -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, diff --git a/app/src/main/java/dev/achmad/ledgerr/ui/screens/expenses/ExpenseListScreen.kt b/app/src/main/java/dev/achmad/ledgerr/ui/screens/expenses/ExpenseListScreen.kt index ad1e964..ba08737 100644 --- a/app/src/main/java/dev/achmad/ledgerr/ui/screens/expenses/ExpenseListScreen.kt +++ b/app/src/main/java/dev/achmad/ledgerr/ui/screens/expenses/ExpenseListScreen.kt @@ -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,10 +128,63 @@ object ExpenseListScreen : Screen { ) }, 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( expanded = isFabExpanded, onToggle = { isFabExpanded = !isFabExpanded }, + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(16.dp), ) { if (selectedTab == 0) { 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)) }, - ) - } - } } }