mirror of
https://github.com/null2264/yokai.git
synced 2025-06-20 18:24:42 +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.os.Build
|
||||
import androidx.compose.foundation.basicMarquee
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
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.Scaffold
|
||||
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.TopAppBarScrollBehavior
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
|
@ -28,6 +25,10 @@ import dev.icerock.moko.resources.compose.stringResource
|
|||
import yokai.i18n.MR
|
||||
import yokai.presentation.component.ToolTipButton
|
||||
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
|
||||
fun YokaiScaffold(
|
||||
|
@ -43,7 +44,7 @@ fun YokaiScaffold(
|
|||
snackbarHost: @Composable () -> Unit = {},
|
||||
content: @Composable (PaddingValues) -> Unit,
|
||||
) {
|
||||
val scrollBehaviorOrDefault = scrollBehavior ?: TopAppBarDefaults.enterAlwaysScrollBehavior(state = rememberTopAppBarState())
|
||||
val scrollBehaviorOrDefault = scrollBehavior ?: enterAlwaysScrollBehavior(state = rememberTopAppBarState())
|
||||
val view = LocalView.current
|
||||
val useDarkIcons = MaterialTheme.colorScheme.surface.luminance() > .5
|
||||
val (color, scrolledColor) = getTopAppBarColor(title)
|
||||
|
@ -63,7 +64,10 @@ fun YokaiScaffold(
|
|||
when (appBarType) {
|
||||
AppBarType.SMALL -> TopAppBar(
|
||||
title = {
|
||||
Text(text = title)
|
||||
Text(
|
||||
modifier = Modifier.basicMarquee(),
|
||||
text = title,
|
||||
)
|
||||
},
|
||||
// modifier = Modifier.statusBarsPadding(),
|
||||
colors = topAppBarColors(
|
||||
|
@ -82,7 +86,10 @@ fun YokaiScaffold(
|
|||
)
|
||||
AppBarType.LARGE -> ExpandedAppBar(
|
||||
title = {
|
||||
Text(text = title)
|
||||
Text(
|
||||
modifier = Modifier.basicMarquee(),
|
||||
text = title,
|
||||
)
|
||||
},
|
||||
// modifier = Modifier.statusBarsPadding(),
|
||||
colors = topAppBarColors(
|
||||
|
|
|
@ -11,8 +11,6 @@ import androidx.compose.material3.AlertDialog
|
|||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
|
@ -41,6 +39,8 @@ import yokai.presentation.AppBarType
|
|||
import yokai.presentation.YokaiScaffold
|
||||
import yokai.presentation.component.EmptyScreen
|
||||
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.ExtensionRepoItem
|
||||
import yokai.util.Screen
|
||||
|
@ -67,7 +67,7 @@ class ExtensionRepoScreen(
|
|||
onNavigationIconClicked = onBackPress,
|
||||
title = title,
|
||||
appBarType = AppBarType.SMALL,
|
||||
scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(
|
||||
scrollBehavior = enterAlwaysScrollBehavior(
|
||||
state = rememberTopAppBarState(),
|
||||
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.items
|
||||
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.LaunchedEffect
|
||||
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.PreferenceItem
|
||||
import yokai.presentation.component.preference.widget.PreferenceGroupHeader
|
||||
import yokai.presentation.core.TopAppBarScrollBehavior
|
||||
import yokai.presentation.core.drawVerticalScrollbar
|
||||
import yokai.presentation.core.enterAlwaysCollapsedScrollBehavior
|
||||
import yokai.presentation.core.rememberTopAppBarState
|
||||
|
||||
@Composable
|
||||
fun SettingsScaffold(
|
||||
|
|
|
@ -6,8 +6,6 @@ import androidx.compose.foundation.rememberScrollState
|
|||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
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.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
|
@ -23,6 +21,8 @@ import yokai.i18n.MR
|
|||
import yokai.presentation.AppBarType
|
||||
import yokai.presentation.YokaiScaffold
|
||||
import yokai.presentation.component.ToolTipButton
|
||||
import yokai.presentation.core.enterAlwaysScrollBehavior
|
||||
import yokai.presentation.core.rememberTopAppBarState
|
||||
import yokai.util.Screen
|
||||
|
||||
class AboutLibraryLicenseScreen(
|
||||
|
@ -45,7 +45,7 @@ class AboutLibraryLicenseScreen(
|
|||
},
|
||||
title = name,
|
||||
appBarType = AppBarType.SMALL,
|
||||
scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(
|
||||
scrollBehavior = enterAlwaysScrollBehavior(
|
||||
state = rememberTopAppBarState(),
|
||||
),
|
||||
actions = {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package yokai.presentation.settings.screen.about
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
|
@ -13,6 +12,7 @@ import eu.kanade.tachiyomi.util.compose.currentOrThrow
|
|||
import yokai.i18n.MR
|
||||
import yokai.presentation.AppBarType
|
||||
import yokai.presentation.YokaiScaffold
|
||||
import yokai.presentation.core.pinnedScrollBehavior
|
||||
import yokai.util.Screen
|
||||
|
||||
class AboutLicenseScreen : Screen() {
|
||||
|
@ -25,7 +25,7 @@ class AboutLicenseScreen : Screen() {
|
|||
onNavigationIconClicked = backPress,
|
||||
title = stringResource(MR.strings.open_source_licenses),
|
||||
appBarType = AppBarType.SMALL,
|
||||
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(),
|
||||
scrollBehavior = pinnedScrollBehavior(),
|
||||
) { innerPadding ->
|
||||
LibrariesContainer(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
|
|
|
@ -18,7 +18,6 @@ import androidx.compose.material.icons.outlined.Public
|
|||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
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.Discord
|
||||
import yokai.presentation.core.icons.GitHub
|
||||
import yokai.presentation.core.rememberTopAppBarState
|
||||
import yokai.presentation.settings.SettingsScaffold
|
||||
import yokai.util.Screen
|
||||
import yokai.util.lang.getString
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package yokai.presentation.core
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.AnimationSpec
|
||||
import androidx.compose.animation.core.AnimationState
|
||||
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.padding
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.TopAppBarColors
|
||||
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.CompositionLocalProvider
|
||||
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.Modifier
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
|
@ -62,6 +70,35 @@ import kotlin.math.abs
|
|||
import kotlin.math.max
|
||||
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]
|
||||
*
|
||||
|
@ -87,14 +124,99 @@ fun ExpandedAppBar(
|
|||
modifier = modifier,
|
||||
navigationIcon = navigationIcon,
|
||||
actions = actions,
|
||||
collapsedHeight = CollapsedContainerHeight,
|
||||
expandedHeight = ExpandedContainerHeight,
|
||||
topHeight = CollapsedContainerHeight,
|
||||
bottomHeight = ExpandedContainerHeight - CollapsedContainerHeight,
|
||||
windowInsets = windowInsets,
|
||||
colors = colors,
|
||||
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
|
||||
private fun TwoRowsTopAppBar(
|
||||
modifier: Modifier = Modifier,
|
||||
|
@ -105,35 +227,33 @@ private fun TwoRowsTopAppBar(
|
|||
smallTitleTextStyle: TextStyle,
|
||||
navigationIcon: @Composable () -> Unit,
|
||||
actions: @Composable RowScope.() -> Unit,
|
||||
collapsedHeight: Dp,
|
||||
expandedHeight: Dp,
|
||||
topHeight: Dp,
|
||||
bottomHeight: Dp,
|
||||
windowInsets: WindowInsets,
|
||||
colors: TopAppBarColors,
|
||||
scrollBehavior: TopAppBarScrollBehavior?
|
||||
) {
|
||||
require(collapsedHeight.isSpecified && collapsedHeight.isFinite) {
|
||||
"The collapsedHeight is expected to be specified and finite"
|
||||
require(topHeight.isSpecified && topHeight.isFinite) {
|
||||
"The topHeight is expected to be specified and finite"
|
||||
}
|
||||
require(expandedHeight.isSpecified && expandedHeight.isFinite) {
|
||||
"The expandedHeight is expected to be specified and finite"
|
||||
require(bottomHeight.isSpecified && bottomHeight.isFinite) {
|
||||
"The bottomHeight is expected to be specified and finite"
|
||||
}
|
||||
require(expandedHeight >= collapsedHeight) {
|
||||
"The expandedHeight is expected to be greater or equal to the collapsedHeight"
|
||||
}
|
||||
val expandedHeightPx: Float
|
||||
val collapsedHeightPx: Float
|
||||
val topHeightPx: Float
|
||||
val bottomHeightPx: Float
|
||||
val titleBottomPaddingPx: Int
|
||||
LocalDensity.current.run {
|
||||
expandedHeightPx = expandedHeight.toPx()
|
||||
collapsedHeightPx = collapsedHeight.toPx()
|
||||
topHeightPx = topHeight.toPx()
|
||||
bottomHeightPx = bottomHeight.toPx()
|
||||
titleBottomPaddingPx = titleBottomPadding.roundToPx()
|
||||
}
|
||||
|
||||
// Sets the app bar's height offset limit to hide just the bottom title area and keep top title
|
||||
// visible when collapsed.
|
||||
SideEffect {
|
||||
if (scrollBehavior?.state?.heightOffsetLimit != -expandedHeightPx) {
|
||||
scrollBehavior?.state?.heightOffsetLimit = -expandedHeightPx
|
||||
if (scrollBehavior?.state?.heights != listOf(topHeightPx, bottomHeightPx)) {
|
||||
scrollBehavior?.state?.heights?.clear()
|
||||
scrollBehavior?.state?.heights?.addAll(listOf(topHeightPx, bottomHeightPx))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -142,7 +262,7 @@ private fun TwoRowsTopAppBar(
|
|||
// collapse.
|
||||
// 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.
|
||||
val colorTransitionFraction = scrollBehavior?.state?.bottomCollapsedFraction(collapsedHeightPx, expandedHeightPx) ?: 0f
|
||||
val colorTransitionFraction = scrollBehavior?.state?.collapsedFraction(1) ?: 0f
|
||||
|
||||
val appBarContainerColor =
|
||||
lerp(
|
||||
|
@ -175,12 +295,9 @@ private fun TwoRowsTopAppBar(
|
|||
.windowInsetsPadding(windowInsets)
|
||||
// clip after padding so we don't show the title over the inset area
|
||||
.clipToBounds()
|
||||
.heightIn(max = collapsedHeight),
|
||||
.heightIn(max = topHeight),
|
||||
scrolledOffset = {
|
||||
scrollBehavior?.state?.topHeightOffset(
|
||||
topHeightPx = collapsedHeightPx,
|
||||
totalHeightPx = expandedHeightPx,
|
||||
) ?: 0f
|
||||
scrollBehavior?.state?.heightOffset(0) ?: 0f
|
||||
},
|
||||
navigationIconContentColor = colors.navigationIconContentColor,
|
||||
titleContentColor = colors.titleContentColor,
|
||||
|
@ -203,12 +320,9 @@ private fun TwoRowsTopAppBar(
|
|||
// padding will always be applied by the layout above
|
||||
.windowInsetsPadding(windowInsets.only(WindowInsetsSides.Horizontal))
|
||||
.clipToBounds()
|
||||
.heightIn(max = expandedHeight - collapsedHeight),
|
||||
.heightIn(max = bottomHeight),
|
||||
scrolledOffset = {
|
||||
scrollBehavior?.state?.bottomHeightOffset(
|
||||
topHeightPx = collapsedHeightPx,
|
||||
totalHeightPx = expandedHeightPx,
|
||||
) ?: 0f
|
||||
scrollBehavior?.state?.heightOffset(1) ?: 0f
|
||||
},
|
||||
navigationIconContentColor = colors.navigationIconContentColor,
|
||||
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
|
||||
private fun AppBarLayout(
|
||||
modifier: Modifier,
|
||||
|
@ -457,16 +725,17 @@ private fun interface ScrolledOffset {
|
|||
private suspend fun settleAppBar(
|
||||
state: TopAppBarState,
|
||||
velocity: Float,
|
||||
topHeightPx: Float,
|
||||
totalHeightPx: Float,
|
||||
flingAnimationSpec: DecayAnimationSpec<Float>?,
|
||||
snapAnimationSpec: AnimationSpec<Float>?,
|
||||
topOnly: Boolean = false,
|
||||
): Velocity {
|
||||
// Check if the app bar is completely collapsed/expanded. If so, no need to settle the app bar,
|
||||
// and just return Zero Velocity.
|
||||
// Note that we don't check for 0f due to float precision with the collapsedFraction
|
||||
// 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
|
||||
}
|
||||
var remainingVelocity = velocity
|
||||
|
@ -491,53 +760,96 @@ private suspend fun settleAppBar(
|
|||
}
|
||||
// Snap if animation specs were provided.
|
||||
if (snapAnimationSpec != null) {
|
||||
if (state.topHeightOffset(topHeightPx, totalHeightPx) < 0 && state.topHeightOffset(topHeightPx, totalHeightPx) > -topHeightPx) {
|
||||
AnimationState(initialValue = state.topHeightOffset(topHeightPx, totalHeightPx)).animateTo(
|
||||
if (state.topCollapsedFraction(topHeightPx, totalHeightPx) < 0.5f) {
|
||||
0f
|
||||
} else {
|
||||
-topHeightPx
|
||||
},
|
||||
animationSpec = snapAnimationSpec
|
||||
) {
|
||||
state.heightOffset = value + (topHeightPx - totalHeightPx)
|
||||
}
|
||||
when (topOnly) {
|
||||
true ->
|
||||
if (state.heightOffset(0) < 0 && state.heightOffset(0) > -state.heights[0]) {
|
||||
AnimationState(initialValue = state.heightOffset(0)).animateTo(
|
||||
if (state.collapsedFraction(0) < 0.5f) {
|
||||
0f
|
||||
} else {
|
||||
-state.heights[0]
|
||||
},
|
||||
animationSpec = snapAnimationSpec
|
||||
) {
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
* Default values:
|
||||
* - Top app bar height: 128px
|
||||
* - 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)
|
||||
*/
|
||||
interface TopAppBarScrollBehavior {
|
||||
|
||||
private fun TopAppBarState.rawTopHeightOffset(topHeightPx: Float, totalHeightPx: Float): Float {
|
||||
return heightOffset + (totalHeightPx - topHeightPx)
|
||||
/**
|
||||
* A [TopAppBarState] that is attached to this behavior and is read and updated when scrolling
|
||||
* happens.
|
||||
*/
|
||||
val state: TopAppBarState
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
return rawTopHeightOffset(topHeightPx, totalHeightPx).fastCoerceIn(-topHeightPx, 0f)
|
||||
}
|
||||
@Composable
|
||||
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 {
|
||||
return heightOffset.fastCoerceIn(topHeightPx - totalHeightPx, 0f)
|
||||
}
|
||||
|
||||
private fun TopAppBarState.topCollapsedFraction(topHeightPx: Float, totalHeightPx: Float): Float {
|
||||
val offset = topHeightOffset(topHeightPx, totalHeightPx)
|
||||
return offset / -topHeightPx
|
||||
}
|
||||
|
||||
private fun TopAppBarState.bottomCollapsedFraction(topHeightPx: Float, totalHeightPx: Float): Float {
|
||||
val offset = bottomHeightOffset(topHeightPx, totalHeightPx)
|
||||
return offset / (topHeightPx - totalHeightPx)
|
||||
}
|
||||
@Composable
|
||||
fun enterAlwaysScrollBehavior(
|
||||
state: TopAppBarState = rememberTopAppBarState(),
|
||||
canScroll: () -> Boolean = { true },
|
||||
snapAnimationSpec: AnimationSpec<Float>? = spring(stiffness = Spring.StiffnessMediumLow),
|
||||
flingAnimationSpec: DecayAnimationSpec<Float>? = rememberSplineBasedDecay()
|
||||
): TopAppBarScrollBehavior =
|
||||
remember(state, canScroll, snapAnimationSpec, flingAnimationSpec) {
|
||||
EnterAlwaysScrollBehavior(
|
||||
state = state,
|
||||
snapAnimationSpec = snapAnimationSpec,
|
||||
flingAnimationSpec = flingAnimationSpec,
|
||||
canScroll = canScroll
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun enterAlwaysCollapsedScrollBehavior(
|
||||
|
@ -546,22 +858,106 @@ fun enterAlwaysCollapsedScrollBehavior(
|
|||
isAtTop: () -> Boolean = { true },
|
||||
snapAnimationSpec: AnimationSpec<Float>? = spring(stiffness = Spring.StiffnessMediumLow),
|
||||
flingAnimationSpec: DecayAnimationSpec<Float>? = rememberSplineBasedDecay()
|
||||
): TopAppBarScrollBehavior {
|
||||
val (topHeightPx, totalHeightPx) = with(LocalDensity.current) {
|
||||
CollapsedContainerHeight.toPx() to ExpandedContainerHeight.toPx()
|
||||
}
|
||||
|
||||
return remember(state, canScroll, isAtTop, snapAnimationSpec, flingAnimationSpec, topHeightPx, totalHeightPx) {
|
||||
): TopAppBarScrollBehavior =
|
||||
remember(state, canScroll, isAtTop, snapAnimationSpec, flingAnimationSpec) {
|
||||
EnterAlwaysCollapsedScrollBehavior(
|
||||
state = state,
|
||||
snapAnimationSpec = snapAnimationSpec,
|
||||
flingAnimationSpec = flingAnimationSpec,
|
||||
canScroll = canScroll,
|
||||
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(
|
||||
|
@ -571,27 +967,20 @@ private class EnterAlwaysCollapsedScrollBehavior(
|
|||
val canScroll: () -> Boolean = { true },
|
||||
// FIXME: See if it's possible to eliminate this argument
|
||||
val isAtTop: () -> Boolean = { true },
|
||||
val topHeightPx: Float,
|
||||
val totalHeightPx: Float,
|
||||
) : TopAppBarScrollBehavior {
|
||||
override val isPinned: Boolean = false
|
||||
override var 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 {
|
||||
// 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
|
||||
|
||||
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) {
|
||||
// We're in the middle of top app bar collapse or expand.
|
||||
// Consume only the scroll on the Y axis.
|
||||
|
@ -612,7 +1001,10 @@ private class EnterAlwaysCollapsedScrollBehavior(
|
|||
if (available.y < 0f || consumed.y < 0f) {
|
||||
// When scrolling up, just update the state's height offset.
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -626,7 +1018,10 @@ private class EnterAlwaysCollapsedScrollBehavior(
|
|||
// 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.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.Zero
|
||||
|
@ -635,7 +1030,70 @@ private class EnterAlwaysCollapsedScrollBehavior(
|
|||
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
||||
val superConsumed = super.onPostFling(consumed, available)
|
||||
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