mirror of
https://github.com/null2264/yokai.git
synced 2025-06-21 02:34:39 +00:00
wip: Preparing three rows app bar
Issue: - I broke the collapsing app bar again for expanded top app bar :^)
This commit is contained in:
parent
258708b038
commit
678fba61b8
7 changed files with 578 additions and 113 deletions
|
@ -2,6 +2,7 @@ package yokai.presentation
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import androidx.compose.foundation.basicMarquee
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
@ -9,11 +10,7 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
|
||||||
import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
|
import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
|
||||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
|
||||||
import androidx.compose.material3.rememberTopAppBarState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.SideEffect
|
import androidx.compose.runtime.SideEffect
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
@ -28,6 +25,10 @@ import dev.icerock.moko.resources.compose.stringResource
|
||||||
import yokai.i18n.MR
|
import yokai.i18n.MR
|
||||||
import yokai.presentation.component.ToolTipButton
|
import yokai.presentation.component.ToolTipButton
|
||||||
import yokai.presentation.core.ExpandedAppBar
|
import yokai.presentation.core.ExpandedAppBar
|
||||||
|
import yokai.presentation.core.TopAppBar
|
||||||
|
import yokai.presentation.core.TopAppBarScrollBehavior
|
||||||
|
import yokai.presentation.core.enterAlwaysScrollBehavior
|
||||||
|
import yokai.presentation.core.rememberTopAppBarState
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun YokaiScaffold(
|
fun YokaiScaffold(
|
||||||
|
@ -43,7 +44,7 @@ fun YokaiScaffold(
|
||||||
snackbarHost: @Composable () -> Unit = {},
|
snackbarHost: @Composable () -> Unit = {},
|
||||||
content: @Composable (PaddingValues) -> Unit,
|
content: @Composable (PaddingValues) -> Unit,
|
||||||
) {
|
) {
|
||||||
val scrollBehaviorOrDefault = scrollBehavior ?: TopAppBarDefaults.enterAlwaysScrollBehavior(state = rememberTopAppBarState())
|
val scrollBehaviorOrDefault = scrollBehavior ?: enterAlwaysScrollBehavior(state = rememberTopAppBarState())
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
val useDarkIcons = MaterialTheme.colorScheme.surface.luminance() > .5
|
val useDarkIcons = MaterialTheme.colorScheme.surface.luminance() > .5
|
||||||
val (color, scrolledColor) = getTopAppBarColor(title)
|
val (color, scrolledColor) = getTopAppBarColor(title)
|
||||||
|
@ -63,7 +64,10 @@ fun YokaiScaffold(
|
||||||
when (appBarType) {
|
when (appBarType) {
|
||||||
AppBarType.SMALL -> TopAppBar(
|
AppBarType.SMALL -> TopAppBar(
|
||||||
title = {
|
title = {
|
||||||
Text(text = title)
|
Text(
|
||||||
|
modifier = Modifier.basicMarquee(),
|
||||||
|
text = title,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
// modifier = Modifier.statusBarsPadding(),
|
// modifier = Modifier.statusBarsPadding(),
|
||||||
colors = topAppBarColors(
|
colors = topAppBarColors(
|
||||||
|
@ -82,7 +86,10 @@ fun YokaiScaffold(
|
||||||
)
|
)
|
||||||
AppBarType.LARGE -> ExpandedAppBar(
|
AppBarType.LARGE -> ExpandedAppBar(
|
||||||
title = {
|
title = {
|
||||||
Text(text = title)
|
Text(
|
||||||
|
modifier = Modifier.basicMarquee(),
|
||||||
|
text = title,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
// modifier = Modifier.statusBarsPadding(),
|
// modifier = Modifier.statusBarsPadding(),
|
||||||
colors = topAppBarColors(
|
colors = topAppBarColors(
|
||||||
|
|
|
@ -11,8 +11,6 @@ import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
|
||||||
import androidx.compose.material3.rememberTopAppBarState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
@ -41,6 +39,8 @@ import yokai.presentation.AppBarType
|
||||||
import yokai.presentation.YokaiScaffold
|
import yokai.presentation.YokaiScaffold
|
||||||
import yokai.presentation.component.EmptyScreen
|
import yokai.presentation.component.EmptyScreen
|
||||||
import yokai.presentation.component.ToolTipButton
|
import yokai.presentation.component.ToolTipButton
|
||||||
|
import yokai.presentation.core.enterAlwaysScrollBehavior
|
||||||
|
import yokai.presentation.core.rememberTopAppBarState
|
||||||
import yokai.presentation.extension.repo.component.ExtensionRepoInput
|
import yokai.presentation.extension.repo.component.ExtensionRepoInput
|
||||||
import yokai.presentation.extension.repo.component.ExtensionRepoItem
|
import yokai.presentation.extension.repo.component.ExtensionRepoItem
|
||||||
import yokai.util.Screen
|
import yokai.util.Screen
|
||||||
|
@ -67,7 +67,7 @@ class ExtensionRepoScreen(
|
||||||
onNavigationIconClicked = onBackPress,
|
onNavigationIconClicked = onBackPress,
|
||||||
title = title,
|
title = title,
|
||||||
appBarType = AppBarType.SMALL,
|
appBarType = AppBarType.SMALL,
|
||||||
scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(
|
scrollBehavior = enterAlwaysScrollBehavior(
|
||||||
state = rememberTopAppBarState(),
|
state = rememberTopAppBarState(),
|
||||||
canScroll = { listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0 },
|
canScroll = { listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0 },
|
||||||
),
|
),
|
||||||
|
|
|
@ -7,8 +7,6 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
|
||||||
import androidx.compose.material3.rememberTopAppBarState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
@ -29,8 +27,10 @@ import yokai.presentation.component.Gap
|
||||||
import yokai.presentation.component.preference.Preference
|
import yokai.presentation.component.preference.Preference
|
||||||
import yokai.presentation.component.preference.PreferenceItem
|
import yokai.presentation.component.preference.PreferenceItem
|
||||||
import yokai.presentation.component.preference.widget.PreferenceGroupHeader
|
import yokai.presentation.component.preference.widget.PreferenceGroupHeader
|
||||||
|
import yokai.presentation.core.TopAppBarScrollBehavior
|
||||||
import yokai.presentation.core.drawVerticalScrollbar
|
import yokai.presentation.core.drawVerticalScrollbar
|
||||||
import yokai.presentation.core.enterAlwaysCollapsedScrollBehavior
|
import yokai.presentation.core.enterAlwaysCollapsedScrollBehavior
|
||||||
|
import yokai.presentation.core.rememberTopAppBarState
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsScaffold(
|
fun SettingsScaffold(
|
||||||
|
|
|
@ -6,8 +6,6 @@ import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Public
|
import androidx.compose.material.icons.outlined.Public
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
|
||||||
import androidx.compose.material3.rememberTopAppBarState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
|
@ -23,6 +21,8 @@ import yokai.i18n.MR
|
||||||
import yokai.presentation.AppBarType
|
import yokai.presentation.AppBarType
|
||||||
import yokai.presentation.YokaiScaffold
|
import yokai.presentation.YokaiScaffold
|
||||||
import yokai.presentation.component.ToolTipButton
|
import yokai.presentation.component.ToolTipButton
|
||||||
|
import yokai.presentation.core.enterAlwaysScrollBehavior
|
||||||
|
import yokai.presentation.core.rememberTopAppBarState
|
||||||
import yokai.util.Screen
|
import yokai.util.Screen
|
||||||
|
|
||||||
class AboutLibraryLicenseScreen(
|
class AboutLibraryLicenseScreen(
|
||||||
|
@ -45,7 +45,7 @@ class AboutLibraryLicenseScreen(
|
||||||
},
|
},
|
||||||
title = name,
|
title = name,
|
||||||
appBarType = AppBarType.SMALL,
|
appBarType = AppBarType.SMALL,
|
||||||
scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(
|
scrollBehavior = enterAlwaysScrollBehavior(
|
||||||
state = rememberTopAppBarState(),
|
state = rememberTopAppBarState(),
|
||||||
),
|
),
|
||||||
actions = {
|
actions = {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package yokai.presentation.settings.screen.about
|
package yokai.presentation.settings.screen.about
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
|
@ -13,6 +12,7 @@ import eu.kanade.tachiyomi.util.compose.currentOrThrow
|
||||||
import yokai.i18n.MR
|
import yokai.i18n.MR
|
||||||
import yokai.presentation.AppBarType
|
import yokai.presentation.AppBarType
|
||||||
import yokai.presentation.YokaiScaffold
|
import yokai.presentation.YokaiScaffold
|
||||||
|
import yokai.presentation.core.pinnedScrollBehavior
|
||||||
import yokai.util.Screen
|
import yokai.util.Screen
|
||||||
|
|
||||||
class AboutLicenseScreen : Screen() {
|
class AboutLicenseScreen : Screen() {
|
||||||
|
@ -25,7 +25,7 @@ class AboutLicenseScreen : Screen() {
|
||||||
onNavigationIconClicked = backPress,
|
onNavigationIconClicked = backPress,
|
||||||
title = stringResource(MR.strings.open_source_licenses),
|
title = stringResource(MR.strings.open_source_licenses),
|
||||||
appBarType = AppBarType.SMALL,
|
appBarType = AppBarType.SMALL,
|
||||||
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(),
|
scrollBehavior = pinnedScrollBehavior(),
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
LibrariesContainer(
|
LibrariesContainer(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
|
|
@ -18,7 +18,6 @@ import androidx.compose.material.icons.outlined.Public
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.rememberTopAppBarState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
@ -66,6 +65,7 @@ import yokai.presentation.core.enterAlwaysCollapsedScrollBehavior
|
||||||
import yokai.presentation.core.icons.CustomIcons
|
import yokai.presentation.core.icons.CustomIcons
|
||||||
import yokai.presentation.core.icons.Discord
|
import yokai.presentation.core.icons.Discord
|
||||||
import yokai.presentation.core.icons.GitHub
|
import yokai.presentation.core.icons.GitHub
|
||||||
|
import yokai.presentation.core.rememberTopAppBarState
|
||||||
import yokai.presentation.settings.SettingsScaffold
|
import yokai.presentation.settings.SettingsScaffold
|
||||||
import yokai.util.Screen
|
import yokai.util.Screen
|
||||||
import yokai.util.lang.getString
|
import yokai.util.lang.getString
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package yokai.presentation.core
|
package yokai.presentation.core
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.core.AnimationSpec
|
import androidx.compose.animation.core.AnimationSpec
|
||||||
import androidx.compose.animation.core.AnimationState
|
import androidx.compose.animation.core.AnimationState
|
||||||
import androidx.compose.animation.core.CubicBezierEasing
|
import androidx.compose.animation.core.CubicBezierEasing
|
||||||
|
@ -21,19 +22,26 @@ import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.only
|
import androidx.compose.foundation.layout.only
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.LocalContentColor
|
import androidx.compose.material3.LocalContentColor
|
||||||
import androidx.compose.material3.LocalTextStyle
|
import androidx.compose.material3.LocalTextStyle
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.TopAppBarColors
|
import androidx.compose.material3.TopAppBarColors
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
|
||||||
import androidx.compose.material3.TopAppBarState
|
|
||||||
import androidx.compose.material3.rememberTopAppBarState
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.SideEffect
|
import androidx.compose.runtime.SideEffect
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.listSaver
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.toMutableStateList
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clipToBounds
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
|
@ -62,6 +70,35 @@ import kotlin.math.abs
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TopAppBar(
|
||||||
|
title: @Composable () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
navigationIcon: @Composable () -> Unit = {},
|
||||||
|
actions: @Composable RowScope.() -> Unit = {},
|
||||||
|
expandedHeight: Dp = TopAppBarDefaults.TopAppBarExpandedHeight,
|
||||||
|
windowInsets: WindowInsets = TopAppBarDefaults.windowInsets,
|
||||||
|
colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(),
|
||||||
|
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||||
|
) =
|
||||||
|
SingleRowTopAppBar(
|
||||||
|
modifier = modifier,
|
||||||
|
title = title,
|
||||||
|
titleTextStyle = MaterialTheme.typography.titleLarge,
|
||||||
|
centeredTitle = false,
|
||||||
|
navigationIcon = navigationIcon,
|
||||||
|
actions = actions,
|
||||||
|
expandedHeight =
|
||||||
|
if (expandedHeight == Dp.Unspecified || expandedHeight == Dp.Infinity) {
|
||||||
|
TopAppBarDefaults.TopAppBarExpandedHeight
|
||||||
|
} else {
|
||||||
|
expandedHeight
|
||||||
|
},
|
||||||
|
windowInsets = windowInsets,
|
||||||
|
colors = colors,
|
||||||
|
scrollBehavior = scrollBehavior
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Composable replacement for [eu.kanade.tachiyomi.ui.base.ExpandedAppBarLayout]
|
* Composable replacement for [eu.kanade.tachiyomi.ui.base.ExpandedAppBarLayout]
|
||||||
*
|
*
|
||||||
|
@ -87,14 +124,99 @@ fun ExpandedAppBar(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
navigationIcon = navigationIcon,
|
navigationIcon = navigationIcon,
|
||||||
actions = actions,
|
actions = actions,
|
||||||
collapsedHeight = CollapsedContainerHeight,
|
topHeight = CollapsedContainerHeight,
|
||||||
expandedHeight = ExpandedContainerHeight,
|
bottomHeight = ExpandedContainerHeight - CollapsedContainerHeight,
|
||||||
windowInsets = windowInsets,
|
windowInsets = windowInsets,
|
||||||
colors = colors,
|
colors = colors,
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SingleRowTopAppBar(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
title: @Composable () -> Unit,
|
||||||
|
titleTextStyle: TextStyle,
|
||||||
|
centeredTitle: Boolean,
|
||||||
|
navigationIcon: @Composable () -> Unit,
|
||||||
|
actions: @Composable RowScope.() -> Unit,
|
||||||
|
expandedHeight: Dp,
|
||||||
|
windowInsets: WindowInsets,
|
||||||
|
colors: TopAppBarColors,
|
||||||
|
scrollBehavior: TopAppBarScrollBehavior?
|
||||||
|
) {
|
||||||
|
require(expandedHeight.isSpecified && expandedHeight.isFinite) {
|
||||||
|
"The expandedHeight is expected to be specified and finite"
|
||||||
|
}
|
||||||
|
val expandedHeightPx = with(LocalDensity.current) { expandedHeight.toPx().coerceAtLeast(0f) }
|
||||||
|
SideEffect {
|
||||||
|
// Sets the app bar's height offset to collapse the entire bar's height when content is
|
||||||
|
// scrolled.
|
||||||
|
if (scrollBehavior?.state?.heights != listOf(expandedHeightPx)) {
|
||||||
|
scrollBehavior?.state?.heights?.clear()
|
||||||
|
scrollBehavior?.state?.heights?.addAll(listOf(expandedHeightPx))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtain the container color from the TopAppBarColors using the `overlapFraction`. This
|
||||||
|
// ensures that the colors will adjust whether the app bar behavior is pinned or scrolled.
|
||||||
|
// This may potentially animate or interpolate a transition between the container-color and the
|
||||||
|
// container's scrolled-color according to the app bar's scroll state.
|
||||||
|
val colorTransitionFraction by remember(scrollBehavior) {
|
||||||
|
// derivedStateOf to prevent redundant recompositions when the content scrolls.
|
||||||
|
derivedStateOf {
|
||||||
|
val overlappingFraction = scrollBehavior?.state?.overlappedFraction ?: 0f
|
||||||
|
if (overlappingFraction > 0.01f) 1f else 0f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val appBarContainerColor by animateColorAsState(
|
||||||
|
targetValue = lerp(
|
||||||
|
colors.containerColor,
|
||||||
|
colors.scrolledContainerColor,
|
||||||
|
FastOutLinearInEasing.transform(colorTransitionFraction)
|
||||||
|
),
|
||||||
|
animationSpec = spring(stiffness = Spring.StiffnessMediumLow)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Wrap the given actions in a Row.
|
||||||
|
val actionsRow =
|
||||||
|
@Composable {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
content = actions
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compose a Surface with a TopAppBarLayout content.
|
||||||
|
// The surface's background color is animated as specified above.
|
||||||
|
// The height of the app bar is determined by subtracting the bar's height offset from the
|
||||||
|
// app bar's defined constant height value (i.e. the ContainerHeight token).
|
||||||
|
Surface(modifier = modifier, color = appBarContainerColor) {
|
||||||
|
AppBarLayout(
|
||||||
|
modifier =
|
||||||
|
Modifier.windowInsetsPadding(windowInsets)
|
||||||
|
// clip after padding so we don't show the title over the inset area
|
||||||
|
.clipToBounds()
|
||||||
|
.heightIn(max = expandedHeight),
|
||||||
|
scrolledOffset = { scrollBehavior?.state?.heightOffset ?: 0f },
|
||||||
|
navigationIconContentColor = colors.navigationIconContentColor,
|
||||||
|
titleContentColor = colors.titleContentColor,
|
||||||
|
actionIconContentColor = colors.actionIconContentColor,
|
||||||
|
title = title,
|
||||||
|
titleTextStyle = titleTextStyle,
|
||||||
|
titleAlpha = 1f,
|
||||||
|
titleVerticalArrangement = Arrangement.Center,
|
||||||
|
titleHorizontalArrangement =
|
||||||
|
if (centeredTitle) Arrangement.Center else Arrangement.Start,
|
||||||
|
titleBottomPadding = 0,
|
||||||
|
hideTitleSemantics = false,
|
||||||
|
navigationIcon = navigationIcon,
|
||||||
|
actions = actionsRow,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun TwoRowsTopAppBar(
|
private fun TwoRowsTopAppBar(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
@ -105,35 +227,33 @@ private fun TwoRowsTopAppBar(
|
||||||
smallTitleTextStyle: TextStyle,
|
smallTitleTextStyle: TextStyle,
|
||||||
navigationIcon: @Composable () -> Unit,
|
navigationIcon: @Composable () -> Unit,
|
||||||
actions: @Composable RowScope.() -> Unit,
|
actions: @Composable RowScope.() -> Unit,
|
||||||
collapsedHeight: Dp,
|
topHeight: Dp,
|
||||||
expandedHeight: Dp,
|
bottomHeight: Dp,
|
||||||
windowInsets: WindowInsets,
|
windowInsets: WindowInsets,
|
||||||
colors: TopAppBarColors,
|
colors: TopAppBarColors,
|
||||||
scrollBehavior: TopAppBarScrollBehavior?
|
scrollBehavior: TopAppBarScrollBehavior?
|
||||||
) {
|
) {
|
||||||
require(collapsedHeight.isSpecified && collapsedHeight.isFinite) {
|
require(topHeight.isSpecified && topHeight.isFinite) {
|
||||||
"The collapsedHeight is expected to be specified and finite"
|
"The topHeight is expected to be specified and finite"
|
||||||
}
|
}
|
||||||
require(expandedHeight.isSpecified && expandedHeight.isFinite) {
|
require(bottomHeight.isSpecified && bottomHeight.isFinite) {
|
||||||
"The expandedHeight is expected to be specified and finite"
|
"The bottomHeight is expected to be specified and finite"
|
||||||
}
|
}
|
||||||
require(expandedHeight >= collapsedHeight) {
|
val topHeightPx: Float
|
||||||
"The expandedHeight is expected to be greater or equal to the collapsedHeight"
|
val bottomHeightPx: Float
|
||||||
}
|
|
||||||
val expandedHeightPx: Float
|
|
||||||
val collapsedHeightPx: Float
|
|
||||||
val titleBottomPaddingPx: Int
|
val titleBottomPaddingPx: Int
|
||||||
LocalDensity.current.run {
|
LocalDensity.current.run {
|
||||||
expandedHeightPx = expandedHeight.toPx()
|
topHeightPx = topHeight.toPx()
|
||||||
collapsedHeightPx = collapsedHeight.toPx()
|
bottomHeightPx = bottomHeight.toPx()
|
||||||
titleBottomPaddingPx = titleBottomPadding.roundToPx()
|
titleBottomPaddingPx = titleBottomPadding.roundToPx()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sets the app bar's height offset limit to hide just the bottom title area and keep top title
|
// Sets the app bar's height offset limit to hide just the bottom title area and keep top title
|
||||||
// visible when collapsed.
|
// visible when collapsed.
|
||||||
SideEffect {
|
SideEffect {
|
||||||
if (scrollBehavior?.state?.heightOffsetLimit != -expandedHeightPx) {
|
if (scrollBehavior?.state?.heights != listOf(topHeightPx, bottomHeightPx)) {
|
||||||
scrollBehavior?.state?.heightOffsetLimit = -expandedHeightPx
|
scrollBehavior?.state?.heights?.clear()
|
||||||
|
scrollBehavior?.state?.heights?.addAll(listOf(topHeightPx, bottomHeightPx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,7 +262,7 @@ private fun TwoRowsTopAppBar(
|
||||||
// collapse.
|
// collapse.
|
||||||
// This will potentially animate or interpolate a transition between the container color and the
|
// This will potentially animate or interpolate a transition between the container color and the
|
||||||
// container's scrolled color according to the app bar's scroll state.
|
// container's scrolled color according to the app bar's scroll state.
|
||||||
val colorTransitionFraction = scrollBehavior?.state?.bottomCollapsedFraction(collapsedHeightPx, expandedHeightPx) ?: 0f
|
val colorTransitionFraction = scrollBehavior?.state?.collapsedFraction(1) ?: 0f
|
||||||
|
|
||||||
val appBarContainerColor =
|
val appBarContainerColor =
|
||||||
lerp(
|
lerp(
|
||||||
|
@ -175,12 +295,9 @@ private fun TwoRowsTopAppBar(
|
||||||
.windowInsetsPadding(windowInsets)
|
.windowInsetsPadding(windowInsets)
|
||||||
// clip after padding so we don't show the title over the inset area
|
// clip after padding so we don't show the title over the inset area
|
||||||
.clipToBounds()
|
.clipToBounds()
|
||||||
.heightIn(max = collapsedHeight),
|
.heightIn(max = topHeight),
|
||||||
scrolledOffset = {
|
scrolledOffset = {
|
||||||
scrollBehavior?.state?.topHeightOffset(
|
scrollBehavior?.state?.heightOffset(0) ?: 0f
|
||||||
topHeightPx = collapsedHeightPx,
|
|
||||||
totalHeightPx = expandedHeightPx,
|
|
||||||
) ?: 0f
|
|
||||||
},
|
},
|
||||||
navigationIconContentColor = colors.navigationIconContentColor,
|
navigationIconContentColor = colors.navigationIconContentColor,
|
||||||
titleContentColor = colors.titleContentColor,
|
titleContentColor = colors.titleContentColor,
|
||||||
|
@ -203,12 +320,9 @@ private fun TwoRowsTopAppBar(
|
||||||
// padding will always be applied by the layout above
|
// padding will always be applied by the layout above
|
||||||
.windowInsetsPadding(windowInsets.only(WindowInsetsSides.Horizontal))
|
.windowInsetsPadding(windowInsets.only(WindowInsetsSides.Horizontal))
|
||||||
.clipToBounds()
|
.clipToBounds()
|
||||||
.heightIn(max = expandedHeight - collapsedHeight),
|
.heightIn(max = bottomHeight),
|
||||||
scrolledOffset = {
|
scrolledOffset = {
|
||||||
scrollBehavior?.state?.bottomHeightOffset(
|
scrollBehavior?.state?.heightOffset(1) ?: 0f
|
||||||
topHeightPx = collapsedHeightPx,
|
|
||||||
totalHeightPx = expandedHeightPx,
|
|
||||||
) ?: 0f
|
|
||||||
},
|
},
|
||||||
navigationIconContentColor = colors.navigationIconContentColor,
|
navigationIconContentColor = colors.navigationIconContentColor,
|
||||||
titleContentColor = colors.titleContentColor,
|
titleContentColor = colors.titleContentColor,
|
||||||
|
@ -227,6 +341,160 @@ private fun TwoRowsTopAppBar(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a [TopAppBarState] that is remembered across compositions.
|
||||||
|
*
|
||||||
|
* @param initialHeightOffsetLimit the initial value for [TopAppBarState.heightOffsetLimit], which
|
||||||
|
* represents the pixel limit that a top app bar is allowed to collapse when the scrollable
|
||||||
|
* content is scrolled
|
||||||
|
* @param initialHeightOffset the initial value for [TopAppBarState.heightOffset]. The initial
|
||||||
|
* offset height offset should be between zero and [initialHeightOffsetLimit].
|
||||||
|
* @param initialContentOffset the initial value for [TopAppBarState.contentOffset]
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun rememberTopAppBarState(
|
||||||
|
initialHeights: List<Float> = listOf(),
|
||||||
|
initialHeightOffset: Float = 0f,
|
||||||
|
initialContentOffset: Float = 0f,
|
||||||
|
): TopAppBarState {
|
||||||
|
return rememberSaveable(saver = TopAppBarState.Saver) {
|
||||||
|
TopAppBarState(initialHeights, initialHeightOffset, initialContentOffset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A state object that can be hoisted to control and observe the top app bar state. The state is
|
||||||
|
* read and updated by a [TopAppBarScrollBehavior] implementation.
|
||||||
|
*
|
||||||
|
* In most cases, this state will be created via [rememberTopAppBarState].
|
||||||
|
*
|
||||||
|
* @param initialHeightOffsetLimit the initial value for [TopAppBarState.heightOffsetLimit]
|
||||||
|
* @param initialHeightOffset the initial value for [TopAppBarState.heightOffset]
|
||||||
|
* @param initialContentOffset the initial value for [TopAppBarState.contentOffset]
|
||||||
|
*/
|
||||||
|
@ExperimentalMaterial3Api
|
||||||
|
@Stable
|
||||||
|
class TopAppBarState(
|
||||||
|
initialHeights: List<Float>,
|
||||||
|
initialHeightOffset: Float,
|
||||||
|
initialContentOffset: Float
|
||||||
|
) {
|
||||||
|
val heights = initialHeights.toMutableStateList()
|
||||||
|
val heightOffsetLimit get() = -heights.sum()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The top app bar's current height offset in pixels. This height offset is applied to the fixed
|
||||||
|
* height of the app bar to control the displayed height when content is being scrolled.
|
||||||
|
*
|
||||||
|
* Updates to the [heightOffset] value are coerced between zero and [heightOffsetLimit].
|
||||||
|
*/
|
||||||
|
var heightOffset: Float
|
||||||
|
get() = _heightOffset.floatValue
|
||||||
|
set(newOffset) {
|
||||||
|
_heightOffset.floatValue =
|
||||||
|
newOffset.fastCoerceIn(minimumValue = heightOffsetLimit, maximumValue = 0f)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun heightOffset(index: Int = Int.MIN_VALUE, raw: Boolean = false): Float {
|
||||||
|
if (index == Int.MIN_VALUE) return _heightOffset.floatValue
|
||||||
|
if (heights.isEmpty() || heights.size < index + 1) return 0f
|
||||||
|
|
||||||
|
val selectedHeight = heights[index]
|
||||||
|
val limit = heights.sum()
|
||||||
|
val rt = _heightOffset.value + (limit - selectedHeight)
|
||||||
|
if (raw) return rt
|
||||||
|
|
||||||
|
return rt.fastCoerceIn(-selectedHeight, 0f)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setHeightOffsetAgainstIndex(index: Int = Int.MIN_VALUE, offset: Float) {
|
||||||
|
if (index == Int.MIN_VALUE) {
|
||||||
|
_heightOffset.floatValue = offset.fastCoerceIn(heightOffsetLimit, 0f)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (heights.isEmpty() || heights.size < index + 1) return
|
||||||
|
|
||||||
|
_heightOffset.floatValue = offset.fastCoerceIn(heightOffsetLimit, -heights[index])
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The total offset of the content scrolled under the top app bar.
|
||||||
|
*
|
||||||
|
* The content offset is used to compute the [overlappedFraction], which can later be read by an
|
||||||
|
* implementation.
|
||||||
|
*
|
||||||
|
* This value is updated by a [TopAppBarScrollBehavior] whenever a nested scroll connection
|
||||||
|
* consumes scroll events. A common implementation would update the value to be the sum of all
|
||||||
|
* [NestedScrollConnection.onPostScroll] `consumed.y` values.
|
||||||
|
*/
|
||||||
|
var contentOffset by mutableFloatStateOf(initialContentOffset)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A value that represents the collapsed height percentage of the app bar.
|
||||||
|
*
|
||||||
|
* A `0.0` represents a fully expanded bar, and `1.0` represents a fully collapsed bar (computed
|
||||||
|
* as [heightOffset] / [heightOffsetLimit]).
|
||||||
|
*/
|
||||||
|
val collapsedFraction: Float
|
||||||
|
get() =
|
||||||
|
if (heights.isNotEmpty() || heightOffsetLimit != 0f) {
|
||||||
|
heightOffset / heightOffsetLimit
|
||||||
|
} else {
|
||||||
|
0f
|
||||||
|
}
|
||||||
|
|
||||||
|
fun collapsedFraction(index: Int = Int.MIN_VALUE): Float {
|
||||||
|
if (heights.isEmpty()) return 0f
|
||||||
|
if (index == Int.MIN_VALUE) return if (heightOffsetLimit != 0f) {
|
||||||
|
_heightOffset.floatValue / heightOffsetLimit
|
||||||
|
} else {
|
||||||
|
0f
|
||||||
|
}
|
||||||
|
if (heights.size < index + 1) return 0f
|
||||||
|
|
||||||
|
return heightOffset(index) / -heights[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A value that represents the percentage of the app bar area that is overlapping with the
|
||||||
|
* content scrolled behind it.
|
||||||
|
*
|
||||||
|
* A `0.0` indicates that the app bar does not overlap any content, while `1.0` indicates that
|
||||||
|
* the entire visible app bar area overlaps the scrolled content.
|
||||||
|
*/
|
||||||
|
val overlappedFraction: Float
|
||||||
|
get() =
|
||||||
|
if (heights.isNotEmpty() || heightOffsetLimit != 0f) {
|
||||||
|
1 -
|
||||||
|
((heightOffsetLimit - contentOffset).coerceIn(
|
||||||
|
minimumValue = heightOffsetLimit,
|
||||||
|
maximumValue = 0f
|
||||||
|
) / heightOffsetLimit)
|
||||||
|
} else {
|
||||||
|
0f
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/** The default [Saver] implementation for [TopAppBarState]. */
|
||||||
|
val Saver: Saver<TopAppBarState, *> =
|
||||||
|
listSaver(
|
||||||
|
save = { listOf(it.heightOffset, it.contentOffset) + it.heights.toList() },
|
||||||
|
restore = {
|
||||||
|
val list = it.toMutableList()
|
||||||
|
val initialHeightOffset = list.removeAt(0)
|
||||||
|
val initialContentOffset = list.removeAt(0)
|
||||||
|
TopAppBarState(
|
||||||
|
initialHeights = list.toMutableStateList(),
|
||||||
|
initialHeightOffset = initialHeightOffset,
|
||||||
|
initialContentOffset = initialContentOffset,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var _heightOffset = mutableFloatStateOf(initialHeightOffset)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun AppBarLayout(
|
private fun AppBarLayout(
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
|
@ -457,16 +725,17 @@ private fun interface ScrolledOffset {
|
||||||
private suspend fun settleAppBar(
|
private suspend fun settleAppBar(
|
||||||
state: TopAppBarState,
|
state: TopAppBarState,
|
||||||
velocity: Float,
|
velocity: Float,
|
||||||
topHeightPx: Float,
|
|
||||||
totalHeightPx: Float,
|
|
||||||
flingAnimationSpec: DecayAnimationSpec<Float>?,
|
flingAnimationSpec: DecayAnimationSpec<Float>?,
|
||||||
snapAnimationSpec: AnimationSpec<Float>?,
|
snapAnimationSpec: AnimationSpec<Float>?,
|
||||||
|
topOnly: Boolean = false,
|
||||||
): Velocity {
|
): Velocity {
|
||||||
// Check if the app bar is completely collapsed/expanded. If so, no need to settle the app bar,
|
// Check if the app bar is completely collapsed/expanded. If so, no need to settle the app bar,
|
||||||
// and just return Zero Velocity.
|
// and just return Zero Velocity.
|
||||||
// Note that we don't check for 0f due to float precision with the collapsedFraction
|
// Note that we don't check for 0f due to float precision with the collapsedFraction
|
||||||
// calculation.
|
// calculation.
|
||||||
if (state.topCollapsedFraction(topHeightPx, totalHeightPx) < 0.01f || state.topCollapsedFraction(topHeightPx, totalHeightPx) == 1f) {
|
if (topOnly && (state.collapsedFraction(0) < 0.01f || state.collapsedFraction(0) == 1f)) {
|
||||||
|
return Velocity.Zero
|
||||||
|
} else if (state.collapsedFraction < 0.01f || state.collapsedFraction == 1f) {
|
||||||
return Velocity.Zero
|
return Velocity.Zero
|
||||||
}
|
}
|
||||||
var remainingVelocity = velocity
|
var remainingVelocity = velocity
|
||||||
|
@ -491,52 +760,95 @@ private suspend fun settleAppBar(
|
||||||
}
|
}
|
||||||
// Snap if animation specs were provided.
|
// Snap if animation specs were provided.
|
||||||
if (snapAnimationSpec != null) {
|
if (snapAnimationSpec != null) {
|
||||||
if (state.topHeightOffset(topHeightPx, totalHeightPx) < 0 && state.topHeightOffset(topHeightPx, totalHeightPx) > -topHeightPx) {
|
when (topOnly) {
|
||||||
AnimationState(initialValue = state.topHeightOffset(topHeightPx, totalHeightPx)).animateTo(
|
true ->
|
||||||
if (state.topCollapsedFraction(topHeightPx, totalHeightPx) < 0.5f) {
|
if (state.heightOffset(0) < 0 && state.heightOffset(0) > -state.heights[0]) {
|
||||||
|
AnimationState(initialValue = state.heightOffset(0)).animateTo(
|
||||||
|
if (state.collapsedFraction(0) < 0.5f) {
|
||||||
0f
|
0f
|
||||||
} else {
|
} else {
|
||||||
-topHeightPx
|
-state.heights[0]
|
||||||
},
|
},
|
||||||
animationSpec = snapAnimationSpec
|
animationSpec = snapAnimationSpec
|
||||||
) {
|
) {
|
||||||
state.heightOffset = value + (topHeightPx - totalHeightPx)
|
state.heightOffset = value + (-state.heights[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
false ->
|
||||||
|
if (state.heightOffset < 0 && state.heightOffset > state.heightOffsetLimit) {
|
||||||
|
AnimationState(initialValue = state.heightOffset).animateTo(
|
||||||
|
if (state.collapsedFraction < 0.5f) {
|
||||||
|
0f
|
||||||
|
} else {
|
||||||
|
state.heightOffsetLimit
|
||||||
|
},
|
||||||
|
animationSpec = snapAnimationSpec
|
||||||
|
) {
|
||||||
|
state.heightOffset = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Velocity(0f, remainingVelocity)
|
return Velocity(0f, remainingVelocity)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TopAppBarScrollBehavior {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default values:
|
* A [TopAppBarState] that is attached to this behavior and is read and updated when scrolling
|
||||||
* - Top app bar height: 128px
|
* happens.
|
||||||
* - Total app bar height: 304px
|
|
||||||
* - Bottom app bar height: 176px
|
|
||||||
* - Top offset limit: (-(Total), (Top - Total)) = (-304px, -176px)
|
|
||||||
* - Bottom offset limit: ((Top - Total), 0) = (-176px, 0px)
|
|
||||||
*/
|
*/
|
||||||
|
val state: TopAppBarState
|
||||||
|
|
||||||
private fun TopAppBarState.rawTopHeightOffset(topHeightPx: Float, totalHeightPx: Float): Float {
|
/**
|
||||||
return heightOffset + (totalHeightPx - topHeightPx)
|
* Indicates whether the top app bar is pinned.
|
||||||
|
*
|
||||||
|
* A pinned app bar will stay fixed in place when content is scrolled and will not react to any
|
||||||
|
* drag gestures.
|
||||||
|
*/
|
||||||
|
val isPinned: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An optional [AnimationSpec] that defines how the top app bar snaps to either fully collapsed
|
||||||
|
* or fully extended state when a fling or a drag scrolled it into an intermediate position.
|
||||||
|
*/
|
||||||
|
val snapAnimationSpec: AnimationSpec<Float>?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An optional [DecayAnimationSpec] that defined how to fling the top app bar when the user
|
||||||
|
* flings the app bar itself, or the content below it.
|
||||||
|
*/
|
||||||
|
val flingAnimationSpec: DecayAnimationSpec<Float>?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [NestedScrollConnection] that should be attached to a [Modifier.nestedScroll] in order to
|
||||||
|
* keep track of the scroll events.
|
||||||
|
*/
|
||||||
|
val nestedScrollConnection: NestedScrollConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun TopAppBarState.topHeightOffset(topHeightPx: Float, totalHeightPx: Float): Float {
|
@Composable
|
||||||
return rawTopHeightOffset(topHeightPx, totalHeightPx).fastCoerceIn(-topHeightPx, 0f)
|
fun pinnedScrollBehavior(
|
||||||
}
|
state: TopAppBarState = rememberTopAppBarState(),
|
||||||
|
canScroll: () -> Boolean = { true }
|
||||||
|
): TopAppBarScrollBehavior = remember(state, canScroll) { PinnedScrollBehavior(state = state, canScroll = canScroll) }
|
||||||
|
|
||||||
private fun TopAppBarState.bottomHeightOffset(topHeightPx: Float, totalHeightPx: Float): Float {
|
@Composable
|
||||||
return heightOffset.fastCoerceIn(topHeightPx - totalHeightPx, 0f)
|
fun enterAlwaysScrollBehavior(
|
||||||
}
|
state: TopAppBarState = rememberTopAppBarState(),
|
||||||
|
canScroll: () -> Boolean = { true },
|
||||||
private fun TopAppBarState.topCollapsedFraction(topHeightPx: Float, totalHeightPx: Float): Float {
|
snapAnimationSpec: AnimationSpec<Float>? = spring(stiffness = Spring.StiffnessMediumLow),
|
||||||
val offset = topHeightOffset(topHeightPx, totalHeightPx)
|
flingAnimationSpec: DecayAnimationSpec<Float>? = rememberSplineBasedDecay()
|
||||||
return offset / -topHeightPx
|
): TopAppBarScrollBehavior =
|
||||||
}
|
remember(state, canScroll, snapAnimationSpec, flingAnimationSpec) {
|
||||||
|
EnterAlwaysScrollBehavior(
|
||||||
private fun TopAppBarState.bottomCollapsedFraction(topHeightPx: Float, totalHeightPx: Float): Float {
|
state = state,
|
||||||
val offset = bottomHeightOffset(topHeightPx, totalHeightPx)
|
snapAnimationSpec = snapAnimationSpec,
|
||||||
return offset / (topHeightPx - totalHeightPx)
|
flingAnimationSpec = flingAnimationSpec,
|
||||||
|
canScroll = canScroll
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -546,22 +858,106 @@ fun enterAlwaysCollapsedScrollBehavior(
|
||||||
isAtTop: () -> Boolean = { true },
|
isAtTop: () -> Boolean = { true },
|
||||||
snapAnimationSpec: AnimationSpec<Float>? = spring(stiffness = Spring.StiffnessMediumLow),
|
snapAnimationSpec: AnimationSpec<Float>? = spring(stiffness = Spring.StiffnessMediumLow),
|
||||||
flingAnimationSpec: DecayAnimationSpec<Float>? = rememberSplineBasedDecay()
|
flingAnimationSpec: DecayAnimationSpec<Float>? = rememberSplineBasedDecay()
|
||||||
): TopAppBarScrollBehavior {
|
): TopAppBarScrollBehavior =
|
||||||
val (topHeightPx, totalHeightPx) = with(LocalDensity.current) {
|
remember(state, canScroll, isAtTop, snapAnimationSpec, flingAnimationSpec) {
|
||||||
CollapsedContainerHeight.toPx() to ExpandedContainerHeight.toPx()
|
|
||||||
}
|
|
||||||
|
|
||||||
return remember(state, canScroll, isAtTop, snapAnimationSpec, flingAnimationSpec, topHeightPx, totalHeightPx) {
|
|
||||||
EnterAlwaysCollapsedScrollBehavior(
|
EnterAlwaysCollapsedScrollBehavior(
|
||||||
state = state,
|
state = state,
|
||||||
snapAnimationSpec = snapAnimationSpec,
|
snapAnimationSpec = snapAnimationSpec,
|
||||||
flingAnimationSpec = flingAnimationSpec,
|
flingAnimationSpec = flingAnimationSpec,
|
||||||
canScroll = canScroll,
|
canScroll = canScroll,
|
||||||
isAtTop = isAtTop,
|
isAtTop = isAtTop,
|
||||||
topHeightPx = topHeightPx,
|
|
||||||
totalHeightPx = totalHeightPx,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun exitUntilCollapsedScrollBehavior(
|
||||||
|
state: TopAppBarState = rememberTopAppBarState(),
|
||||||
|
canScroll: () -> Boolean = { true },
|
||||||
|
snapAnimationSpec: AnimationSpec<Float>? = spring(stiffness = Spring.StiffnessMediumLow),
|
||||||
|
flingAnimationSpec: DecayAnimationSpec<Float>? = rememberSplineBasedDecay()
|
||||||
|
): TopAppBarScrollBehavior =
|
||||||
|
remember(state, canScroll, snapAnimationSpec, flingAnimationSpec) {
|
||||||
|
ExitUntilCollapsedScrollBehavior(
|
||||||
|
state = state,
|
||||||
|
snapAnimationSpec = snapAnimationSpec,
|
||||||
|
flingAnimationSpec = flingAnimationSpec,
|
||||||
|
canScroll = canScroll
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class PinnedScrollBehavior(
|
||||||
|
override val state: TopAppBarState,
|
||||||
|
val canScroll: () -> Boolean = { true }
|
||||||
|
) : TopAppBarScrollBehavior {
|
||||||
|
override val isPinned: Boolean = true
|
||||||
|
override val snapAnimationSpec: AnimationSpec<Float>? = null
|
||||||
|
override val flingAnimationSpec: DecayAnimationSpec<Float>? = null
|
||||||
|
override var nestedScrollConnection =
|
||||||
|
object : NestedScrollConnection {
|
||||||
|
override fun onPostScroll(
|
||||||
|
consumed: Offset,
|
||||||
|
available: Offset,
|
||||||
|
source: NestedScrollSource
|
||||||
|
): Offset {
|
||||||
|
if (!canScroll()) return Offset.Zero
|
||||||
|
if (consumed.y == 0f && available.y > 0f) {
|
||||||
|
// Reset the total content offset to zero when scrolling all the way down.
|
||||||
|
// This will eliminate some float precision inaccuracies.
|
||||||
|
state.contentOffset = 0f
|
||||||
|
} else {
|
||||||
|
state.contentOffset += consumed.y
|
||||||
|
}
|
||||||
|
return Offset.Zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class EnterAlwaysScrollBehavior(
|
||||||
|
override val state: TopAppBarState,
|
||||||
|
override val snapAnimationSpec: AnimationSpec<Float>?,
|
||||||
|
override val flingAnimationSpec: DecayAnimationSpec<Float>?,
|
||||||
|
val canScroll: () -> Boolean = { true }
|
||||||
|
) : TopAppBarScrollBehavior {
|
||||||
|
override val isPinned: Boolean = false
|
||||||
|
override var nestedScrollConnection =
|
||||||
|
object : NestedScrollConnection {
|
||||||
|
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||||
|
if (!canScroll()) return Offset.Zero
|
||||||
|
val prevHeightOffset = state.heightOffset
|
||||||
|
state.heightOffset = state.heightOffset + available.y
|
||||||
|
return if (prevHeightOffset != state.heightOffset) {
|
||||||
|
// We're in the middle of top app bar collapse or expand.
|
||||||
|
// Consume only the scroll on the Y axis.
|
||||||
|
available.copy(x = 0f)
|
||||||
|
} else {
|
||||||
|
Offset.Zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPostScroll(
|
||||||
|
consumed: Offset,
|
||||||
|
available: Offset,
|
||||||
|
source: NestedScrollSource
|
||||||
|
): Offset {
|
||||||
|
if (!canScroll()) return Offset.Zero
|
||||||
|
state.contentOffset += consumed.y
|
||||||
|
if (state.heightOffset == 0f || state.heightOffset == state.heightOffsetLimit) {
|
||||||
|
if (consumed.y == 0f && available.y > 0f) {
|
||||||
|
// Reset the total content offset to zero when scrolling all the way down.
|
||||||
|
// This will eliminate some float precision inaccuracies.
|
||||||
|
state.contentOffset = 0f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.heightOffset = state.heightOffset + consumed.y
|
||||||
|
return Offset.Zero
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
||||||
|
val superConsumed = super.onPostFling(consumed, available)
|
||||||
|
return superConsumed +
|
||||||
|
settleAppBar(state, available.y, flingAnimationSpec, snapAnimationSpec)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class EnterAlwaysCollapsedScrollBehavior(
|
private class EnterAlwaysCollapsedScrollBehavior(
|
||||||
|
@ -571,27 +967,20 @@ private class EnterAlwaysCollapsedScrollBehavior(
|
||||||
val canScroll: () -> Boolean = { true },
|
val canScroll: () -> Boolean = { true },
|
||||||
// FIXME: See if it's possible to eliminate this argument
|
// FIXME: See if it's possible to eliminate this argument
|
||||||
val isAtTop: () -> Boolean = { true },
|
val isAtTop: () -> Boolean = { true },
|
||||||
val topHeightPx: Float,
|
|
||||||
val totalHeightPx: Float,
|
|
||||||
) : TopAppBarScrollBehavior {
|
) : TopAppBarScrollBehavior {
|
||||||
override val isPinned: Boolean = false
|
override val isPinned: Boolean = false
|
||||||
override var nestedScrollConnection =
|
override var nestedScrollConnection =
|
||||||
object : NestedScrollConnection {
|
object : NestedScrollConnection {
|
||||||
private fun TopAppBarState.setClampedOffsetIfAtTop(offset: Float) {
|
|
||||||
heightOffset = if (isAtTop()) {
|
|
||||||
offset
|
|
||||||
} else {
|
|
||||||
offset.fastCoerceIn(-totalHeightPx, (topHeightPx - totalHeightPx))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||||
// Don't intercept if scrolling down.
|
// Don't intercept if scrolling down.
|
||||||
if (!canScroll() || (available.y > 0f && state.rawTopHeightOffset(topHeightPx, totalHeightPx) >= 0f))
|
if (!canScroll() || (available.y > 0f && state.heightOffset(0) >= 0f))
|
||||||
return Offset.Zero
|
return Offset.Zero
|
||||||
|
|
||||||
val prevHeightOffset = state.heightOffset
|
val prevHeightOffset = state.heightOffset
|
||||||
state.setClampedOffsetIfAtTop(state.heightOffset + available.y)
|
state.setHeightOffsetAgainstIndex(
|
||||||
|
index = if (isAtTop()) Int.MIN_VALUE else 0,
|
||||||
|
offset = state.heightOffset + available.y,
|
||||||
|
)
|
||||||
return if (prevHeightOffset != state.heightOffset) {
|
return if (prevHeightOffset != state.heightOffset) {
|
||||||
// We're in the middle of top app bar collapse or expand.
|
// We're in the middle of top app bar collapse or expand.
|
||||||
// Consume only the scroll on the Y axis.
|
// Consume only the scroll on the Y axis.
|
||||||
|
@ -612,7 +1001,10 @@ private class EnterAlwaysCollapsedScrollBehavior(
|
||||||
if (available.y < 0f || consumed.y < 0f) {
|
if (available.y < 0f || consumed.y < 0f) {
|
||||||
// When scrolling up, just update the state's height offset.
|
// When scrolling up, just update the state's height offset.
|
||||||
val oldHeightOffset = state.heightOffset
|
val oldHeightOffset = state.heightOffset
|
||||||
state.setClampedOffsetIfAtTop(state.heightOffset + consumed.y)
|
state.setHeightOffsetAgainstIndex(
|
||||||
|
index = if (isAtTop()) Int.MIN_VALUE else 0,
|
||||||
|
offset = state.heightOffset + consumed.y,
|
||||||
|
)
|
||||||
return Offset(0f, state.heightOffset - oldHeightOffset)
|
return Offset(0f, state.heightOffset - oldHeightOffset)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -626,7 +1018,10 @@ private class EnterAlwaysCollapsedScrollBehavior(
|
||||||
// Adjust the height offset in case the consumed delta Y is less than what was
|
// Adjust the height offset in case the consumed delta Y is less than what was
|
||||||
// recorded as available delta Y in the pre-scroll.
|
// recorded as available delta Y in the pre-scroll.
|
||||||
val oldHeightOffset = state.heightOffset
|
val oldHeightOffset = state.heightOffset
|
||||||
state.setClampedOffsetIfAtTop(state.heightOffset + available.y)
|
state.setHeightOffsetAgainstIndex(
|
||||||
|
index = if (isAtTop()) Int.MIN_VALUE else 0,
|
||||||
|
offset = state.heightOffset + available.y,
|
||||||
|
)
|
||||||
return Offset(0f, state.heightOffset - oldHeightOffset)
|
return Offset(0f, state.heightOffset - oldHeightOffset)
|
||||||
}
|
}
|
||||||
return Offset.Zero
|
return Offset.Zero
|
||||||
|
@ -635,7 +1030,70 @@ private class EnterAlwaysCollapsedScrollBehavior(
|
||||||
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
||||||
val superConsumed = super.onPostFling(consumed, available)
|
val superConsumed = super.onPostFling(consumed, available)
|
||||||
return superConsumed +
|
return superConsumed +
|
||||||
settleAppBar(state, available.y, topHeightPx, totalHeightPx, flingAnimationSpec, snapAnimationSpec)
|
settleAppBar(state, available.y, flingAnimationSpec, snapAnimationSpec, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ExitUntilCollapsedScrollBehavior(
|
||||||
|
override val state: TopAppBarState,
|
||||||
|
override val snapAnimationSpec: AnimationSpec<Float>?,
|
||||||
|
override val flingAnimationSpec: DecayAnimationSpec<Float>?,
|
||||||
|
val canScroll: () -> Boolean = { true }
|
||||||
|
) : TopAppBarScrollBehavior {
|
||||||
|
override val isPinned: Boolean = false
|
||||||
|
override var nestedScrollConnection =
|
||||||
|
object : NestedScrollConnection {
|
||||||
|
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||||
|
// Don't intercept if scrolling down.
|
||||||
|
if (!canScroll() || available.y > 0f) return Offset.Zero
|
||||||
|
|
||||||
|
val prevHeightOffset = state.heightOffset
|
||||||
|
state.heightOffset += available.y
|
||||||
|
return if (prevHeightOffset != state.heightOffset) {
|
||||||
|
// We're in the middle of top app bar collapse or expand.
|
||||||
|
// Consume only the scroll on the Y axis.
|
||||||
|
available.copy(x = 0f)
|
||||||
|
} else {
|
||||||
|
Offset.Zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPostScroll(
|
||||||
|
consumed: Offset,
|
||||||
|
available: Offset,
|
||||||
|
source: NestedScrollSource
|
||||||
|
): Offset {
|
||||||
|
if (!canScroll()) return Offset.Zero
|
||||||
|
state.contentOffset += consumed.y
|
||||||
|
|
||||||
|
if (available.y < 0f || consumed.y < 0f) {
|
||||||
|
// When scrolling up, just update the state's height offset.
|
||||||
|
val oldHeightOffset = state.heightOffset
|
||||||
|
state.heightOffset += consumed.y
|
||||||
|
return Offset(0f, state.heightOffset - oldHeightOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (consumed.y == 0f && available.y > 0) {
|
||||||
|
// Reset the total content offset to zero when scrolling all the way down. This
|
||||||
|
// will eliminate some float precision inaccuracies.
|
||||||
|
state.contentOffset = 0f
|
||||||
|
}
|
||||||
|
|
||||||
|
if (available.y > 0f) {
|
||||||
|
// Adjust the height offset in case the consumed delta Y is less than what was
|
||||||
|
// recorded as available delta Y in the pre-scroll.
|
||||||
|
val oldHeightOffset = state.heightOffset
|
||||||
|
state.heightOffset += available.y
|
||||||
|
return Offset(0f, state.heightOffset - oldHeightOffset)
|
||||||
|
}
|
||||||
|
return Offset.Zero
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
||||||
|
val superConsumed = super.onPostFling(consumed, available)
|
||||||
|
return superConsumed +
|
||||||
|
settleAppBar(state, available.y, flingAnimationSpec, snapAnimationSpec)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue