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:
@@ -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,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)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user