| /* |
| * Copyright 2021 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package androidx.compose.material3 |
| |
| import androidx.compose.animation.animateColorAsState |
| import androidx.compose.animation.core.AnimationSpec |
| import androidx.compose.animation.core.AnimationState |
| import androidx.compose.animation.core.CubicBezierEasing |
| import androidx.compose.animation.core.DecayAnimationSpec |
| import androidx.compose.animation.core.FastOutLinearInEasing |
| import androidx.compose.animation.core.Spring |
| import androidx.compose.animation.core.animateDecay |
| import androidx.compose.animation.core.animateTo |
| import androidx.compose.animation.core.spring |
| import androidx.compose.animation.rememberSplineBasedDecay |
| import androidx.compose.foundation.background |
| import androidx.compose.foundation.gestures.Orientation |
| import androidx.compose.foundation.gestures.draggable |
| import androidx.compose.foundation.gestures.rememberDraggableState |
| import androidx.compose.foundation.layout.Arrangement |
| import androidx.compose.foundation.layout.Box |
| import androidx.compose.foundation.layout.Column |
| import androidx.compose.foundation.layout.PaddingValues |
| import androidx.compose.foundation.layout.Row |
| import androidx.compose.foundation.layout.RowScope |
| import androidx.compose.foundation.layout.Spacer |
| import androidx.compose.foundation.layout.WindowInsets |
| import androidx.compose.foundation.layout.WindowInsetsSides |
| import androidx.compose.foundation.layout.fillMaxHeight |
| import androidx.compose.foundation.layout.fillMaxWidth |
| import androidx.compose.foundation.layout.height |
| import androidx.compose.foundation.layout.only |
| import androidx.compose.foundation.layout.padding |
| import androidx.compose.foundation.layout.windowInsetsPadding |
| import androidx.compose.material3.tokens.BottomAppBarTokens |
| import androidx.compose.material3.tokens.FabSecondaryTokens |
| import androidx.compose.material3.tokens.TopAppBarLargeTokens |
| import androidx.compose.material3.tokens.TopAppBarMediumTokens |
| import androidx.compose.material3.tokens.TopAppBarSmallCenteredTokens |
| import androidx.compose.material3.tokens.TopAppBarSmallTokens |
| import androidx.compose.runtime.Composable |
| import androidx.compose.runtime.CompositionLocalProvider |
| import androidx.compose.runtime.SideEffect |
| import androidx.compose.runtime.Stable |
| import androidx.compose.runtime.getValue |
| import androidx.compose.runtime.mutableStateOf |
| import androidx.compose.runtime.rememberUpdatedState |
| 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.ui.Alignment |
| import androidx.compose.ui.Modifier |
| import androidx.compose.ui.draw.clipToBounds |
| import androidx.compose.ui.geometry.Offset |
| import androidx.compose.ui.graphics.Color |
| import androidx.compose.ui.graphics.lerp |
| import androidx.compose.ui.input.nestedscroll.NestedScrollConnection |
| import androidx.compose.ui.input.nestedscroll.NestedScrollSource |
| import androidx.compose.ui.layout.AlignmentLine |
| import androidx.compose.ui.layout.LastBaseline |
| import androidx.compose.ui.layout.Layout |
| import androidx.compose.ui.layout.layoutId |
| import androidx.compose.ui.platform.LocalDensity |
| import androidx.compose.ui.semantics.clearAndSetSemantics |
| import androidx.compose.ui.text.TextStyle |
| import androidx.compose.ui.unit.Constraints |
| import androidx.compose.ui.unit.Dp |
| import androidx.compose.ui.unit.Velocity |
| import androidx.compose.ui.unit.dp |
| import kotlin.math.abs |
| import kotlin.math.max |
| import kotlin.math.roundToInt |
| |
| /** |
| * <a href="https://m3.material.io/components/top-app-bar/overview" class="external" target="_blank">Material Design small top app bar</a>. |
| * |
| * Top app bars display information and actions at the top of a screen. |
| * |
| * This small TopAppBar has slots for a title, navigation icon, and actions. |
| * |
| * ![Small top app bar image](https://developer.android.com/images/reference/androidx/compose/material3/small-top-app-bar.png) |
| * |
| * A simple top app bar looks like: |
| * @sample androidx.compose.material3.samples.SimpleTopAppBar |
| * A top app bar that uses a [scrollBehavior] to customize its nested scrolling behavior when |
| * working in conjunction with a scrolling content looks like: |
| * @sample androidx.compose.material3.samples.PinnedTopAppBar |
| * @sample androidx.compose.material3.samples.EnterAlwaysTopAppBar |
| * |
| * @param title the title to be displayed in the top app bar |
| * @param modifier the [Modifier] to be applied to this top app bar |
| * @param navigationIcon the navigation icon displayed at the start of the top app bar. This should |
| * typically be an [IconButton] or [IconToggleButton]. |
| * @param actions the actions displayed at the end of the top app bar. This should typically be |
| * [IconButton]s. The default layout here is a [Row], so icons inside will be placed horizontally. |
| * @param windowInsets a window insets that app bar will respect. |
| * @param colors [TopAppBarColors] that will be used to resolve the colors used for this top app |
| * bar in different states. See [TopAppBarDefaults.smallTopAppBarColors]. |
| * @param scrollBehavior a [TopAppBarScrollBehavior] which holds various offset values that will be |
| * applied by this top app bar to set up its height and colors. A scroll behavior is designed to |
| * work in conjunction with a scrolled content to change the top app bar appearance as the content |
| * scrolls. See [TopAppBarScrollBehavior.nestedScrollConnection]. |
| */ |
| @ExperimentalMaterial3Api |
| @Composable |
| fun TopAppBar( |
| title: @Composable () -> Unit, |
| modifier: Modifier = Modifier, |
| navigationIcon: @Composable () -> Unit = {}, |
| actions: @Composable RowScope.() -> Unit = {}, |
| windowInsets: WindowInsets = TopAppBarDefaults.windowInsets, |
| colors: TopAppBarColors = TopAppBarDefaults.smallTopAppBarColors(), |
| scrollBehavior: TopAppBarScrollBehavior? = null |
| ) { |
| SingleRowTopAppBar( |
| modifier = modifier, |
| title = title, |
| titleTextStyle = MaterialTheme.typography.fromToken(TopAppBarSmallTokens.HeadlineFont), |
| centeredTitle = false, |
| navigationIcon = navigationIcon, |
| actions = actions, |
| windowInsets = windowInsets, |
| colors = colors, |
| scrollBehavior = scrollBehavior |
| ) |
| } |
| |
| /** |
| * <a href="https://m3.material.io/components/top-app-bar/overview" class="external" target="_blank">Material Design small top app bar</a>. |
| * |
| * Top app bars display information and actions at the top of a screen. |
| * |
| * This SmallTopAppBar has slots for a title, navigation icon, and actions. |
| * |
| * ![Small top app bar image](https://developer.android.com/images/reference/androidx/compose/material3/small-top-app-bar.png) |
| * |
| * A simple top app bar looks like: |
| * @sample androidx.compose.material3.samples.SimpleTopAppBar |
| * A top app bar that uses a [scrollBehavior] to customize its nested scrolling behavior when |
| * working in conjunction with a scrolling content looks like: |
| * @sample androidx.compose.material3.samples.PinnedTopAppBar |
| * @sample androidx.compose.material3.samples.EnterAlwaysTopAppBar |
| * |
| * @param title the title to be displayed in the top app bar |
| * @param modifier the [Modifier] to be applied to this top app bar |
| * @param navigationIcon the navigation icon displayed at the start of the top app bar. This should |
| * typically be an [IconButton] or [IconToggleButton]. |
| * @param actions the actions displayed at the end of the top app bar. This should typically be |
| * [IconButton]s. The default layout here is a [Row], so icons inside will be placed horizontally. |
| * @param windowInsets a window insets that app bar will respect. |
| * @param colors [TopAppBarColors] that will be used to resolve the colors used for this top app |
| * bar in different states. See [TopAppBarDefaults.smallTopAppBarColors]. |
| * @param scrollBehavior a [TopAppBarScrollBehavior] which holds various offset values that will be |
| * applied by this top app bar to set up its height and colors. A scroll behavior is designed to |
| * work in conjunction with a scrolled content to change the top app bar appearance as the content |
| * scrolls. See [TopAppBarScrollBehavior.nestedScrollConnection]. |
| * @deprecated use [TopAppBar] instead |
| */ |
| @Deprecated( |
| message = "Use TopAppBar instead.", |
| replaceWith = ReplaceWith( |
| "TopAppBar(title, modifier, navigationIcon, actions, windowInsets, colors, " + |
| "scrollBehavior)" |
| ), |
| level = DeprecationLevel.WARNING |
| ) |
| @ExperimentalMaterial3Api |
| @Composable |
| fun SmallTopAppBar( |
| title: @Composable () -> Unit, |
| modifier: Modifier = Modifier, |
| navigationIcon: @Composable () -> Unit = {}, |
| actions: @Composable RowScope.() -> Unit = {}, |
| windowInsets: WindowInsets = TopAppBarDefaults.windowInsets, |
| colors: TopAppBarColors = TopAppBarDefaults.smallTopAppBarColors(), |
| scrollBehavior: TopAppBarScrollBehavior? = null |
| ) = TopAppBar(title, modifier, navigationIcon, actions, windowInsets, colors, scrollBehavior) |
| |
| /** |
| * <a href="https://m3.material.io/components/top-app-bar/overview" class="external" target="_blank">Material Design center-aligned small top app bar</a>. |
| * |
| * Top app bars display information and actions at the top of a screen. |
| * |
| * This small top app bar has a header title that is horizontally aligned to the center. |
| * |
| * ![Center-aligned top app bar image](https://developer.android.com/images/reference/androidx/compose/material3/center-aligned-top-app-bar.png) |
| * |
| * This CenterAlignedTopAppBar has slots for a title, navigation icon, and actions. |
| * |
| * A center aligned top app bar that uses a [scrollBehavior] to customize its nested scrolling |
| * behavior when working in conjunction with a scrolling content looks like: |
| * @sample androidx.compose.material3.samples.SimpleCenterAlignedTopAppBar |
| * |
| * @param title the title to be displayed in the top app bar |
| * @param modifier the [Modifier] to be applied to this top app bar |
| * @param navigationIcon the navigation icon displayed at the start of the top app bar. This should |
| * typically be an [IconButton] or [IconToggleButton]. |
| * @param actions the actions displayed at the end of the top app bar. This should typically be |
| * [IconButton]s. The default layout here is a [Row], so icons inside will be placed horizontally. |
| * @param windowInsets a window insets that app bar will respect. |
| * @param colors [TopAppBarColors] that will be used to resolve the colors used for this top app |
| * bar in different states. See [TopAppBarDefaults.centerAlignedTopAppBarColors]. |
| * @param scrollBehavior a [TopAppBarScrollBehavior] which holds various offset values that will be |
| * applied by this top app bar to set up its height and colors. A scroll behavior is designed to |
| * work in conjunction with a scrolled content to change the top app bar appearance as the content |
| * scrolls. See [TopAppBarScrollBehavior.nestedScrollConnection]. |
| */ |
| @ExperimentalMaterial3Api |
| @Composable |
| fun CenterAlignedTopAppBar( |
| title: @Composable () -> Unit, |
| modifier: Modifier = Modifier, |
| navigationIcon: @Composable () -> Unit = {}, |
| actions: @Composable RowScope.() -> Unit = {}, |
| windowInsets: WindowInsets = TopAppBarDefaults.windowInsets, |
| colors: TopAppBarColors = TopAppBarDefaults.centerAlignedTopAppBarColors(), |
| scrollBehavior: TopAppBarScrollBehavior? = null |
| ) { |
| SingleRowTopAppBar( |
| modifier = modifier, |
| title = title, |
| titleTextStyle = |
| MaterialTheme.typography.fromToken(TopAppBarSmallTokens.HeadlineFont), |
| centeredTitle = true, |
| navigationIcon = navigationIcon, |
| actions = actions, |
| colors = colors, |
| windowInsets = windowInsets, |
| scrollBehavior = scrollBehavior |
| ) |
| } |
| |
| /** |
| * <a href="https://m3.material.io/components/top-app-bar/overview" class="external" target="_blank">Material Design medium top app bar</a>. |
| * |
| * Top app bars display information and actions at the top of a screen. |
| * |
| * ![Medium top app bar image](https://developer.android.com/images/reference/androidx/compose/material3/medium-top-app-bar.png) |
| * |
| * This MediumTopAppBar has slots for a title, navigation icon, and actions. In its default expanded |
| * state, the title is displayed in a second row under the navigation and actions. |
| * |
| * A medium top app bar that uses a [scrollBehavior] to customize its nested scrolling behavior when |
| * working in conjunction with scrolling content looks like: |
| * @sample androidx.compose.material3.samples.ExitUntilCollapsedMediumTopAppBar |
| * |
| * @param title the title to be displayed in the top app bar. This title will be used in the app |
| * bar's expanded and collapsed states, although in its collapsed state it will be composed with a |
| * smaller sized [TextStyle] |
| * @param modifier the [Modifier] to be applied to this top app bar |
| * @param navigationIcon the navigation icon displayed at the start of the top app bar. This should |
| * typically be an [IconButton] or [IconToggleButton]. |
| * @param actions the actions displayed at the end of the top app bar. This should typically be |
| * [IconButton]s. The default layout here is a [Row], so icons inside will be placed horizontally. |
| * @param windowInsets a window insets that app bar will respect. |
| * @param colors [TopAppBarColors] that will be used to resolve the colors used for this top app |
| * bar in different states. See [TopAppBarDefaults.mediumTopAppBarColors]. |
| * @param scrollBehavior a [TopAppBarScrollBehavior] which holds various offset values that will be |
| * applied by this top app bar to set up its height and colors. A scroll behavior is designed to |
| * work in conjunction with a scrolled content to change the top app bar appearance as the content |
| * scrolls. See [TopAppBarScrollBehavior.nestedScrollConnection]. |
| */ |
| @ExperimentalMaterial3Api |
| @Composable |
| fun MediumTopAppBar( |
| title: @Composable () -> Unit, |
| modifier: Modifier = Modifier, |
| navigationIcon: @Composable () -> Unit = {}, |
| actions: @Composable RowScope.() -> Unit = {}, |
| windowInsets: WindowInsets = TopAppBarDefaults.windowInsets, |
| colors: TopAppBarColors = TopAppBarDefaults.mediumTopAppBarColors(), |
| scrollBehavior: TopAppBarScrollBehavior? = null |
| ) { |
| TwoRowsTopAppBar( |
| modifier = modifier, |
| title = title, |
| titleTextStyle = MaterialTheme.typography.fromToken(TopAppBarMediumTokens.HeadlineFont), |
| smallTitleTextStyle = MaterialTheme.typography.fromToken(TopAppBarSmallTokens.HeadlineFont), |
| titleBottomPadding = MediumTitleBottomPadding, |
| smallTitle = title, |
| navigationIcon = navigationIcon, |
| actions = actions, |
| colors = colors, |
| windowInsets = windowInsets, |
| maxHeight = TopAppBarMediumTokens.ContainerHeight, |
| pinnedHeight = TopAppBarSmallTokens.ContainerHeight, |
| scrollBehavior = scrollBehavior |
| ) |
| } |
| |
| /** |
| * <a href="https://m3.material.io/components/top-app-bar/overview" class="external" target="_blank">Material Design large top app bar</a>. |
| * |
| * Top app bars display information and actions at the top of a screen. |
| * |
| * ![Large top app bar image](https://developer.android.com/images/reference/androidx/compose/material3/large-top-app-bar.png) |
| * |
| * This LargeTopAppBar has slots for a title, navigation icon, and actions. In its default expanded |
| * state, the title is displayed in a second row under the navigation and actions. |
| * |
| * A large top app bar that uses a [scrollBehavior] to customize its nested scrolling behavior when |
| * working in conjunction with scrolling content looks like: |
| * @sample androidx.compose.material3.samples.ExitUntilCollapsedLargeTopAppBar |
| * |
| * @param title the title to be displayed in the top app bar. This title will be used in the app |
| * bar's expanded and collapsed states, although in its collapsed state it will be composed with a |
| * smaller sized [TextStyle] |
| * @param modifier the [Modifier] to be applied to this top app bar |
| * @param navigationIcon the navigation icon displayed at the start of the top app bar. This should |
| * typically be an [IconButton] or [IconToggleButton]. |
| * @param actions the actions displayed at the end of the top app bar. This should typically be |
| * [IconButton]s. The default layout here is a [Row], so icons inside will be placed horizontally. |
| * @param windowInsets a window insets that app bar will respect. |
| * @param colors [TopAppBarColors] that will be used to resolve the colors used for this top app |
| * bar in different states. See [TopAppBarDefaults.largeTopAppBarColors]. |
| * @param scrollBehavior a [TopAppBarScrollBehavior] which holds various offset values that will be |
| * applied by this top app bar to set up its height and colors. A scroll behavior is designed to |
| * work in conjunction with a scrolled content to change the top app bar appearance as the content |
| * scrolls. See [TopAppBarScrollBehavior.nestedScrollConnection]. |
| */ |
| @ExperimentalMaterial3Api |
| @Composable |
| fun LargeTopAppBar( |
| title: @Composable () -> Unit, |
| modifier: Modifier = Modifier, |
| navigationIcon: @Composable () -> Unit = {}, |
| actions: @Composable RowScope.() -> Unit = {}, |
| windowInsets: WindowInsets = TopAppBarDefaults.windowInsets, |
| colors: TopAppBarColors = TopAppBarDefaults.largeTopAppBarColors(), |
| scrollBehavior: TopAppBarScrollBehavior? = null |
| ) { |
| TwoRowsTopAppBar( |
| title = title, |
| titleTextStyle = MaterialTheme.typography.fromToken(TopAppBarLargeTokens.HeadlineFont), |
| smallTitleTextStyle = MaterialTheme.typography.fromToken(TopAppBarSmallTokens.HeadlineFont), |
| titleBottomPadding = LargeTitleBottomPadding, |
| smallTitle = title, |
| modifier = modifier, |
| navigationIcon = navigationIcon, |
| actions = actions, |
| colors = colors, |
| windowInsets = windowInsets, |
| maxHeight = TopAppBarLargeTokens.ContainerHeight, |
| pinnedHeight = TopAppBarSmallTokens.ContainerHeight, |
| scrollBehavior = scrollBehavior |
| ) |
| } |
| |
| /** |
| * <a href="https://m3.material.io/components/bottom-app-bar/overview" class="external" target="_blank">Material Design bottom app bar</a>. |
| * |
| * A bottom app bar displays navigation and key actions at the bottom of mobile screens. |
| * |
| * ![Bottom app bar image](https://developer.android.com/images/reference/androidx/compose/material3/bottom-app-bar.png) |
| * |
| * @sample androidx.compose.material3.samples.SimpleBottomAppBar |
| * |
| * It can optionally display a [FloatingActionButton] embedded at the end of the BottomAppBar. |
| * |
| * @sample androidx.compose.material3.samples.BottomAppBarWithFAB |
| * |
| * Also see [NavigationBar]. |
| * |
| * @param actions the icon content of this BottomAppBar. The default layout here is a [Row], |
| * so content inside will be placed horizontally. |
| * @param modifier the [Modifier] to be applied to this BottomAppBar |
| * @param floatingActionButton optional floating action button at the end of this BottomAppBar |
| * @param containerColor the color used for the background of this BottomAppBar. Use |
| * [Color.Transparent] to have no color. |
| * @param contentColor the preferred color for content inside this BottomAppBar. Defaults to either |
| * the matching content color for [containerColor], or to the current [LocalContentColor] if |
| * [containerColor] is not a color from the theme. |
| * @param tonalElevation when [containerColor] is [ColorScheme.surface], a translucent primary color |
| * overlay is applied on top of the container. A higher tonal elevation value will result in a |
| * darker color in light theme and lighter color in dark theme. See also: [Surface]. |
| * @param contentPadding the padding applied to the content of this BottomAppBar |
| * @param windowInsets a window insets that app bar will respect. |
| */ |
| @Composable |
| fun BottomAppBar( |
| actions: @Composable RowScope.() -> Unit, |
| modifier: Modifier = Modifier, |
| floatingActionButton: @Composable (() -> Unit)? = null, |
| containerColor: Color = BottomAppBarDefaults.containerColor, |
| contentColor: Color = contentColorFor(containerColor), |
| tonalElevation: Dp = BottomAppBarDefaults.ContainerElevation, |
| contentPadding: PaddingValues = BottomAppBarDefaults.ContentPadding, |
| windowInsets: WindowInsets = BottomAppBarDefaults.windowInsets, |
| ) = BottomAppBar( |
| modifier = modifier, |
| containerColor = containerColor, |
| contentColor = contentColor, |
| tonalElevation = tonalElevation, |
| windowInsets = windowInsets, |
| contentPadding = contentPadding |
| ) { |
| actions() |
| if (floatingActionButton != null) { |
| Spacer(Modifier.weight(1f, true)) |
| Box( |
| Modifier |
| .fillMaxHeight() |
| .padding( |
| top = FABVerticalPadding, |
| end = FABHorizontalPadding |
| ), |
| contentAlignment = Alignment.TopStart |
| ) { |
| floatingActionButton() |
| } |
| } |
| } |
| |
| /** |
| * <a href="https://m3.material.io/components/bottom-app-bar/overview" class="external" target="_blank">Material Design bottom app bar</a>. |
| * |
| * A bottom app bar displays navigation and key actions at the bottom of mobile screens. |
| * |
| * ![Bottom app bar image](https://developer.android.com/images/reference/androidx/compose/material3/bottom-app-bar.png) |
| * |
| * If you are interested in displaying a [FloatingActionButton], consider using another overload. |
| * |
| * Also see [NavigationBar]. |
| * |
| * @param modifier the [Modifier] to be applied to this BottomAppBar |
| * @param containerColor the color used for the background of this BottomAppBar. Use |
| * [Color.Transparent] to have no color. |
| * @param contentColor the preferred color for content inside this BottomAppBar. Defaults to either |
| * the matching content color for [containerColor], or to the current [LocalContentColor] if |
| * [containerColor] is not a color from the theme. |
| * @param tonalElevation when [containerColor] is [ColorScheme.surface], a translucent primary color |
| * overlay is applied on top of the container. A higher tonal elevation value will result in a |
| * darker color in light theme and lighter color in dark theme. See also: [Surface]. |
| * @param contentPadding the padding applied to the content of this BottomAppBar |
| * @param windowInsets a window insets that app bar will respect. |
| * @param content the content of this BottomAppBar. The default layout here is a [Row], |
| * so content inside will be placed horizontally. |
| */ |
| @Composable |
| fun BottomAppBar( |
| modifier: Modifier = Modifier, |
| containerColor: Color = BottomAppBarDefaults.containerColor, |
| contentColor: Color = contentColorFor(containerColor), |
| tonalElevation: Dp = BottomAppBarDefaults.ContainerElevation, |
| contentPadding: PaddingValues = BottomAppBarDefaults.ContentPadding, |
| windowInsets: WindowInsets = BottomAppBarDefaults.windowInsets, |
| content: @Composable RowScope.() -> Unit |
| ) { |
| Surface( |
| color = containerColor, |
| contentColor = contentColor, |
| tonalElevation = tonalElevation, |
| // TODO(b/209583788): Consider adding a shape parameter if updated design guidance allows |
| shape = BottomAppBarTokens.ContainerShape.toShape(), |
| modifier = modifier |
| ) { |
| Row( |
| Modifier |
| .fillMaxWidth() |
| .windowInsetsPadding(windowInsets) |
| .height(BottomAppBarTokens.ContainerHeight) |
| .padding(contentPadding), |
| horizontalArrangement = Arrangement.Start, |
| verticalAlignment = Alignment.CenterVertically, |
| content = content |
| ) |
| } |
| } |
| |
| /** |
| * A TopAppBarScrollBehavior defines how an app bar should behave when the content under it is |
| * scrolled. |
| * |
| * @see [TopAppBarDefaults.pinnedScrollBehavior] |
| * @see [TopAppBarDefaults.enterAlwaysScrollBehavior] |
| * @see [TopAppBarDefaults.exitUntilCollapsedScrollBehavior] |
| */ |
| @ExperimentalMaterial3Api |
| @Stable |
| interface TopAppBarScrollBehavior { |
| |
| /** |
| * 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 |
| } |
| |
| /** Contains default values used for the top app bar implementations. */ |
| @ExperimentalMaterial3Api |
| object TopAppBarDefaults { |
| |
| /** |
| * Creates a [TopAppBarColors] for small top app bars. The default implementation animates |
| * between the provided colors according to the Material Design specification. |
| * |
| * @param containerColor the container color |
| * @param scrolledContainerColor the container color when content is scrolled behind it |
| * @param navigationIconContentColor the content color used for the navigation icon |
| * @param titleContentColor the content color used for the title |
| * @param actionIconContentColor the content color used for actions |
| * @return the resulting [TopAppBarColors] used for the top app bar |
| */ |
| @Composable |
| fun smallTopAppBarColors( |
| containerColor: Color = TopAppBarSmallTokens.ContainerColor.toColor(), |
| scrolledContainerColor: Color = MaterialTheme.colorScheme.applyTonalElevation( |
| backgroundColor = containerColor, |
| elevation = TopAppBarSmallTokens.OnScrollContainerElevation |
| ), |
| navigationIconContentColor: Color = TopAppBarSmallTokens.LeadingIconColor.toColor(), |
| titleContentColor: Color = TopAppBarSmallTokens.HeadlineColor.toColor(), |
| actionIconContentColor: Color = TopAppBarSmallTokens.TrailingIconColor.toColor(), |
| ): TopAppBarColors = |
| TopAppBarColors( |
| containerColor, |
| scrolledContainerColor, |
| navigationIconContentColor, |
| titleContentColor, |
| actionIconContentColor |
| ) |
| |
| /** |
| * Default insets to be used and consumed by the top app bars |
| */ |
| val windowInsets: WindowInsets |
| @Composable |
| get() = WindowInsets.systemBarsForVisualComponents |
| .only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top) |
| |
| /** |
| * Creates a [TopAppBarColors] for center aligned top app bars. The default implementation |
| * animates between the provided colors according to the Material Design specification. |
| * |
| * @param containerColor the container color |
| * @param scrolledContainerColor the container color when content is scrolled behind it |
| * @param navigationIconContentColor the content color used for the navigation icon |
| * @param titleContentColor the content color used for the title |
| * @param actionIconContentColor the content color used for actions |
| * @return the resulting [TopAppBarColors] used for the top app bar |
| */ |
| @Composable |
| fun centerAlignedTopAppBarColors( |
| containerColor: Color = TopAppBarSmallCenteredTokens.ContainerColor.toColor(), |
| scrolledContainerColor: Color = MaterialTheme.colorScheme.applyTonalElevation( |
| backgroundColor = containerColor, |
| elevation = TopAppBarSmallTokens.OnScrollContainerElevation |
| ), |
| navigationIconContentColor: Color = TopAppBarSmallCenteredTokens.LeadingIconColor.toColor(), |
| titleContentColor: Color = TopAppBarSmallCenteredTokens.HeadlineColor.toColor(), |
| actionIconContentColor: Color = TopAppBarSmallCenteredTokens.TrailingIconColor.toColor(), |
| ): TopAppBarColors = |
| TopAppBarColors( |
| containerColor, |
| scrolledContainerColor, |
| navigationIconContentColor, |
| titleContentColor, |
| actionIconContentColor |
| ) |
| |
| /** |
| * Creates a [TopAppBarColors] for medium top app bars. The default implementation interpolates |
| * between the provided colors as the top app bar scrolls according to the Material Design |
| * specification. |
| * |
| * @param containerColor the container color |
| * @param scrolledContainerColor the container color when content is scrolled behind it |
| * @param navigationIconContentColor the content color used for the navigation icon |
| * @param titleContentColor the content color used for the title |
| * @param actionIconContentColor the content color used for actions |
| * @return the resulting [TopAppBarColors] used for the top app bar |
| */ |
| @Composable |
| fun mediumTopAppBarColors( |
| containerColor: Color = TopAppBarMediumTokens.ContainerColor.toColor(), |
| scrolledContainerColor: Color = MaterialTheme.colorScheme.applyTonalElevation( |
| backgroundColor = containerColor, |
| elevation = TopAppBarSmallTokens.OnScrollContainerElevation |
| ), |
| navigationIconContentColor: Color = TopAppBarMediumTokens.LeadingIconColor.toColor(), |
| titleContentColor: Color = TopAppBarMediumTokens.HeadlineColor.toColor(), |
| actionIconContentColor: Color = TopAppBarMediumTokens.TrailingIconColor.toColor(), |
| ): TopAppBarColors = |
| TopAppBarColors( |
| containerColor, |
| scrolledContainerColor, |
| navigationIconContentColor, |
| titleContentColor, |
| actionIconContentColor |
| ) |
| |
| /** |
| * Creates a [TopAppBarColors] for large top app bars. The default implementation interpolates |
| * between the provided colors as the top app bar scrolls according to the Material Design |
| * specification. |
| * |
| * @param containerColor the container color |
| * @param scrolledContainerColor the container color when content is scrolled behind it |
| * @param navigationIconContentColor the content color used for the navigation icon |
| * @param titleContentColor the content color used for the title |
| * @param actionIconContentColor the content color used for actions |
| * @return the resulting [TopAppBarColors] used for the top app bar |
| */ |
| @Composable |
| fun largeTopAppBarColors( |
| containerColor: Color = TopAppBarLargeTokens.ContainerColor.toColor(), |
| scrolledContainerColor: Color = MaterialTheme.colorScheme.applyTonalElevation( |
| backgroundColor = containerColor, |
| elevation = TopAppBarSmallTokens.OnScrollContainerElevation |
| ), |
| navigationIconContentColor: Color = TopAppBarLargeTokens.LeadingIconColor.toColor(), |
| titleContentColor: Color = TopAppBarLargeTokens.HeadlineColor.toColor(), |
| actionIconContentColor: Color = TopAppBarLargeTokens.TrailingIconColor.toColor(), |
| ): TopAppBarColors = |
| TopAppBarColors( |
| containerColor, |
| scrolledContainerColor, |
| navigationIconContentColor, |
| titleContentColor, |
| actionIconContentColor |
| ) |
| |
| /** |
| * Returns a pinned [TopAppBarScrollBehavior] that tracks nested-scroll callbacks and |
| * updates its [TopAppBarState.contentOffset] accordingly. |
| * |
| * @param state the state object to be used to control or observe the top app bar's scroll |
| * state. See [rememberTopAppBarState] for a state that is remembered across compositions. |
| * @param canScroll a callback used to determine whether scroll events are to be handled by this |
| * pinned [TopAppBarScrollBehavior] |
| */ |
| @ExperimentalMaterial3Api |
| @Composable |
| fun pinnedScrollBehavior( |
| state: TopAppBarState = rememberTopAppBarState(), |
| canScroll: () -> Boolean = { true } |
| ): TopAppBarScrollBehavior = PinnedScrollBehavior(state = state, canScroll = canScroll) |
| |
| /** |
| * Returns a [TopAppBarScrollBehavior]. A top app bar that is set up with this |
| * [TopAppBarScrollBehavior] will immediately collapse when the content is pulled up, and will |
| * immediately appear when the content is pulled down. |
| * |
| * @param state the state object to be used to control or observe the top app bar's scroll |
| * state. See [rememberTopAppBarState] for a state that is remembered across compositions. |
| * @param canScroll a callback used to determine whether scroll events are to be |
| * handled by this [EnterAlwaysScrollBehavior] |
| * @param snapAnimationSpec 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 |
| * @param flingAnimationSpec 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 |
| */ |
| @ExperimentalMaterial3Api |
| @Composable |
| fun enterAlwaysScrollBehavior( |
| state: TopAppBarState = rememberTopAppBarState(), |
| canScroll: () -> Boolean = { true }, |
| snapAnimationSpec: AnimationSpec<Float>? = spring(stiffness = Spring.StiffnessMediumLow), |
| flingAnimationSpec: DecayAnimationSpec<Float>? = rememberSplineBasedDecay() |
| ): TopAppBarScrollBehavior = |
| EnterAlwaysScrollBehavior( |
| state = state, |
| snapAnimationSpec = snapAnimationSpec, |
| flingAnimationSpec = flingAnimationSpec, |
| canScroll = canScroll |
| ) |
| |
| /** |
| * Returns a [TopAppBarScrollBehavior] that adjusts its properties to affect the colors and |
| * height of the top app bar. |
| * |
| * A top app bar that is set up with this [TopAppBarScrollBehavior] will immediately collapse |
| * when the nested content is pulled up, and will expand back the collapsed area when the |
| * content is pulled all the way down. |
| * |
| * @param state the state object to be used to control or observe the top app bar's scroll |
| * state. See [rememberTopAppBarState] for a state that is remembered across compositions. |
| * @param canScroll a callback used to determine whether scroll events are to be |
| * handled by this [ExitUntilCollapsedScrollBehavior] |
| * @param snapAnimationSpec 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 |
| * @param flingAnimationSpec 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 |
| */ |
| @ExperimentalMaterial3Api |
| @Composable |
| fun exitUntilCollapsedScrollBehavior( |
| state: TopAppBarState = rememberTopAppBarState(), |
| canScroll: () -> Boolean = { true }, |
| snapAnimationSpec: AnimationSpec<Float>? = spring(stiffness = Spring.StiffnessMediumLow), |
| flingAnimationSpec: DecayAnimationSpec<Float>? = rememberSplineBasedDecay() |
| ): TopAppBarScrollBehavior = |
| ExitUntilCollapsedScrollBehavior( |
| state = state, |
| snapAnimationSpec = snapAnimationSpec, |
| flingAnimationSpec = flingAnimationSpec, |
| canScroll = canScroll |
| ) |
| } |
| |
| /** |
| * 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] |
| */ |
| @ExperimentalMaterial3Api |
| @Composable |
| fun rememberTopAppBarState( |
| initialHeightOffsetLimit: Float = -Float.MAX_VALUE, |
| initialHeightOffset: Float = 0f, |
| initialContentOffset: Float = 0f |
| ): TopAppBarState { |
| return rememberSaveable(saver = TopAppBarState.Saver) { |
| TopAppBarState( |
| initialHeightOffsetLimit, |
| 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( |
| initialHeightOffsetLimit: Float, |
| initialHeightOffset: Float, |
| initialContentOffset: Float |
| ) { |
| |
| /** |
| * The top app bar's height offset limit in pixels, which represents the limit that a top app |
| * bar is allowed to collapse to. |
| * |
| * Use this limit to coerce the [heightOffset] value when it's updated. |
| */ |
| var heightOffsetLimit by mutableStateOf(initialHeightOffsetLimit) |
| |
| /** |
| * 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.value |
| set(newOffset) { |
| _heightOffset.value = newOffset.coerceIn( |
| minimumValue = heightOffsetLimit, |
| maximumValue = 0f |
| ) |
| } |
| |
| /** |
| * 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 mutableStateOf(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 (heightOffsetLimit != 0f) { |
| heightOffset / heightOffsetLimit |
| } else { |
| 0f |
| } |
| |
| /** |
| * 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 (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.heightOffsetLimit, it.heightOffset, it.contentOffset) }, |
| restore = { |
| TopAppBarState( |
| initialHeightOffsetLimit = it[0], |
| initialHeightOffset = it[1], |
| initialContentOffset = it[2] |
| ) |
| } |
| ) |
| } |
| |
| private var _heightOffset = mutableStateOf(initialHeightOffset) |
| } |
| |
| /** |
| * Represents the colors used by a top app bar in different states. |
| * This implementation animates the container color according to the top app bar scroll state. It |
| * does not animate the leading, headline, or trailing colors. |
| */ |
| @ExperimentalMaterial3Api |
| @Stable |
| class TopAppBarColors internal constructor( |
| private val containerColor: Color, |
| private val scrolledContainerColor: Color, |
| internal val navigationIconContentColor: Color, |
| internal val titleContentColor: Color, |
| internal val actionIconContentColor: Color, |
| ) { |
| |
| /** |
| * Represents the container color used for the top app bar. |
| * |
| * A [colorTransitionFraction] provides a percentage value that can be used to generate a color. |
| * Usually, an app bar implementation will pass in a [colorTransitionFraction] read from |
| * the [TopAppBarState.collapsedFraction] or the [TopAppBarState.overlappedFraction]. |
| * |
| * @param colorTransitionFraction a `0.0` to `1.0` value that represents a color transition |
| * percentage |
| */ |
| @Composable |
| internal fun containerColor(colorTransitionFraction: Float): Color { |
| return lerp( |
| containerColor, |
| scrolledContainerColor, |
| FastOutLinearInEasing.transform(colorTransitionFraction) |
| ) |
| } |
| |
| override fun equals(other: Any?): Boolean { |
| if (this === other) return true |
| if (other == null || other !is TopAppBarColors) return false |
| |
| if (containerColor != other.containerColor) return false |
| if (scrolledContainerColor != other.scrolledContainerColor) return false |
| if (navigationIconContentColor != other.navigationIconContentColor) return false |
| if (titleContentColor != other.titleContentColor) return false |
| if (actionIconContentColor != other.actionIconContentColor) return false |
| |
| return true |
| } |
| |
| override fun hashCode(): Int { |
| var result = containerColor.hashCode() |
| result = 31 * result + scrolledContainerColor.hashCode() |
| result = 31 * result + navigationIconContentColor.hashCode() |
| result = 31 * result + titleContentColor.hashCode() |
| result = 31 * result + actionIconContentColor.hashCode() |
| |
| return result |
| } |
| } |
| |
| /** Contains default values used for the bottom app bar implementations. */ |
| object BottomAppBarDefaults { |
| |
| /** Default color used for [BottomAppBar] container **/ |
| val containerColor: Color @Composable get() = BottomAppBarTokens.ContainerColor.toColor() |
| |
| /** Default elevation used for [BottomAppBar] **/ |
| val ContainerElevation: Dp = BottomAppBarTokens.ContainerElevation |
| |
| /** |
| * Default padding used for [BottomAppBar] when content are default size (24dp) icons in |
| * [IconButton] that meet the minimum touch target (48.dp). |
| */ |
| val ContentPadding = PaddingValues( |
| start = BottomAppBarHorizontalPadding, |
| top = BottomAppBarVerticalPadding, |
| end = BottomAppBarHorizontalPadding |
| ) |
| |
| /** |
| * Default insets that will be used and consumed by [BottomAppBar]. |
| */ |
| val windowInsets: WindowInsets |
| @Composable |
| get() { |
| return WindowInsets.systemBarsForVisualComponents |
| .only(WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom) |
| } |
| |
| /** The color of a [BottomAppBar]'s [FloatingActionButton] */ |
| val bottomAppBarFabColor: Color |
| @Composable get() = |
| FabSecondaryTokens.ContainerColor.toColor() |
| } |
| |
| // Padding minus IconButton's min touch target expansion |
| private val BottomAppBarHorizontalPadding = 16.dp - 12.dp |
| internal val BottomAppBarVerticalPadding = 16.dp - 12.dp |
| |
| // Padding minus content padding |
| private val FABHorizontalPadding = 16.dp - BottomAppBarHorizontalPadding |
| private val FABVerticalPadding = 12.dp - BottomAppBarVerticalPadding |
| |
| /** |
| * A single-row top app bar that is designed to be called by the small and center aligned top app |
| * bar composables. |
| * |
| * This SingleRowTopAppBar has slots for a title, navigation icon, and actions. When the |
| * [centeredTitle] flag is true, the title will be horizontally aligned to the center of the top app |
| * bar width. |
| */ |
| @OptIn(ExperimentalMaterial3Api::class) |
| @Composable |
| private fun SingleRowTopAppBar( |
| modifier: Modifier = Modifier, |
| title: @Composable () -> Unit, |
| titleTextStyle: TextStyle, |
| centeredTitle: Boolean, |
| navigationIcon: @Composable () -> Unit, |
| actions: @Composable RowScope.() -> Unit, |
| windowInsets: WindowInsets, |
| colors: TopAppBarColors, |
| scrollBehavior: TopAppBarScrollBehavior? |
| ) { |
| // Sets the app bar's height offset to collapse the entire bar's height when content is |
| // scrolled. |
| val heightOffsetLimit = |
| with(LocalDensity.current) { -TopAppBarSmallTokens.ContainerHeight.toPx() } |
| SideEffect { |
| if (scrollBehavior?.state?.heightOffsetLimit != heightOffsetLimit) { |
| scrollBehavior?.state?.heightOffsetLimit = heightOffsetLimit |
| } |
| } |
| |
| // 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 = scrollBehavior?.state?.overlappedFraction ?: 0f |
| val fraction = if (colorTransitionFraction > 0.01f) 1f else 0f |
| val appBarContainerColor by animateColorAsState( |
| targetValue = colors.containerColor(fraction), |
| animationSpec = spring(stiffness = Spring.StiffnessMediumLow) |
| ) |
| |
| // Wrap the given actions in a Row. |
| val actionsRow = @Composable { |
| Row( |
| horizontalArrangement = Arrangement.End, |
| verticalAlignment = Alignment.CenterVertically, |
| content = actions |
| ) |
| } |
| |
| // Set up support for resizing the top app bar when vertically dragging the bar itself. |
| val appBarDragModifier = if (scrollBehavior != null && !scrollBehavior.isPinned) { |
| Modifier.draggable( |
| orientation = Orientation.Vertical, |
| state = rememberDraggableState { delta -> |
| scrollBehavior.state.heightOffset = scrollBehavior.state.heightOffset + delta |
| }, |
| onDragStopped = { velocity -> |
| settleAppBar( |
| scrollBehavior.state, |
| velocity, |
| scrollBehavior.flingAnimationSpec, |
| scrollBehavior.snapAnimationSpec |
| ) |
| } |
| ) |
| } else { |
| Modifier |
| } |
| |
| // 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.then(appBarDragModifier), color = appBarContainerColor) { |
| val height = LocalDensity.current.run { |
| TopAppBarSmallTokens.ContainerHeight.toPx() + (scrollBehavior?.state?.heightOffset |
| ?: 0f) |
| } |
| TopAppBarLayout( |
| modifier = Modifier |
| .windowInsetsPadding(windowInsets) |
| // clip after padding so we don't show the title over the inset area |
| .clipToBounds(), |
| heightPx = height, |
| 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, |
| ) |
| } |
| } |
| |
| /** |
| * A two-rows top app bar that is designed to be called by the Large and Medium top app bar |
| * composables. |
| * |
| * @throws [IllegalArgumentException] if the given [maxHeight] is equal or smaller than the |
| * [pinnedHeight] |
| */ |
| @OptIn(ExperimentalMaterial3Api::class) |
| @Composable |
| private fun TwoRowsTopAppBar( |
| modifier: Modifier = Modifier, |
| title: @Composable () -> Unit, |
| titleTextStyle: TextStyle, |
| titleBottomPadding: Dp, |
| smallTitle: @Composable () -> Unit, |
| smallTitleTextStyle: TextStyle, |
| navigationIcon: @Composable () -> Unit, |
| actions: @Composable RowScope.() -> Unit, |
| windowInsets: WindowInsets, |
| colors: TopAppBarColors, |
| maxHeight: Dp, |
| pinnedHeight: Dp, |
| scrollBehavior: TopAppBarScrollBehavior? |
| ) { |
| if (maxHeight <= pinnedHeight) { |
| throw IllegalArgumentException( |
| "A TwoRowsTopAppBar max height should be greater than its pinned height" |
| ) |
| } |
| val pinnedHeightPx: Float |
| val maxHeightPx: Float |
| val titleBottomPaddingPx: Int |
| LocalDensity.current.run { |
| pinnedHeightPx = pinnedHeight.toPx() |
| maxHeightPx = maxHeight.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 != pinnedHeightPx - maxHeightPx) { |
| scrollBehavior?.state?.heightOffsetLimit = pinnedHeightPx - maxHeightPx |
| } |
| } |
| |
| // Obtain the container Color from the TopAppBarColors using the `collapsedFraction`, as the |
| // bottom part of this TwoRowsTopAppBar changes color at the same rate the app bar expands or |
| // 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?.collapsedFraction ?: 0f |
| val appBarContainerColor by rememberUpdatedState(colors.containerColor(colorTransitionFraction)) |
| |
| // Wrap the given actions in a Row. |
| val actionsRow = @Composable { |
| Row( |
| horizontalArrangement = Arrangement.End, |
| verticalAlignment = Alignment.CenterVertically, |
| content = actions |
| ) |
| } |
| val topTitleAlpha = TopTitleAlphaEasing.transform(colorTransitionFraction) |
| val bottomTitleAlpha = 1f - colorTransitionFraction |
| // Hide the top row title semantics when its alpha value goes below 0.5 threshold. |
| // Hide the bottom row title semantics when the top title semantics are active. |
| val hideTopRowSemantics = colorTransitionFraction < 0.5f |
| val hideBottomRowSemantics = !hideTopRowSemantics |
| |
| // Set up support for resizing the top app bar when vertically dragging the bar itself. |
| val appBarDragModifier = if (scrollBehavior != null && !scrollBehavior.isPinned) { |
| Modifier.draggable( |
| orientation = Orientation.Vertical, |
| state = rememberDraggableState { delta -> |
| scrollBehavior.state.heightOffset = scrollBehavior.state.heightOffset + delta |
| }, |
| onDragStopped = { velocity -> |
| settleAppBar( |
| scrollBehavior.state, |
| velocity, |
| scrollBehavior.flingAnimationSpec, |
| scrollBehavior.snapAnimationSpec |
| ) |
| } |
| ) |
| } else { |
| Modifier |
| } |
| |
| Surface(modifier = modifier.then(appBarDragModifier)) { |
| Column { |
| TopAppBarLayout( |
| modifier = Modifier |
| .background(color = appBarContainerColor) |
| .windowInsetsPadding(windowInsets) |
| // clip after padding so we don't show the title over the inset area |
| .clipToBounds(), |
| heightPx = pinnedHeightPx, |
| navigationIconContentColor = |
| colors.navigationIconContentColor, |
| titleContentColor = colors.titleContentColor, |
| actionIconContentColor = |
| colors.actionIconContentColor, |
| title = smallTitle, |
| titleTextStyle = smallTitleTextStyle, |
| titleAlpha = topTitleAlpha, |
| titleVerticalArrangement = Arrangement.Center, |
| titleHorizontalArrangement = Arrangement.Start, |
| titleBottomPadding = 0, |
| hideTitleSemantics = hideTopRowSemantics, |
| navigationIcon = navigationIcon, |
| actions = actionsRow, |
| ) |
| TopAppBarLayout( |
| modifier = Modifier.clipToBounds(), |
| heightPx = maxHeightPx - pinnedHeightPx + (scrollBehavior?.state?.heightOffset |
| ?: 0f), |
| navigationIconContentColor = |
| colors.navigationIconContentColor, |
| titleContentColor = colors.titleContentColor, |
| actionIconContentColor = |
| colors.actionIconContentColor, |
| title = title, |
| titleTextStyle = titleTextStyle, |
| titleAlpha = bottomTitleAlpha, |
| titleVerticalArrangement = Arrangement.Bottom, |
| titleHorizontalArrangement = Arrangement.Start, |
| titleBottomPadding = titleBottomPaddingPx, |
| hideTitleSemantics = hideBottomRowSemantics, |
| navigationIcon = {}, |
| actions = {} |
| ) |
| } |
| } |
| } |
| |
| /** |
| * The base [Layout] for all top app bars. This function lays out a top app bar navigation icon |
| * (leading icon), a title (header), and action icons (trailing icons). Note that the navigation and |
| * the actions are optional. |
| * |
| * @param heightPx the total height this layout is capped to |
| * @param navigationIconContentColor the content color that will be applied via a |
| * [LocalContentColor] when composing the navigation icon |
| * @param titleContentColor the color that will be applied via a [LocalContentColor] when composing |
| * the title |
| * @param actionIconContentColor the content color that will be applied via a [LocalContentColor] |
| * when composing the action icons |
| * @param title the top app bar title (header) |
| * @param titleTextStyle the title's text style |
| * @param modifier a [Modifier] |
| * @param titleAlpha the title's alpha |
| * @param titleVerticalArrangement the title's vertical arrangement |
| * @param titleHorizontalArrangement the title's horizontal arrangement |
| * @param titleBottomPadding the title's bottom padding |
| * @param hideTitleSemantics hides the title node from the semantic tree. Apply this |
| * boolean when this layout is part of a [TwoRowsTopAppBar] to hide the title's semantics |
| * from accessibility services. This is needed to avoid having multiple titles visible to |
| * accessibility services at the same time, when animating between collapsed / expanded states. |
| * @param navigationIcon a navigation icon [Composable] |
| * @param actions actions [Composable] |
| */ |
| @Composable |
| private fun TopAppBarLayout( |
| modifier: Modifier, |
| heightPx: Float, |
| navigationIconContentColor: Color, |
| titleContentColor: Color, |
| actionIconContentColor: Color, |
| title: @Composable () -> Unit, |
| titleTextStyle: TextStyle, |
| titleAlpha: Float, |
| titleVerticalArrangement: Arrangement.Vertical, |
| titleHorizontalArrangement: Arrangement.Horizontal, |
| titleBottomPadding: Int, |
| hideTitleSemantics: Boolean, |
| navigationIcon: @Composable () -> Unit, |
| actions: @Composable () -> Unit, |
| ) { |
| Layout( |
| { |
| Box( |
| Modifier |
| .layoutId("navigationIcon") |
| .padding(start = TopAppBarHorizontalPadding) |
| ) { |
| CompositionLocalProvider( |
| LocalContentColor provides navigationIconContentColor, |
| content = navigationIcon |
| ) |
| } |
| Box( |
| Modifier |
| .layoutId("title") |
| .padding(horizontal = TopAppBarHorizontalPadding) |
| .then(if (hideTitleSemantics) Modifier.clearAndSetSemantics { } else Modifier) |
| ) { |
| ProvideTextStyle(value = titleTextStyle) { |
| CompositionLocalProvider( |
| LocalContentColor provides titleContentColor.copy(alpha = titleAlpha), |
| content = title |
| ) |
| } |
| } |
| Box( |
| Modifier |
| .layoutId("actionIcons") |
| .padding(end = TopAppBarHorizontalPadding) |
| ) { |
| CompositionLocalProvider( |
| LocalContentColor provides actionIconContentColor, |
| content = actions |
| ) |
| } |
| }, |
| modifier = modifier |
| ) { measurables, constraints -> |
| val navigationIconPlaceable = |
| measurables.first { it.layoutId == "navigationIcon" } |
| .measure(constraints.copy(minWidth = 0)) |
| val actionIconsPlaceable = |
| measurables.first { it.layoutId == "actionIcons" } |
| .measure(constraints.copy(minWidth = 0)) |
| |
| val maxTitleWidth = if (constraints.maxWidth == Constraints.Infinity) { |
| constraints.maxWidth |
| } else { |
| (constraints.maxWidth - navigationIconPlaceable.width - actionIconsPlaceable.width) |
| .coerceAtLeast(0) |
| } |
| val titlePlaceable = |
| measurables.first { it.layoutId == "title" } |
| .measure(constraints.copy(minWidth = 0, maxWidth = maxTitleWidth)) |
| |
| // Locate the title's baseline. |
| val titleBaseline = |
| if (titlePlaceable[LastBaseline] != AlignmentLine.Unspecified) { |
| titlePlaceable[LastBaseline] |
| } else { |
| 0 |
| } |
| |
| val layoutHeight = heightPx.roundToInt() |
| |
| layout(constraints.maxWidth, layoutHeight) { |
| // Navigation icon |
| navigationIconPlaceable.placeRelative( |
| x = 0, |
| y = (layoutHeight - navigationIconPlaceable.height) / 2 |
| ) |
| |
| // Title |
| titlePlaceable.placeRelative( |
| x = when (titleHorizontalArrangement) { |
| Arrangement.Center -> (constraints.maxWidth - titlePlaceable.width) / 2 |
| Arrangement.End -> |
| constraints.maxWidth - titlePlaceable.width - actionIconsPlaceable.width |
| // Arrangement.Start. |
| // An TopAppBarTitleInset will make sure the title is offset in case the |
| // navigation icon is missing. |
| else -> max(TopAppBarTitleInset.roundToPx(), navigationIconPlaceable.width) |
| }, |
| y = when (titleVerticalArrangement) { |
| Arrangement.Center -> (layoutHeight - titlePlaceable.height) / 2 |
| // Apply bottom padding from the title's baseline only when the Arrangement is |
| // "Bottom". |
| Arrangement.Bottom -> |
| if (titleBottomPadding == 0) layoutHeight - titlePlaceable.height |
| else layoutHeight - titlePlaceable.height - max( |
| 0, |
| titleBottomPadding - titlePlaceable.height + titleBaseline |
| ) |
| // Arrangement.Top |
| else -> 0 |
| } |
| ) |
| |
| // Action icons |
| actionIconsPlaceable.placeRelative( |
| x = constraints.maxWidth - actionIconsPlaceable.width, |
| y = (layoutHeight - actionIconsPlaceable.height) / 2 |
| ) |
| } |
| } |
| } |
| |
| /** |
| * Returns a [TopAppBarScrollBehavior] that only adjusts its content offset, without adjusting any |
| * properties that affect the height of a top app bar. |
| * |
| * @param state a [TopAppBarState] |
| * @param canScroll a callback used to determine whether scroll events are to be |
| * handled by this [PinnedScrollBehavior] |
| */ |
| @OptIn(ExperimentalMaterial3Api::class) |
| 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 |
| } |
| } |
| } |
| |
| /** |
| * A [TopAppBarScrollBehavior] that adjusts its properties to affect the colors and height of a top |
| * app bar. |
| * |
| * A top app bar that is set up with this [TopAppBarScrollBehavior] will immediately collapse when |
| * the nested content is pulled up, and will immediately appear when the content is pulled down. |
| * |
| * @param state a [TopAppBarState] |
| * @param snapAnimationSpec 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 |
| * @param flingAnimationSpec 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 |
| * @param canScroll a callback used to determine whether scroll events are to be |
| * handled by this [EnterAlwaysScrollBehavior] |
| */ |
| @OptIn(ExperimentalMaterial3Api::class) |
| 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 |
| ) |
| } |
| } |
| } |
| |
| /** |
| * A [TopAppBarScrollBehavior] that adjusts its properties to affect the colors and height of a top |
| * app bar. |
| * |
| * A top app bar that is set up with this [TopAppBarScrollBehavior] will immediately collapse when |
| * the nested content is pulled up, and will expand back the collapsed area when the content is |
| * pulled all the way down. |
| * |
| * @param state a [TopAppBarState] |
| * @param snapAnimationSpec 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 |
| * @param flingAnimationSpec 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 |
| * @param canScroll a callback used to determine whether scroll events are to be |
| * handled by this [ExitUntilCollapsedScrollBehavior] |
| */ |
| @OptIn(ExperimentalMaterial3Api::class) |
| 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 = 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 = 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 = 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 |
| ) |
| } |
| } |
| } |
| |
| /** |
| * Settles the app bar by flinging, in case the given velocity is greater than zero, and snapping |
| * after the fling settles. |
| */ |
| @OptIn(ExperimentalMaterial3Api::class) |
| private suspend fun settleAppBar( |
| state: TopAppBarState, |
| velocity: Float, |
| flingAnimationSpec: DecayAnimationSpec<Float>?, |
| snapAnimationSpec: AnimationSpec<Float>? |
| ): 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.collapsedFraction < 0.01f || state.collapsedFraction == 1f) { |
| return Velocity.Zero |
| } |
| var remainingVelocity = velocity |
| // In case there is an initial velocity that was left after a previous user fling, animate to |
| // continue the motion to expand or collapse the app bar. |
| if (flingAnimationSpec != null && abs(velocity) > 1f) { |
| var lastValue = 0f |
| AnimationState( |
| initialValue = 0f, |
| initialVelocity = velocity, |
| ) |
| .animateDecay(flingAnimationSpec) { |
| val delta = value - lastValue |
| val initialHeightOffset = state.heightOffset |
| state.heightOffset = initialHeightOffset + delta |
| val consumed = abs(initialHeightOffset - state.heightOffset) |
| lastValue = value |
| remainingVelocity = this.velocity |
| // avoid rounding errors and stop if anything is unconsumed |
| if (abs(delta - consumed) > 0.5f) this.cancelAnimation() |
| } |
| } |
| // Snap if animation specs were provided. |
| if (snapAnimationSpec != null) { |
| 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) |
| } |
| |
| // An easing function used to compute the alpha value that is applied to the top title part of a |
| // Medium or Large app bar. |
| /*@VisibleForTesting*/ |
| internal val TopTitleAlphaEasing = CubicBezierEasing(.8f, 0f, .8f, .15f) |
| |
| private val MediumTitleBottomPadding = 24.dp |
| private val LargeTitleBottomPadding = 28.dp |
| private val TopAppBarHorizontalPadding = 4.dp |
| |
| // A title inset when the App-Bar is a Medium or Large one. Also used to size a spacer when the |
| // navigation icon is missing. |
| private val TopAppBarTitleInset = 16.dp - TopAppBarHorizontalPadding |