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