Hold the top bar and FAB still across view switches (#184)
This commit is contained in:
@@ -50,6 +50,9 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import de.jeanlucmakiola.calendula.ui.common.morphChrome
|
||||
import de.jeanlucmakiola.calendula.ui.common.ViewMorphChrome
|
||||
import de.jeanlucmakiola.calendula.ui.common.ChromeSlot
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -115,6 +118,9 @@ fun AgendaScreen(
|
||||
val weekStart by viewModel.weekStart.collectAsStateWithLifecycle()
|
||||
|
||||
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
||||
// The bar and FAB hold still through a view switch, but not over an open drawer.
|
||||
val chromeMorph = drawerState.currentValue == DrawerValue.Closed &&
|
||||
drawerState.targetValue == DrawerValue.Closed
|
||||
val scope = rememberCoroutineScope()
|
||||
var showRangePicker by remember { mutableStateOf(false) }
|
||||
|
||||
@@ -173,24 +179,28 @@ fun AgendaScreen(
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
AgendaTopBar(
|
||||
selectedView = selectedView,
|
||||
onCycleView = { onSelectView(selectedView.next(quickSwitchViews)) },
|
||||
quickSwitchViews = quickSwitchViews,
|
||||
onOpenDrawer = { scope.launch { drawerState.open() } },
|
||||
onOpenSearch = onOpenSearch,
|
||||
showTodayButton = todayInToolbar,
|
||||
onToday = jumpToToday,
|
||||
)
|
||||
ViewMorphChrome(chromeMorph) {
|
||||
AgendaTopBar(
|
||||
selectedView = selectedView,
|
||||
onCycleView = { onSelectView(selectedView.next(quickSwitchViews)) },
|
||||
quickSwitchViews = quickSwitchViews,
|
||||
onOpenDrawer = { scope.launch { drawerState.open() } },
|
||||
onOpenSearch = onOpenSearch,
|
||||
showTodayButton = todayInToolbar,
|
||||
onToday = jumpToToday,
|
||||
)
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
CalendarFabColumn(
|
||||
todayVisible = (!isOnToday || (scrolledAway && successState != null)) &&
|
||||
!todayInToolbar,
|
||||
todayText = stringResource(R.string.agenda_today_action),
|
||||
onToday = jumpToToday,
|
||||
onCreate = { onCreateEvent(anchor, null) },
|
||||
)
|
||||
ViewMorphChrome(chromeMorph) {
|
||||
CalendarFabColumn(
|
||||
todayVisible = (!isOnToday || (scrolledAway && successState != null)) &&
|
||||
!todayInToolbar,
|
||||
todayText = stringResource(R.string.agenda_today_action),
|
||||
onToday = jumpToToday,
|
||||
onCreate = { onCreateEvent(anchor, null) },
|
||||
)
|
||||
}
|
||||
},
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
@@ -510,11 +520,13 @@ private fun AgendaTopBar(
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(start = AppBarSpacing.TitleInset),
|
||||
modifier = Modifier
|
||||
.morphChrome(ChromeSlot.Title)
|
||||
.padding(start = AppBarSpacing.TitleInset),
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onOpenDrawer) {
|
||||
IconButton(onClick = onOpenDrawer, modifier = Modifier.morphChrome(ChromeSlot.Menu)) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Menu,
|
||||
contentDescription = stringResource(R.string.month_open_menu),
|
||||
@@ -523,7 +535,7 @@ private fun AgendaTopBar(
|
||||
},
|
||||
actions = {
|
||||
TodayAction(show = showTodayButton, onToday = onToday)
|
||||
IconButton(onClick = onOpenSearch) {
|
||||
IconButton(onClick = onOpenSearch, modifier = Modifier.morphChrome(ChromeSlot.Search)) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Search,
|
||||
contentDescription = stringResource(R.string.search_action),
|
||||
|
||||
@@ -15,6 +15,7 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -48,7 +49,7 @@ fun CalendarFabColumn(
|
||||
text = { Text(todayText) },
|
||||
)
|
||||
}
|
||||
FloatingActionButton(onClick = onCreate) {
|
||||
FloatingActionButton(onClick = onCreate, modifier = Modifier.morphChrome(ChromeSlot.Create)) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
contentDescription = stringResource(R.string.event_edit_new_title),
|
||||
|
||||
@@ -56,6 +56,7 @@ fun CalendarTitleButton(
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.morphChrome(ChromeSlot.Title)
|
||||
.heightIn(min = 48.dp)
|
||||
.clip(MaterialTheme.shapes.large)
|
||||
.clickable(
|
||||
|
||||
@@ -59,7 +59,9 @@ fun TodayAction(show: Boolean, onToday: () -> Unit) {
|
||||
val description = stringResource(R.string.today_jump_action)
|
||||
IconButton(
|
||||
onClick = onToday,
|
||||
modifier = Modifier.semantics { contentDescription = description },
|
||||
modifier = Modifier
|
||||
.morphChrome(ChromeSlot.Today)
|
||||
.semantics { contentDescription = description },
|
||||
) {
|
||||
val textSize = with(LocalDensity.current) { GlyphTextHeight.toSp() }
|
||||
Box(
|
||||
|
||||
@@ -20,6 +20,8 @@ import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.Path
|
||||
@@ -200,6 +202,60 @@ fun TimelineGrounds(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The pieces of the top bar and the FAB every view has (#184). Those drawn the
|
||||
* same in each view hold still through a switch instead of fading out and back
|
||||
* in; the title and the view pill, which differ, cross-fade in place.
|
||||
*/
|
||||
enum class ChromeSlot(val identical: Boolean) {
|
||||
Menu(identical = true),
|
||||
Search(identical = true),
|
||||
Today(identical = true),
|
||||
Create(identical = true),
|
||||
Title(identical = false),
|
||||
Pill(identical = false),
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the view morph to a screen's top bar or FAB only while [enabled]:
|
||||
* with its drawer open or opening, what travels would be drawn over the drawer.
|
||||
*/
|
||||
@Composable
|
||||
fun ViewMorphChrome(enabled: Boolean, content: @Composable () -> Unit) = ViewMorphPage(enabled, content)
|
||||
|
||||
/** Tags a piece of the top bar or the FAB; see [ChromeSlot]. */
|
||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||
@Composable
|
||||
fun Modifier.morphChrome(slot: ChromeSlot): Modifier {
|
||||
val morph = LocalViewMorph.current ?: return this
|
||||
return with(morph.shared) {
|
||||
val state = rememberSharedContentState(slot)
|
||||
if (slot.identical) {
|
||||
this@morphChrome.sharedElement(
|
||||
sharedContentState = state,
|
||||
animatedVisibilityScope = morph.visibility,
|
||||
boundsTransform = morph.boundsTransform,
|
||||
zIndexInOverlay = CHROME_Z_INDEX,
|
||||
)
|
||||
} else {
|
||||
this@morphChrome.sharedBounds(
|
||||
sharedContentState = state,
|
||||
animatedVisibilityScope = morph.visibility,
|
||||
enter = fadeIn(morph.fadeSpec),
|
||||
exit = fadeOut(morph.fadeSpec),
|
||||
boundsTransform = morph.boundsTransform,
|
||||
// Each side keeps its own size: a title squeezed to the other's
|
||||
// width would ellipsise and unfold on the way.
|
||||
resizeMode = SharedTransitionScope.ResizeMode.scaleToBounds(ContentScale.None, Alignment.CenterStart),
|
||||
zIndexInOverlay = CHROME_Z_INDEX,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Above the events and dates, which pass under the top bar on their way. */
|
||||
private const val CHROME_Z_INDEX = 2f
|
||||
|
||||
/** The view switch's travel: the M3 Expressive default spatial spring, as the pagers settle. */
|
||||
@OptIn(ExperimentalSharedTransitionApi::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
|
||||
@@ -40,7 +40,9 @@ fun ViewSwitcherPill(
|
||||
FilledTonalButton(
|
||||
onClick = onCycle,
|
||||
shape = MaterialTheme.shapes.large,
|
||||
modifier = modifier.padding(end = trailingInset),
|
||||
modifier = modifier
|
||||
.padding(end = trailingInset)
|
||||
.morphChrome(ChromeSlot.Pill),
|
||||
) {
|
||||
// The label cross-fades and the pill eases to its width, so cycling
|
||||
// views turns it over rather than snapping it (#184).
|
||||
|
||||
@@ -97,6 +97,9 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import de.jeanlucmakiola.calendula.ui.common.morphChrome
|
||||
import de.jeanlucmakiola.calendula.ui.common.ViewMorphChrome
|
||||
import de.jeanlucmakiola.calendula.ui.common.ChromeSlot
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
@@ -237,6 +240,9 @@ fun MonthScreen(
|
||||
}
|
||||
|
||||
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
||||
// The bar and FAB hold still through a view switch, but not over an open drawer.
|
||||
val chromeMorph = drawerState.currentValue == DrawerValue.Closed &&
|
||||
drawerState.targetValue == DrawerValue.Closed
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val scrolling = viewStyle.isScrolling
|
||||
@@ -420,38 +426,42 @@ fun MonthScreen(
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
MonthTopBar(
|
||||
title = topBarTitle,
|
||||
shortTitle = topBarShortTitle,
|
||||
titleDate = LocalDate(titleMonth.year, titleMonth.month, 1),
|
||||
selectedView = selectedView,
|
||||
onCycleView = { onSelectView(selectedView.next(quickSwitchViews)) },
|
||||
quickSwitchViews = quickSwitchViews,
|
||||
onOpenDrawer = { scope.launch { drawerState.open() } },
|
||||
onOpenSearch = onOpenSearch,
|
||||
onJumpToDate = jumpToDate,
|
||||
showTodayButton = todayInToolbar,
|
||||
onToday = jumpToToday,
|
||||
)
|
||||
ViewMorphChrome(chromeMorph) {
|
||||
MonthTopBar(
|
||||
title = topBarTitle,
|
||||
shortTitle = topBarShortTitle,
|
||||
titleDate = LocalDate(titleMonth.year, titleMonth.month, 1),
|
||||
selectedView = selectedView,
|
||||
onCycleView = { onSelectView(selectedView.next(quickSwitchViews)) },
|
||||
quickSwitchViews = quickSwitchViews,
|
||||
onOpenDrawer = { scope.launch { drawerState.open() } },
|
||||
onOpenSearch = onOpenSearch,
|
||||
onJumpToDate = jumpToDate,
|
||||
showTodayButton = todayInToolbar,
|
||||
onToday = jumpToToday,
|
||||
)
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
CalendarFabColumn(
|
||||
todayVisible = !isOnCurrentMonth && !todayInToolbar,
|
||||
todayText = stringResource(R.string.month_today_action),
|
||||
onToday = jumpToToday,
|
||||
onCreate = {
|
||||
// Split has a selected day; the other styles anchor on
|
||||
// today when its month is shown, else the 1st.
|
||||
onCreateEvent(
|
||||
when {
|
||||
viewStyle == MonthViewStyle.Split -> selectedDate
|
||||
isOnCurrentMonth -> today
|
||||
else -> LocalDate(titleMonth.year, titleMonth.month, 1)
|
||||
},
|
||||
null,
|
||||
)
|
||||
},
|
||||
)
|
||||
ViewMorphChrome(chromeMorph) {
|
||||
CalendarFabColumn(
|
||||
todayVisible = !isOnCurrentMonth && !todayInToolbar,
|
||||
todayText = stringResource(R.string.month_today_action),
|
||||
onToday = jumpToToday,
|
||||
onCreate = {
|
||||
// Split has a selected day; the other styles anchor on
|
||||
// today when its month is shown, else the 1st.
|
||||
onCreateEvent(
|
||||
when {
|
||||
viewStyle == MonthViewStyle.Split -> selectedDate
|
||||
isOnCurrentMonth -> today
|
||||
else -> LocalDate(titleMonth.year, titleMonth.month, 1)
|
||||
},
|
||||
null,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
) { innerPadding ->
|
||||
// Hoisted above every style's grid: a dragged chip is drawn in an
|
||||
@@ -810,7 +820,7 @@ private fun MonthTopBar(
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onOpenDrawer) {
|
||||
IconButton(onClick = onOpenDrawer, modifier = Modifier.morphChrome(ChromeSlot.Menu)) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Menu,
|
||||
contentDescription = stringResource(R.string.month_open_menu),
|
||||
@@ -819,7 +829,7 @@ private fun MonthTopBar(
|
||||
},
|
||||
actions = {
|
||||
TodayAction(show = showTodayButton, onToday = onToday)
|
||||
IconButton(onClick = onOpenSearch) {
|
||||
IconButton(onClick = onOpenSearch, modifier = Modifier.morphChrome(ChromeSlot.Search)) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Search,
|
||||
contentDescription = stringResource(R.string.search_action),
|
||||
|
||||
@@ -68,6 +68,9 @@ import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.BiasAlignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import de.jeanlucmakiola.calendula.ui.common.morphChrome
|
||||
import de.jeanlucmakiola.calendula.ui.common.ViewMorphChrome
|
||||
import de.jeanlucmakiola.calendula.ui.common.ChromeSlot
|
||||
import de.jeanlucmakiola.calendula.ui.common.LocalViewMorph
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
@@ -351,6 +354,9 @@ private fun PagedTimelineScreen(
|
||||
}
|
||||
|
||||
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
||||
// The bar and FAB hold still through a view switch, but not over an open drawer.
|
||||
val chromeMorph = drawerState.currentValue == DrawerValue.Closed &&
|
||||
drawerState.targetValue == DrawerValue.Closed
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
// A new grid numbers the pages differently, so it gets a pager of its own,
|
||||
@@ -472,31 +478,35 @@ private fun PagedTimelineScreen(
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
WeekTopBar(
|
||||
kind = titleKind,
|
||||
weekStart = weekStart,
|
||||
weekEnd = weekEnd,
|
||||
currentYear = today.year,
|
||||
selectedView = view,
|
||||
onCycleView = { onSelectView(view.next(quickSwitchViews)) },
|
||||
quickSwitchViews = quickSwitchViews,
|
||||
onOpenDrawer = { scope.launch { drawerState.open() } },
|
||||
onOpenSearch = onOpenSearch,
|
||||
onJumpToDate = jumpToDate,
|
||||
showTodayButton = todayInToolbar,
|
||||
onToday = jumpToToday,
|
||||
)
|
||||
ViewMorphChrome(chromeMorph) {
|
||||
WeekTopBar(
|
||||
kind = titleKind,
|
||||
weekStart = weekStart,
|
||||
weekEnd = weekEnd,
|
||||
currentYear = today.year,
|
||||
selectedView = view,
|
||||
onCycleView = { onSelectView(view.next(quickSwitchViews)) },
|
||||
quickSwitchViews = quickSwitchViews,
|
||||
onOpenDrawer = { scope.launch { drawerState.open() } },
|
||||
onOpenSearch = onOpenSearch,
|
||||
onJumpToDate = jumpToDate,
|
||||
showTodayButton = todayInToolbar,
|
||||
onToday = jumpToToday,
|
||||
)
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
CalendarFabColumn(
|
||||
todayVisible = !isOnCurrentWeek && !todayInToolbar,
|
||||
todayText = stringResource(titleKind.todayText),
|
||||
onToday = jumpToToday,
|
||||
onCreate = {
|
||||
// Anchor on today when it's in view, else the page's first day.
|
||||
onCreateEvent(if (isOnCurrentWeek) today else weekStart, null)
|
||||
},
|
||||
)
|
||||
ViewMorphChrome(chromeMorph) {
|
||||
CalendarFabColumn(
|
||||
todayVisible = !isOnCurrentWeek && !todayInToolbar,
|
||||
todayText = stringResource(titleKind.todayText),
|
||||
onToday = jumpToToday,
|
||||
onCreate = {
|
||||
// Anchor on today when it's in view, else the page's first day.
|
||||
onCreateEvent(if (isOnCurrentWeek) today else weekStart, null)
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
) { innerPadding ->
|
||||
CompositionLocalProvider(LocalDimCutoff provides dimCutoff) {
|
||||
@@ -955,7 +965,7 @@ private fun WeekTopBar(
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onOpenDrawer) {
|
||||
IconButton(onClick = onOpenDrawer, modifier = Modifier.morphChrome(ChromeSlot.Menu)) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Menu,
|
||||
contentDescription = stringResource(R.string.month_open_menu),
|
||||
@@ -964,7 +974,7 @@ private fun WeekTopBar(
|
||||
},
|
||||
actions = {
|
||||
TodayAction(show = showTodayButton, onToday = onToday)
|
||||
IconButton(onClick = onOpenSearch) {
|
||||
IconButton(onClick = onOpenSearch, modifier = Modifier.morphChrome(ChromeSlot.Search)) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Search,
|
||||
contentDescription = stringResource(R.string.search_action),
|
||||
|
||||
Reference in New Issue
Block a user