1 /*
<lambda>null2  * Copyright 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.compose.material3
18 
19 import androidx.compose.animation.AnimatedVisibility
20 import androidx.compose.animation.core.Animatable
21 import androidx.compose.animation.core.AnimationSpec
22 import androidx.compose.animation.core.AnimationState
23 import androidx.compose.animation.core.AnimationVector1D
24 import androidx.compose.animation.core.DecayAnimationSpec
25 import androidx.compose.animation.core.FiniteAnimationSpec
26 import androidx.compose.animation.core.animateDecay
27 import androidx.compose.animation.core.animateDpAsState
28 import androidx.compose.animation.core.animateFloat
29 import androidx.compose.animation.core.animateTo
30 import androidx.compose.animation.core.updateTransition
31 import androidx.compose.animation.expandHorizontally
32 import androidx.compose.animation.expandVertically
33 import androidx.compose.animation.rememberSplineBasedDecay
34 import androidx.compose.animation.shrinkHorizontally
35 import androidx.compose.animation.shrinkVertically
36 import androidx.compose.foundation.background
37 import androidx.compose.foundation.gestures.DraggableState
38 import androidx.compose.foundation.gestures.Orientation
39 import androidx.compose.foundation.gestures.draggable
40 import androidx.compose.foundation.horizontalScroll
41 import androidx.compose.foundation.interaction.Interaction
42 import androidx.compose.foundation.interaction.MutableInteractionSource
43 import androidx.compose.foundation.layout.Arrangement
44 import androidx.compose.foundation.layout.Box
45 import androidx.compose.foundation.layout.Column
46 import androidx.compose.foundation.layout.ColumnScope
47 import androidx.compose.foundation.layout.PaddingValues
48 import androidx.compose.foundation.layout.Row
49 import androidx.compose.foundation.layout.RowScope
50 import androidx.compose.foundation.layout.defaultMinSize
51 import androidx.compose.foundation.layout.fillMaxSize
52 import androidx.compose.foundation.layout.heightIn
53 import androidx.compose.foundation.layout.padding
54 import androidx.compose.foundation.layout.widthIn
55 import androidx.compose.foundation.rememberScrollState
56 import androidx.compose.foundation.verticalScroll
57 import androidx.compose.material3.FloatingToolbarDefaults.horizontalEnterTransition
58 import androidx.compose.material3.FloatingToolbarDefaults.horizontalExitTransition
59 import androidx.compose.material3.FloatingToolbarDefaults.standardFloatingToolbarColors
60 import androidx.compose.material3.FloatingToolbarDefaults.verticalEnterTransition
61 import androidx.compose.material3.FloatingToolbarDefaults.verticalExitTransition
62 import androidx.compose.material3.FloatingToolbarDefaults.vibrantFloatingToolbarColors
63 import androidx.compose.material3.FloatingToolbarExitDirection.Companion.Bottom
64 import androidx.compose.material3.FloatingToolbarExitDirection.Companion.End
65 import androidx.compose.material3.FloatingToolbarExitDirection.Companion.Start
66 import androidx.compose.material3.FloatingToolbarExitDirection.Companion.Top
67 import androidx.compose.material3.FloatingToolbarState.Companion.Saver
68 import androidx.compose.material3.internal.Strings
69 import androidx.compose.material3.internal.getString
70 import androidx.compose.material3.internal.parentSemantics
71 import androidx.compose.material3.internal.rememberAccessibilityServiceState
72 import androidx.compose.material3.tokens.ColorSchemeKeyTokens
73 import androidx.compose.material3.tokens.ElevationTokens
74 import androidx.compose.material3.tokens.FabBaselineTokens
75 import androidx.compose.material3.tokens.FabMediumTokens
76 import androidx.compose.material3.tokens.FloatingToolbarTokens
77 import androidx.compose.material3.tokens.MotionSchemeKeyTokens
78 import androidx.compose.runtime.Composable
79 import androidx.compose.runtime.CompositionLocalProvider
80 import androidx.compose.runtime.Immutable
81 import androidx.compose.runtime.Stable
82 import androidx.compose.runtime.State
83 import androidx.compose.runtime.getValue
84 import androidx.compose.runtime.mutableFloatStateOf
85 import androidx.compose.runtime.mutableStateOf
86 import androidx.compose.runtime.remember
87 import androidx.compose.runtime.rememberUpdatedState
88 import androidx.compose.runtime.saveable.Saver
89 import androidx.compose.runtime.saveable.listSaver
90 import androidx.compose.runtime.saveable.rememberSaveable
91 import androidx.compose.runtime.setValue
92 import androidx.compose.ui.Alignment
93 import androidx.compose.ui.Modifier
94 import androidx.compose.ui.geometry.Offset
95 import androidx.compose.ui.graphics.Color
96 import androidx.compose.ui.graphics.Shape
97 import androidx.compose.ui.graphics.graphicsLayer
98 import androidx.compose.ui.graphics.takeOrElse
99 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
100 import androidx.compose.ui.input.nestedscroll.NestedScrollSource
101 import androidx.compose.ui.input.nestedscroll.nestedScrollModifierNode
102 import androidx.compose.ui.layout.AlignmentLine
103 import androidx.compose.ui.layout.Layout
104 import androidx.compose.ui.layout.Measurable
105 import androidx.compose.ui.layout.MeasureResult
106 import androidx.compose.ui.layout.MeasureScope
107 import androidx.compose.ui.layout.layout
108 import androidx.compose.ui.layout.onGloballyPositioned
109 import androidx.compose.ui.layout.positionInParent
110 import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
111 import androidx.compose.ui.node.DelegatableNode
112 import androidx.compose.ui.node.DelegatingNode
113 import androidx.compose.ui.node.LayoutModifierNode
114 import androidx.compose.ui.node.ModifierNodeElement
115 import androidx.compose.ui.node.requireDensity
116 import androidx.compose.ui.platform.InspectorInfo
117 import androidx.compose.ui.semantics.CustomAccessibilityAction
118 import androidx.compose.ui.semantics.customActions
119 import androidx.compose.ui.unit.Constraints
120 import androidx.compose.ui.unit.Dp
121 import androidx.compose.ui.unit.IntSize
122 import androidx.compose.ui.unit.LayoutDirection
123 import androidx.compose.ui.unit.Velocity
124 import androidx.compose.ui.unit.dp
125 import androidx.compose.ui.unit.lerp
126 import androidx.compose.ui.util.fastRoundToInt
127 import kotlin.jvm.JvmInline
128 import kotlin.math.abs
129 import kotlin.math.roundToInt
130 import kotlinx.coroutines.launch
131 
132 /**
133  * A horizontal floating toolbar displays navigation and key actions in a [Row]. It can be
134  * positioned anywhere on the screen and floats over the rest of the content.
135  *
136  * Note: This component will stay expanded to maintain the toolbar visibility for users with touch
137  * exploration services enabled (e.g., TalkBack).
138  *
139  * @sample androidx.compose.material3.samples.ExpandableHorizontalFloatingToolbarSample
140  * @sample androidx.compose.material3.samples.ScrollableHorizontalFloatingToolbarSample
141  * @param expanded whether the FloatingToolbar is in expanded mode, i.e. showing [leadingContent]
142  *   and [trailingContent]. Note that the toolbar will stay expanded in case a touch exploration
143  *   service (e.g., TalkBack) is active.
144  * @param modifier the [Modifier] to be applied to this FloatingToolbar.
145  * @param colors the colors used for this floating toolbar. There are two predefined
146  *   [FloatingToolbarColors] at [FloatingToolbarDefaults.standardFloatingToolbarColors] and
147  *   [FloatingToolbarDefaults.vibrantFloatingToolbarColors] which you can use or modify.
148  * @param contentPadding the padding applied to the content of this FloatingToolbar.
149  * @param scrollBehavior a [FloatingToolbarScrollBehavior]. If null, this FloatingToolbar will not
150  *   automatically react to scrolling. Note that the toolbar will not react to scrolling in case a
151  *   touch exploration service (e.g., TalkBack) is active.
152  * @param shape the shape used for this FloatingToolbar.
153  * @param leadingContent the leading content of this FloatingToolbar. The default layout here is a
154  *   [Row], so content inside will be placed horizontally. Only showing if [expanded] is true.
155  * @param trailingContent the trailing content of this FloatingToolbar. The default layout here is a
156  *   [Row], so content inside will be placed horizontally. Only showing if [expanded] is true.
157  * @param expandedShadowElevation the elevation for the shadow below this floating toolbar when
158  *   expanded.
159  * @param collapsedShadowElevation the elevation for the shadow below this floating toolbar when
160  *   collapsed.
161  * @param content the main content of this FloatingToolbar. The default layout here is a [Row], so
162  *   content inside will be placed horizontally.
163  */
164 @ExperimentalMaterial3ExpressiveApi
165 @Composable
166 fun HorizontalFloatingToolbar(
167     expanded: Boolean,
168     modifier: Modifier = Modifier,
169     colors: FloatingToolbarColors = FloatingToolbarDefaults.standardFloatingToolbarColors(),
170     contentPadding: PaddingValues = FloatingToolbarDefaults.ContentPadding,
171     scrollBehavior: FloatingToolbarScrollBehavior? = null,
172     shape: Shape = FloatingToolbarDefaults.ContainerShape,
173     leadingContent: @Composable (RowScope.() -> Unit)? = null,
174     trailingContent: @Composable (RowScope.() -> Unit)? = null,
175     expandedShadowElevation: Dp = FloatingToolbarDefaults.ContainerExpandedElevation,
176     collapsedShadowElevation: Dp = FloatingToolbarDefaults.ContainerCollapsedElevation,
177     content: @Composable RowScope.() -> Unit
178 ) {
179     val touchExplorationServiceEnabled by rememberTouchExplorationService()
180     var forceCollapse by rememberSaveable { mutableStateOf(false) }
181     HorizontalFloatingToolbarLayout(
182         modifier = modifier,
183         expanded = !forceCollapse && (touchExplorationServiceEnabled || expanded),
184         onA11yForceCollapse = { force -> forceCollapse = force },
185         colors = colors,
186         contentPadding = contentPadding,
187         scrollBehavior = if (!touchExplorationServiceEnabled) scrollBehavior else null,
188         shape = shape,
189         leadingContent = leadingContent,
190         trailingContent = trailingContent,
191         expandedShadowElevation = expandedShadowElevation,
192         collapsedShadowElevation = collapsedShadowElevation,
193         content = content
194     )
195 }
196 
197 /**
198  * A floating toolbar that displays horizontally. The bar features its content within a [Row], and
199  * an adjacent floating icon button. It can be positioned anywhere on the screen, floating above
200  * other content, and even in a `Scaffold`'s floating action button slot. Its [expanded] flag
201  * controls the visibility of the actions with a slide animations.
202  *
203  * Note: This component will stay expanded to maintain the toolbar visibility for users with touch
204  * exploration services enabled (e.g., TalkBack).
205  *
206  * In case the toolbar is aligned to the right or the left of the screen, you may apply a
207  * [FloatingToolbarDefaults.floatingToolbarVerticalNestedScroll] `Modifier` to update the [expanded]
208  * state when scrolling occurs, as this sample shows:
209  *
210  * @sample androidx.compose.material3.samples.HorizontalFloatingToolbarWithFabSample
211  *
212  * In case the toolbar is positioned along a center edge of the screen (like top or bottom center),
213  * it's recommended to maintain the expanded state on scroll and to attach a [scrollBehavior] in
214  * order to hide or show the entire component, as this sample shows:
215  *
216  * @sample androidx.compose.material3.samples.CenteredHorizontalFloatingToolbarWithFabSample
217  *
218  * Note that if your app uses a `Snackbar`, it's best to position the toolbar in a `Scaffold`'s FAB
219  * slot. This ensures the `Snackbar` appears above the toolbar, preventing any visual overlap or
220  * interference. See this sample:
221  *
222  * @sample androidx.compose.material3.samples.HorizontalFloatingToolbarAsScaffoldFabSample
223  * @param expanded whether the floating toolbar is expanded or not. In its expanded state, the FAB
224  *   and the toolbar content are organized horizontally. Otherwise, only the FAB is visible. Note
225  *   that the toolbar will stay expanded in case a touch exploration service (e.g., TalkBack) is
226  *   active.
227  * @param floatingActionButton a floating action button to be displayed by the toolbar. It's
228  *   recommended to use a [FloatingToolbarDefaults.VibrantFloatingActionButton] or
229  *   [FloatingToolbarDefaults.StandardFloatingActionButton] that is styled to match the [colors].
230  *   Note that the provided FAB's size is controlled by the floating toolbar and animates according
231  *   to its state. In case a custom FAB is provided, make sure it's set with a
232  *   [Modifier.fillMaxSize] to be sized correctly.
233  * @param modifier the [Modifier] to be applied to this floating toolbar.
234  * @param colors the colors used for this floating toolbar. There are two predefined
235  *   [FloatingToolbarColors] at [FloatingToolbarDefaults.standardFloatingToolbarColors] and
236  *   [FloatingToolbarDefaults.vibrantFloatingToolbarColors] which you can use or modify. See also
237  *   [floatingActionButton] for more information on the right FAB to use for proper styling.
238  * @param contentPadding the padding applied to the content of this floating toolbar.
239  * @param scrollBehavior a [FloatingToolbarScrollBehavior]. If provided, this FloatingToolbar will
240  *   automatically react to scrolling. If your toolbar is positioned along a center edge of the
241  *   screen (like top or bottom center), it's best to use this scroll behavior to make the entire
242  *   toolbar scroll off-screen as the user scrolls. This would prevent the FAB from appearing
243  *   off-center, which may occur in this case when using the [expanded] flag to simply expand or
244  *   collapse the toolbar. Note that the toolbar will not react to scrolling in case a touch
245  *   exploration service (e.g., TalkBack) is active.
246  * @param shape the shape used for this floating toolbar content.
247  * @param floatingActionButtonPosition the position of the floating toolbar's floating action
248  *   button. By default, the FAB is placed at the end of the toolbar (i.e. aligned to the right in
249  *   left-to-right layout, or to the left in right-to-left layout).
250  * @param animationSpec the animation spec to use for this floating toolbar expand and collapse
251  *   animation.
252  * @param expandedShadowElevation the elevation for the shadow below this floating toolbar when
253  *   expanded.
254  * @param collapsedShadowElevation the elevation for the shadow below this floating toolbar when
255  *   collapsed.
256  * @param content the main content of this floating toolbar. The default layout here is a [Row], so
257  *   content inside will be placed horizontally.
258  */
259 @ExperimentalMaterial3ExpressiveApi
260 @Composable
HorizontalFloatingToolbarnull261 fun HorizontalFloatingToolbar(
262     expanded: Boolean,
263     floatingActionButton: @Composable () -> Unit,
264     modifier: Modifier = Modifier,
265     colors: FloatingToolbarColors = FloatingToolbarDefaults.standardFloatingToolbarColors(),
266     contentPadding: PaddingValues = FloatingToolbarDefaults.ContentPadding,
267     scrollBehavior: FloatingToolbarScrollBehavior? = null,
268     shape: Shape = FloatingToolbarDefaults.ContainerShape,
269     floatingActionButtonPosition: FloatingToolbarHorizontalFabPosition =
270         FloatingToolbarHorizontalFabPosition.End,
271     animationSpec: FiniteAnimationSpec<Float> = FloatingToolbarDefaults.animationSpec(),
272     expandedShadowElevation: Dp = FloatingToolbarDefaults.ContainerExpandedElevationWithFab,
273     collapsedShadowElevation: Dp = FloatingToolbarDefaults.ContainerCollapsedElevationWithFab,
274     content: @Composable RowScope.() -> Unit
275 ) {
276     val touchExplorationServiceEnabled by rememberTouchExplorationService()
277     var forceCollapse by rememberSaveable { mutableStateOf(false) }
278     HorizontalFloatingToolbarWithFabLayout(
279         modifier = modifier,
280         expanded = !forceCollapse && (touchExplorationServiceEnabled || expanded),
281         onA11yForceCollapse = { force -> forceCollapse = force },
282         colors = colors,
283         toolbarToFabGap = FloatingToolbarDefaults.ToolbarToFabGap,
284         toolbarContentPadding = contentPadding,
285         scrollBehavior = if (!touchExplorationServiceEnabled) scrollBehavior else null,
286         toolbarShape = shape,
287         animationSpec = animationSpec,
288         fab = floatingActionButton,
289         fabPosition = floatingActionButtonPosition,
290         expandedShadowElevation = expandedShadowElevation,
291         collapsedShadowElevation = collapsedShadowElevation,
292         toolbar = content
293     )
294 }
295 
296 /**
297  * A vertical floating toolbar displays navigation and key actions in a [Column]. It can be
298  * positioned anywhere on the screen and floats over the rest of the content.
299  *
300  * Note: This component will stay expanded to maintain the toolbar visibility for users with touch
301  * exploration services enabled (e.g., TalkBack).
302  *
303  * @sample androidx.compose.material3.samples.ExpandableVerticalFloatingToolbarSample
304  * @sample androidx.compose.material3.samples.ScrollableVerticalFloatingToolbarSample
305  * @param expanded whether the FloatingToolbar is in expanded mode, i.e. showing [leadingContent]
306  *   and [trailingContent]. Note that the toolbar will stay expanded in case a touch exploration
307  *   service (e.g., TalkBack) is active.
308  * @param modifier the [Modifier] to be applied to this FloatingToolbar.
309  * @param colors the colors used for this floating toolbar. There are two predefined
310  *   [FloatingToolbarColors] at [FloatingToolbarDefaults.standardFloatingToolbarColors] and
311  *   [FloatingToolbarDefaults.vibrantFloatingToolbarColors] which you can use or modify.
312  * @param contentPadding the padding applied to the content of this FloatingToolbar.
313  * @param scrollBehavior a [FloatingToolbarScrollBehavior]. If null, this FloatingToolbar will not
314  *   automatically react to scrolling. Note that the toolbar will not react to scrolling in case a
315  *   touch exploration service (e.g., TalkBack) is active.
316  * @param shape the shape used for this FloatingToolbar.
317  * @param leadingContent the leading content of this FloatingToolbar. The default layout here is a
318  *   [Column], so content inside will be placed vertically. Only showing if [expanded] is true.
319  * @param trailingContent the trailing content of this FloatingToolbar. The default layout here is a
320  *   [Column], so content inside will be placed vertically. Only showing if [expanded] is true.
321  * @param expandedShadowElevation the elevation for the shadow below this floating toolbar when
322  *   expanded.
323  * @param collapsedShadowElevation the elevation for the shadow below this floating toolbar when
324  *   collapsed.
325  * @param content the main content of this FloatingToolbar. The default layout here is a [Column],
326  *   so content inside will be placed vertically.
327  */
328 @ExperimentalMaterial3ExpressiveApi
329 @Composable
VerticalFloatingToolbarnull330 fun VerticalFloatingToolbar(
331     expanded: Boolean,
332     modifier: Modifier = Modifier,
333     colors: FloatingToolbarColors = FloatingToolbarDefaults.standardFloatingToolbarColors(),
334     contentPadding: PaddingValues = FloatingToolbarDefaults.ContentPadding,
335     scrollBehavior: FloatingToolbarScrollBehavior? = null,
336     shape: Shape = FloatingToolbarDefaults.ContainerShape,
337     leadingContent: @Composable (ColumnScope.() -> Unit)? = null,
338     trailingContent: @Composable (ColumnScope.() -> Unit)? = null,
339     expandedShadowElevation: Dp = FloatingToolbarDefaults.ContainerExpandedElevation,
340     collapsedShadowElevation: Dp = FloatingToolbarDefaults.ContainerCollapsedElevation,
341     content: @Composable ColumnScope.() -> Unit
342 ) {
343     val touchExplorationServiceEnabled by rememberTouchExplorationService()
344     var forceCollapse by rememberSaveable { mutableStateOf(false) }
345     VerticalFloatingToolbarLayout(
346         modifier = modifier,
347         expanded = !forceCollapse && (touchExplorationServiceEnabled || expanded),
348         onA11yForceCollapse = { force -> forceCollapse = force },
349         colors = colors,
350         contentPadding = contentPadding,
351         scrollBehavior = if (!touchExplorationServiceEnabled) scrollBehavior else null,
352         shape = shape,
353         leadingContent = leadingContent,
354         trailingContent = trailingContent,
355         expandedShadowElevation = expandedShadowElevation,
356         collapsedShadowElevation = collapsedShadowElevation,
357         content = content
358     )
359 }
360 
361 /**
362  * A floating toolbar that displays vertically. The bar features its content within a [Column], and
363  * an adjacent floating icon button. It can be positioned anywhere on the screen, floating above
364  * other content, and its [expanded] flag controls the visibility of the actions with a slide
365  * animations.
366  *
367  * Note: This component will stay expanded to maintain the toolbar visibility for users with touch
368  * exploration services enabled (e.g., TalkBack).
369  *
370  * In case the toolbar is aligned to the top or the bottom of the screen, you may apply a
371  * [FloatingToolbarDefaults.floatingToolbarVerticalNestedScroll] `Modifier` to update the [expanded]
372  * state when scrolling occurs, as this sample shows:
373  *
374  * @sample androidx.compose.material3.samples.VerticalFloatingToolbarWithFabSample
375  *
376  * In case the toolbar is positioned along a center edge of the screen (like left or right center),
377  * it's recommended to maintain the expanded state on scroll and to attach a [scrollBehavior] in
378  * order to hide or show the entire component, as this sample shows:
379  *
380  * @sample androidx.compose.material3.samples.CenteredVerticalFloatingToolbarWithFabSample
381  * @param expanded whether the floating toolbar is expanded or not. In its expanded state, the FAB
382  *   and the toolbar content are organized vertically. Otherwise, only the FAB is visible. Note that
383  *   the toolbar will stay expanded in case a touch exploration service (e.g., TalkBack) is active.
384  * @param floatingActionButton a floating action button to be displayed by the toolbar. It's
385  *   recommended to use a [FloatingToolbarDefaults.VibrantFloatingActionButton] or
386  *   [FloatingToolbarDefaults.StandardFloatingActionButton] that is styled to match the [colors].
387  *   Note that the provided FAB's size is controlled by the floating toolbar and animates according
388  *   to its state. In case a custom FAB is provided, make sure it's set with a
389  *   [Modifier.fillMaxSize] to be sized correctly.
390  * @param modifier the [Modifier] to be applied to this floating toolbar.
391  * @param colors the colors used for this floating toolbar. There are two predefined
392  *   [FloatingToolbarColors] at [FloatingToolbarDefaults.standardFloatingToolbarColors] and
393  *   [FloatingToolbarDefaults.vibrantFloatingToolbarColors] which you can use or modify. See also
394  *   [floatingActionButton] for more information on the right FAB to use for proper styling.
395  * @param contentPadding the padding applied to the content of this floating toolbar.
396  * @param scrollBehavior a [FloatingToolbarScrollBehavior]. If provided, this FloatingToolbar will
397  *   automatically react to scrolling. If your toolbar is positioned along a center edge of the
398  *   screen (like left or right center), it's best to use this scroll behavior to make the entire
399  *   toolbar scroll off-screen as the user scrolls. This would prevent the FAB from appearing
400  *   off-center, which may occur in this case when using the [expanded] flag to simply expand or
401  *   collapse the toolbar. Note that the toolbar will not react to scrolling in case a touch
402  *   exploration service (e.g., TalkBack) is active.
403  * @param shape the shape used for this floating toolbar content.
404  * @param floatingActionButtonPosition the position of the floating toolbar's floating action
405  *   button. By default, the FAB is placed at the bottom of the toolbar (i.e. aligned to the
406  *   bottom).
407  * @param animationSpec the animation spec to use for this floating toolbar expand and collapse
408  *   animation.
409  * @param expandedShadowElevation the elevation for the shadow below this floating toolbar when
410  *   expanded.
411  * @param collapsedShadowElevation the elevation for the shadow below this floating toolbar when
412  *   collapsed.
413  * @param content the main content of this floating toolbar. The default layout here is a [Column],
414  *   so content inside will be placed vertically.
415  */
416 @ExperimentalMaterial3ExpressiveApi
417 @Composable
VerticalFloatingToolbarnull418 fun VerticalFloatingToolbar(
419     expanded: Boolean,
420     floatingActionButton: @Composable () -> Unit,
421     modifier: Modifier = Modifier,
422     colors: FloatingToolbarColors = FloatingToolbarDefaults.standardFloatingToolbarColors(),
423     contentPadding: PaddingValues = FloatingToolbarDefaults.ContentPadding,
424     scrollBehavior: FloatingToolbarScrollBehavior? = null,
425     shape: Shape = FloatingToolbarDefaults.ContainerShape,
426     floatingActionButtonPosition: FloatingToolbarVerticalFabPosition =
427         FloatingToolbarVerticalFabPosition.Bottom,
428     animationSpec: FiniteAnimationSpec<Float> = FloatingToolbarDefaults.animationSpec(),
429     expandedShadowElevation: Dp = FloatingToolbarDefaults.ContainerExpandedElevationWithFab,
430     collapsedShadowElevation: Dp = FloatingToolbarDefaults.ContainerCollapsedElevationWithFab,
431     content: @Composable ColumnScope.() -> Unit
432 ) {
433     val touchExplorationServiceEnabled by rememberTouchExplorationService()
434     var forceCollapse by rememberSaveable { mutableStateOf(false) }
435     VerticalFloatingToolbarWithFabLayout(
436         modifier = modifier,
437         expanded = !forceCollapse && (touchExplorationServiceEnabled || expanded),
438         onA11yForceCollapse = { force -> forceCollapse = force },
439         colors = colors,
440         toolbarToFabGap = FloatingToolbarDefaults.ToolbarToFabGap,
441         toolbarContentPadding = contentPadding,
442         scrollBehavior = if (!touchExplorationServiceEnabled) scrollBehavior else null,
443         toolbarShape = shape,
444         animationSpec = animationSpec,
445         fab = floatingActionButton,
446         fabPosition = floatingActionButtonPosition,
447         expandedShadowElevation = expandedShadowElevation,
448         collapsedShadowElevation = collapsedShadowElevation,
449         toolbar = content
450     )
451 }
452 
453 /**
454  * A FloatingToolbarScrollBehavior defines how a floating toolbar should behave when the content
455  * under it is scrolled.
456  *
457  * @see [FloatingToolbarDefaults.exitAlwaysScrollBehavior]
458  */
459 @ExperimentalMaterial3ExpressiveApi
460 @Stable
461 sealed interface FloatingToolbarScrollBehavior : NestedScrollConnection {
462 
463     /** Indicates the direction towards which the floating toolbar exits the screen. */
464     val exitDirection: FloatingToolbarExitDirection
465 
466     /**
467      * A [FloatingToolbarState] that is attached to this behavior and is read and updated when
468      * scrolling happens.
469      */
470     val state: FloatingToolbarState
471 
472     /**
473      * An [AnimationSpec] that defines how the floating toolbar snaps to either fully collapsed or
474      * fully extended state when a fling or a drag scrolled it into an intermediate position.
475      */
476     val snapAnimationSpec: AnimationSpec<Float>
477 
478     /**
479      * An [DecayAnimationSpec] that defines how to fling the floating toolbar when the user flings
480      * the toolbar itself, or the content below it.
481      */
482     val flingAnimationSpec: DecayAnimationSpec<Float>
483 
484     /** A [Modifier] that is attached to this behavior. */
floatingScrollBehaviornull485     fun Modifier.floatingScrollBehavior(): Modifier
486 }
487 
488 /**
489  * A [FloatingToolbarScrollBehavior] that adjusts its properties to affect the size of a floating
490  * toolbar.
491  *
492  * A floating toolbar that is set up with this [FloatingToolbarScrollBehavior] will immediately
493  * collapse when the nested content is pulled up, and will immediately appear when the content is
494  * pulled down.
495  *
496  * @param exitDirection indicates the direction towards which the floating toolbar exits the screen
497  * @param state a [FloatingToolbarState]
498  * @param snapAnimationSpec an [AnimationSpec] that defines how the floating toolbar snaps to either
499  *   fully collapsed or fully extended state when a fling or a drag scrolled it into an intermediate
500  *   position
501  * @param flingAnimationSpec an [DecayAnimationSpec] that defines how to fling the floating toolbar
502  *   when the user flings the toolbar itself, or the content below it
503  */
504 @ExperimentalMaterial3ExpressiveApi
505 class ExitAlwaysFloatingToolbarScrollBehavior(
506     override val exitDirection: FloatingToolbarExitDirection,
507     override val state: FloatingToolbarState,
508     override val snapAnimationSpec: AnimationSpec<Float>,
509     override val flingAnimationSpec: DecayAnimationSpec<Float>,
510 ) : FloatingToolbarScrollBehavior {
511 
512     override fun onPostScroll(
513         consumed: Offset,
514         available: Offset,
515         source: NestedScrollSource
516     ): Offset {
517         state.contentOffset += consumed.y
518         state.offset += consumed.y
519         return Offset.Zero
520     }
521 
522     override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
523         if (available.y > 0f && (state.offset == 0f || state.offset == state.offsetLimit)) {
524             // Reset the total content offset to zero when scrolling all the way down.
525             // This will eliminate some float precision inaccuracies.
526             state.contentOffset = 0f
527         }
528         val superConsumed = super.onPostFling(consumed, available)
529         return superConsumed +
530             settleFloatingToolbar(state, available.y, snapAnimationSpec, flingAnimationSpec)
531     }
532 
533     override fun Modifier.floatingScrollBehavior(): Modifier {
534         var isRtl = false
535         val orientation =
536             when (exitDirection) {
537                 Start,
538                 End -> Orientation.Horizontal
539                 else -> Orientation.Vertical
540             }
541         val draggableState = DraggableState { delta ->
542             val offset = if (exitDirection in listOf(Start, End) && isRtl) -delta else delta
543             when (exitDirection) {
544                 Start,
545                 Top -> state.offset += offset
546                 End,
547                 Bottom -> state.offset -= offset
548             }
549         }
550 
551         return this.layout { measurable, constraints ->
552                 isRtl = layoutDirection == LayoutDirection.Rtl
553 
554                 // Sets the toolbar's offset to collapse the entire bar's when content scrolled.
555                 val placeable = measurable.measure(constraints)
556                 val offset =
557                     if (exitDirection in listOf(Start, End) && isRtl) -state.offset
558                     else state.offset
559                 layout(placeable.width, placeable.height) {
560                     when (exitDirection) {
561                         Start -> placeable.placeWithLayer(offset.roundToInt(), 0)
562                         End -> placeable.placeWithLayer(-offset.roundToInt(), 0)
563                         Top -> placeable.placeWithLayer(0, offset.roundToInt())
564                         Bottom -> placeable.placeWithLayer(0, -offset.roundToInt())
565                     }
566                 }
567             }
568             .draggable(
569                 orientation = orientation,
570                 state = draggableState,
571                 onDragStopped = { velocity ->
572                     settleFloatingToolbar(state, velocity, snapAnimationSpec, flingAnimationSpec)
573                 }
574             )
575             .onGloballyPositioned { coordinates ->
576                 // Updates the toolbar's offsetLimit relative to the parent.
577                 val parentOffset = coordinates.positionInParent()
578                 val parentSize = coordinates.parentLayoutCoordinates?.size ?: IntSize.Zero
579                 val width = coordinates.size.width
580                 val height = coordinates.size.height
581                 val limit =
582                     when (exitDirection) {
583                         Start ->
584                             if (isRtl) parentSize.width - parentOffset.x else width + parentOffset.x
585                         End ->
586                             if (isRtl) width + parentOffset.x else parentSize.width - parentOffset.x
587                         Top -> height + parentOffset.y
588                         else -> parentSize.height - parentOffset.y
589                     }
590                 state.offsetLimit = -(limit - state.offset)
591             }
592     }
593 }
594 
595 // TODO tokens
596 /** Contains default values used for the floating toolbar implementations. */
597 @ExperimentalMaterial3ExpressiveApi
598 object FloatingToolbarDefaults {
599 
600     /** Default size used for [HorizontalFloatingToolbar] and [VerticalFloatingToolbar] container */
601     val ContainerSize: Dp = FloatingToolbarTokens.ContainerHeight
602 
603     /**
604      * Default expanded elevation used for [HorizontalFloatingToolbar] and [VerticalFloatingToolbar]
605      */
606     val ContainerExpandedElevation: Dp = ElevationTokens.Level0 // TODO read from token
607 
608     /**
609      * Default collapsed elevation used for [HorizontalFloatingToolbar] and
610      * [VerticalFloatingToolbar]
611      */
612     val ContainerCollapsedElevation: Dp = ElevationTokens.Level0 // TODO read from token
613 
614     /**
615      * Default expanded elevation used for [HorizontalFloatingToolbar] and [VerticalFloatingToolbar]
616      * with FAB.
617      */
618     val ContainerExpandedElevationWithFab: Dp = ElevationTokens.Level1 // TODO read from token
619 
620     /**
621      * Default collapsed elevation used for [HorizontalFloatingToolbar] and
622      * [VerticalFloatingToolbar] with FAB.
623      */
624     val ContainerCollapsedElevationWithFab: Dp = ElevationTokens.Level2 // TODO read from token
625 
626     /** Default shape used for [HorizontalFloatingToolbar] and [VerticalFloatingToolbar] */
627     val ContainerShape: Shape
628         @Composable get() = FloatingToolbarTokens.ContainerShape.value
629 
630     /**
631      * Default padding used for [HorizontalFloatingToolbar] and [VerticalFloatingToolbar] when
632      * content are default size (24dp) icons in [IconButton] that meet the minimum touch target
633      * (48.dp).
634      */
635     val ContentPadding =
636         PaddingValues(
637             start = FloatingToolbarTokens.ContainerLeadingSpace,
638             top = FloatingToolbarTokens.ContainerLeadingSpace,
639             end = FloatingToolbarTokens.ContainerTrailingSpace,
640             bottom = FloatingToolbarTokens.ContainerTrailingSpace
641         )
642 
643     /**
644      * Default offset from the edge of the screen used for [HorizontalFloatingToolbar] and
645      * [VerticalFloatingToolbar].
646      */
647     val ScreenOffset = FloatingToolbarTokens.ContainerExternalPadding
648 
649     /**
650      * Returns a default animation spec used for [HorizontalFloatingToolbar]s and
651      * [VerticalFloatingToolbar]s.
652      */
653     @Composable
animationSpecnull654     fun <T> animationSpec(): FiniteAnimationSpec<T> {
655         // TODO Load the motionScheme tokens from the component tokens file
656         return MotionSchemeKeyTokens.FastSpatial.value()
657     }
658 
659     // TODO: note that this scroll behavior may impact assistive technologies making the component
660     //  inaccessible.
661     //  See @sample androidx.compose.material3.samples.ScrollableHorizontalFloatingToolbar on how
662     //  to disable scrolling when touch exploration is enabled.
663     /**
664      * Returns a [FloatingToolbarScrollBehavior]. A floating toolbar that is set up with this
665      * [FloatingToolbarScrollBehavior] will immediately collapse when the content is pulled up, and
666      * will immediately appear when the content is pulled down.
667      *
668      * @param exitDirection indicates the direction towards which the floating toolbar exits the
669      *   screen
670      * @param state the state object to be used to control or observe the floating toolbar's scroll
671      *   state. See [rememberFloatingToolbarState] for a state that is remembered across
672      *   compositions.
673      * @param snapAnimationSpec an [AnimationSpec] that defines how the floating toolbar snaps to
674      *   either fully collapsed or fully extended state when a fling or a drag scrolled it into an
675      *   intermediate position
676      * @param flingAnimationSpec an [DecayAnimationSpec] that defines how to fling the floating app
677      *   bar when the user flings the toolbar itself, or the content below it
678      */
679     // TODO Load the motionScheme tokens from the component tokens file
680     @ExperimentalMaterial3ExpressiveApi
681     @Composable
exitAlwaysScrollBehaviornull682     fun exitAlwaysScrollBehavior(
683         exitDirection: FloatingToolbarExitDirection,
684         state: FloatingToolbarState = rememberFloatingToolbarState(),
685         snapAnimationSpec: AnimationSpec<Float> = MotionSchemeKeyTokens.DefaultEffects.value(),
686         flingAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay()
687     ): FloatingToolbarScrollBehavior =
688         remember(exitDirection, state, snapAnimationSpec, flingAnimationSpec) {
689             ExitAlwaysFloatingToolbarScrollBehavior(
690                 exitDirection = exitDirection,
691                 state = state,
692                 snapAnimationSpec = snapAnimationSpec,
693                 flingAnimationSpec = flingAnimationSpec
694             )
695         }
696 
697     /** Default enter transition used for [HorizontalFloatingToolbar] when expanding */
698     @Composable
horizontalEnterTransitionnull699     fun horizontalEnterTransition(expandFrom: Alignment.Horizontal) =
700         expandHorizontally(
701             animationSpec = animationSpec(),
702             expandFrom = expandFrom,
703         )
704 
705     /** Default enter transition used for [VerticalFloatingToolbar] when expanding */
706     @Composable
707     fun verticalEnterTransition(expandFrom: Alignment.Vertical) =
708         expandVertically(
709             animationSpec = animationSpec(),
710             expandFrom = expandFrom,
711         )
712 
713     /** Default exit transition used for [HorizontalFloatingToolbar] when shrinking */
714     @Composable
715     fun horizontalExitTransition(shrinkTowards: Alignment.Horizontal) =
716         shrinkHorizontally(
717             animationSpec = animationSpec(),
718             shrinkTowards = shrinkTowards,
719         )
720 
721     /** Default exit transition used for [VerticalFloatingToolbar] when shrinking */
722     @Composable
723     fun verticalExitTransition(shrinkTowards: Alignment.Vertical) =
724         shrinkVertically(
725             animationSpec = animationSpec(),
726             shrinkTowards = shrinkTowards,
727         )
728 
729     /**
730      * Creates a [FloatingToolbarColors] that represents the default standard colors used in the
731      * various floating toolbars.
732      */
733     @Composable
734     fun standardFloatingToolbarColors(): FloatingToolbarColors =
735         MaterialTheme.colorScheme.defaultFloatingToolbarStandardColors
736 
737     /**
738      * Creates a [FloatingToolbarColors] that represents the default vibrant colors used in the
739      * various floating toolbars.
740      */
741     @Composable
742     fun vibrantFloatingToolbarColors(): FloatingToolbarColors =
743         MaterialTheme.colorScheme.defaultFloatingToolbarVibrantColors
744 
745     /**
746      * Creates a [FloatingToolbarColors] that represents the default standard colors used in the
747      * various floating toolbars.
748      *
749      * @param toolbarContainerColor the container color for the floating toolbar.
750      * @param toolbarContentColor the content color for the floating toolbar.
751      * @param fabContainerColor the container color for an adjacent floating action button.
752      * @param fabContentColor the content color for an adjacent floating action button.
753      */
754     @Composable
755     fun standardFloatingToolbarColors(
756         toolbarContainerColor: Color = Color.Unspecified,
757         toolbarContentColor: Color = Color.Unspecified,
758         fabContainerColor: Color = Color.Unspecified,
759         fabContentColor: Color = Color.Unspecified
760     ): FloatingToolbarColors =
761         MaterialTheme.colorScheme.defaultFloatingToolbarStandardColors.copy(
762             toolbarContainerColor = toolbarContainerColor,
763             toolbarContentColor = toolbarContentColor,
764             fabContainerColor = fabContainerColor,
765             fabContentColor = fabContentColor
766         )
767 
768     /**
769      * Creates a [FloatingToolbarColors] that represents the default vibrant colors used in the
770      * various floating toolbars.
771      *
772      * @param toolbarContainerColor the container color for the floating toolbar.
773      * @param toolbarContentColor the content color for the floating toolbar.
774      * @param fabContainerColor the container color for an adjacent floating action button.
775      * @param fabContentColor the content color for an adjacent floating action button.
776      */
777     @Composable
778     fun vibrantFloatingToolbarColors(
779         toolbarContainerColor: Color = Color.Unspecified,
780         toolbarContentColor: Color = Color.Unspecified,
781         fabContainerColor: Color = Color.Unspecified,
782         fabContentColor: Color = Color.Unspecified
783     ): FloatingToolbarColors =
784         MaterialTheme.colorScheme.defaultFloatingToolbarVibrantColors.copy(
785             toolbarContainerColor = toolbarContainerColor,
786             toolbarContentColor = toolbarContentColor,
787             fabContainerColor = fabContainerColor,
788             fabContentColor = fabContentColor
789         )
790 
791     /**
792      * Creates a [FloatingActionButton] that represents a toolbar floating action button with
793      * vibrant colors.
794      *
795      * The FAB's elevation and size will be controlled by the floating toolbar, so it's applied with
796      * a [Modifier.fillMaxSize].
797      *
798      * @param onClick called when this FAB is clicked
799      * @param modifier the [Modifier] to be applied to this FAB
800      * @param shape defines the shape of this FAB's container and shadow
801      * @param containerColor the color used for the background of this FAB. Defaults to the
802      *   [FloatingToolbarColors.fabContainerColor] from the [vibrantFloatingToolbarColors].
803      * @param contentColor the preferred color for content inside this FAB. Defaults to the
804      *   [FloatingToolbarColors.fabContentColor] from the [vibrantFloatingToolbarColors].
805      * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
806      *   emitting [Interaction]s for this FAB. You can use this to change the FAB's appearance or
807      *   preview the FAB in different states. Note that if `null` is provided, interactions will
808      *   still happen internally.
809      * @param content the content of this FAB, typically an [Icon]
810      */
811     @Composable
812     fun VibrantFloatingActionButton(
813         onClick: () -> Unit,
814         modifier: Modifier = Modifier,
815         shape: Shape = FloatingActionButtonDefaults.shape,
816         containerColor: Color = vibrantFloatingToolbarColors().fabContainerColor,
817         contentColor: Color = vibrantFloatingToolbarColors().fabContentColor,
818         interactionSource: MutableInteractionSource? = null,
819         content: @Composable () -> Unit,
820     ) =
821         FloatingActionButton(
822             onClick = onClick,
823             modifier = modifier.fillMaxSize(),
824             shape = shape,
825             containerColor = containerColor,
826             contentColor = contentColor,
827             interactionSource = interactionSource,
828             content = content
829         )
830 
831     /**
832      * Creates a [FloatingActionButton] that represents a toolbar floating action button with
833      * standard colors.
834      *
835      * The FAB's elevation and size will be controlled by the floating toolbar, so it's applied with
836      * a [Modifier.fillMaxSize].
837      *
838      * @param onClick called when this FAB is clicked
839      * @param modifier the [Modifier] to be applied to this FAB
840      * @param shape defines the shape of this FAB's container and shadow
841      * @param containerColor the color used for the background of this FAB. Defaults to the
842      *   [FloatingToolbarColors.fabContainerColor] from the [standardFloatingToolbarColors].
843      * @param contentColor the preferred color for content inside this FAB. Defaults to the
844      *   [FloatingToolbarColors.fabContentColor] from the [standardFloatingToolbarColors].
845      * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
846      *   emitting [Interaction]s for this FAB. You can use this to change the FAB's appearance or
847      *   preview the FAB in different states. Note that if `null` is provided, interactions will
848      *   still happen internally.
849      * @param content the content of this FAB, typically an [Icon]
850      */
851     @Composable
852     fun StandardFloatingActionButton(
853         onClick: () -> Unit,
854         modifier: Modifier = Modifier,
855         shape: Shape = FloatingActionButtonDefaults.shape,
856         containerColor: Color = standardFloatingToolbarColors().fabContainerColor,
857         contentColor: Color = standardFloatingToolbarColors().fabContentColor,
858         interactionSource: MutableInteractionSource? = null,
859         content: @Composable () -> Unit,
860     ) =
861         FloatingActionButton(
862             onClick = onClick,
863             modifier = modifier.fillMaxSize(),
864             shape = shape,
865             containerColor = containerColor,
866             contentColor = contentColor,
867             interactionSource = interactionSource,
868             content = content
869         )
870 
871     /**
872      * This [Modifier] tracks vertical scroll events on the scrolling container that a floating
873      * toolbar appears above. It then calls [onExpand] and [onCollapse] to adjust the toolbar's
874      * state based on the scroll direction and distance.
875      *
876      * Essentially, it expands the toolbar when you scroll down past a certain threshold and
877      * collapses it when you scroll back up. You can customize the expand and collapse thresholds
878      * through the [expandScrollDistanceThreshold] and [collapseScrollDistanceThreshold].
879      *
880      * @param expanded the current expanded state of the floating toolbar
881      * @param onExpand callback to be invoked when the toolbar should expand
882      * @param onCollapse callback to be invoked when the toolbar should collapse
883      * @param expandScrollDistanceThreshold the scroll distance (in dp) required to trigger an
884      *   [onExpand]
885      * @param collapseScrollDistanceThreshold the scroll distance (in dp) required to trigger an
886      *   [onCollapse]
887      * @param reverseLayout indicates that the scrollable content has a reversed scrolling direction
888      */
889     fun Modifier.floatingToolbarVerticalNestedScroll(
890         expanded: Boolean,
891         onExpand: () -> Unit,
892         onCollapse: () -> Unit,
893         expandScrollDistanceThreshold: Dp = ScrollDistanceThreshold,
894         collapseScrollDistanceThreshold: Dp = ScrollDistanceThreshold,
895         reverseLayout: Boolean = false
896     ): Modifier =
897         this then
898             VerticalNestedScrollExpansionElement(
899                 expanded = expanded,
900                 onExpand = onExpand,
901                 onCollapse = onCollapse,
902                 reverseLayout = reverseLayout,
903                 expandScrollThreshold = expandScrollDistanceThreshold,
904                 collapseScrollThreshold = collapseScrollDistanceThreshold
905             )
906 
907     internal class VerticalNestedScrollExpansionElement(
908         val expanded: Boolean,
909         val onExpand: () -> Unit,
910         val onCollapse: () -> Unit,
911         val reverseLayout: Boolean,
912         val expandScrollThreshold: Dp,
913         val collapseScrollThreshold: Dp
914     ) : ModifierNodeElement<VerticalNestedScrollExpansionNode>() {
915         override fun create() =
916             VerticalNestedScrollExpansionNode(
917                 expanded = expanded,
918                 onExpand = onExpand,
919                 onCollapse = onCollapse,
920                 reverseLayout = reverseLayout,
921                 expandScrollThreshold = expandScrollThreshold,
922                 collapseScrollThreshold = collapseScrollThreshold
923             )
924 
925         override fun update(node: VerticalNestedScrollExpansionNode) {
926             node.updateNode(
927                 expanded,
928                 onExpand,
929                 onCollapse,
930                 reverseLayout,
931                 expandScrollThreshold,
932                 collapseScrollThreshold
933             )
934         }
935 
936         override fun InspectorInfo.inspectableProperties() {
937             name = "floatingToolbarVerticalNestedScroll"
938             properties["expanded"] = expanded
939             properties["expandScrollThreshold"] = expandScrollThreshold
940             properties["collapseScrollThreshold"] = collapseScrollThreshold
941             properties["reverseLayout"] = reverseLayout
942             properties["onExpand"] = onExpand
943             properties["onCollapse"] = onCollapse
944         }
945 
946         override fun equals(other: Any?): Boolean {
947             if (this === other) return true
948             if (other !is VerticalNestedScrollExpansionElement) return false
949 
950             if (expanded != other.expanded) return false
951             if (reverseLayout != other.reverseLayout) return false
952             if (onExpand !== other.onExpand) return false
953             if (onCollapse !== other.onCollapse) return false
954             if (expandScrollThreshold != other.expandScrollThreshold) return false
955             if (collapseScrollThreshold != other.collapseScrollThreshold) return false
956 
957             return true
958         }
959 
960         override fun hashCode(): Int {
961             var result = expanded.hashCode()
962             result = 31 * result + reverseLayout.hashCode()
963             result = 31 * result + onExpand.hashCode()
964             result = 31 * result + onCollapse.hashCode()
965             result = 31 * result + expandScrollThreshold.hashCode()
966             result = 31 * result + collapseScrollThreshold.hashCode()
967             return result
968         }
969     }
970 
971     internal class VerticalNestedScrollExpansionNode(
972         var expanded: Boolean,
973         var onExpand: () -> Unit,
974         var onCollapse: () -> Unit,
975         var reverseLayout: Boolean,
976         var expandScrollThreshold: Dp,
977         var collapseScrollThreshold: Dp,
978     ) : DelegatingNode(), CompositionLocalConsumerModifierNode, NestedScrollConnection {
979         private var expandScrollThresholdPx = 0f
980         private var collapseScrollThresholdPx = 0f
981         private var contentOffset = 0f
982         private var threshold = 0f
983 
984         // In reverse layouts, scrolling direction is flipped. We will use this factor to flip some
985         // of the values we read on the onPostScroll to ensure consistent behavior regardless of
986         // scroll direction.
987         private var reverseLayoutFactor = if (reverseLayout) -1 else 1
988 
989         override val shouldAutoInvalidate: Boolean
990             get() = false
991 
992         private var nestedScrollNode: DelegatableNode =
993             nestedScrollModifierNode(
994                 connection = this,
995                 dispatcher = null,
996             )
997 
onAttachnull998         override fun onAttach() {
999             delegate(nestedScrollNode)
1000             with(nestedScrollNode.requireDensity()) {
1001                 expandScrollThresholdPx = expandScrollThreshold.toPx()
1002                 collapseScrollThresholdPx = collapseScrollThreshold.toPx()
1003             }
1004             updateThreshold()
1005         }
1006 
onPostScrollnull1007         override fun onPostScroll(
1008             consumed: Offset,
1009             available: Offset,
1010             source: NestedScrollSource
1011         ): Offset {
1012             val scrollDelta = consumed.y * reverseLayoutFactor
1013             contentOffset += scrollDelta
1014 
1015             if (scrollDelta < 0 && contentOffset <= threshold) {
1016                 threshold = contentOffset + expandScrollThresholdPx
1017                 onCollapse()
1018             } else if (scrollDelta > 0 && contentOffset >= threshold) {
1019                 threshold = contentOffset - collapseScrollThresholdPx
1020                 onExpand()
1021             }
1022             return Offset.Zero
1023         }
1024 
updateNodenull1025         fun updateNode(
1026             expanded: Boolean,
1027             onExpand: () -> Unit,
1028             onCollapse: () -> Unit,
1029             reverseLayout: Boolean,
1030             expandScrollThreshold: Dp,
1031             collapseScrollThreshold: Dp
1032         ) {
1033             if (
1034                 this.expandScrollThreshold != expandScrollThreshold ||
1035                     this.collapseScrollThreshold != collapseScrollThreshold
1036             ) {
1037                 this.expandScrollThreshold = expandScrollThreshold
1038                 this.collapseScrollThreshold = collapseScrollThreshold
1039                 with(nestedScrollNode.requireDensity()) {
1040                     expandScrollThresholdPx = expandScrollThreshold.toPx()
1041                     collapseScrollThresholdPx = collapseScrollThreshold.toPx()
1042                 }
1043                 updateThreshold()
1044             }
1045             if (this.reverseLayout != reverseLayout) {
1046                 this.reverseLayout = reverseLayout
1047                 reverseLayoutFactor = if (this.reverseLayout) -1 else 1
1048             }
1049 
1050             this.onExpand = onExpand
1051             this.onCollapse = onCollapse
1052 
1053             if (this.expanded != expanded) {
1054                 this.expanded = expanded
1055                 updateThreshold()
1056             }
1057         }
1058 
updateThresholdnull1059         private fun updateThreshold() {
1060             threshold =
1061                 if (expanded) {
1062                     contentOffset - collapseScrollThresholdPx
1063                 } else {
1064                     contentOffset + expandScrollThresholdPx
1065                 }
1066         }
1067     }
1068 
1069     internal val ColorScheme.defaultFloatingToolbarStandardColors: FloatingToolbarColors
1070         get() {
1071             return defaultFloatingToolbarStandardColorsCached
1072                 ?: FloatingToolbarColors(
1073                         // TODO load colors from the toolbar tokens. If possible, remove the usage
1074                         //  of contentColorFor.
1075                         toolbarContainerColor =
1076                             fromToken(FloatingToolbarTokens.StandardContainerColor),
1077                         toolbarContentColor =
1078                             contentColorFor(
1079                                 fromToken(FloatingToolbarTokens.StandardContainerColor)
1080                             ),
1081                         fabContainerColor = fromToken(ColorSchemeKeyTokens.PrimaryContainer),
1082                         fabContentColor =
1083                             contentColorFor(fromToken(ColorSchemeKeyTokens.PrimaryContainer)),
1084                     )
<lambda>null1085                     .also { defaultFloatingToolbarStandardColorsCached = it }
1086         }
1087 
1088     internal val ColorScheme.defaultFloatingToolbarVibrantColors: FloatingToolbarColors
1089         get() {
1090             return defaultFloatingToolbarVibrantColorsCached
1091                 ?: FloatingToolbarColors(
1092                         // TODO load colors from the toolbar tokens. If possible, remove the usage
1093                         //  of contentColorFor.
1094                         toolbarContainerColor =
1095                             fromToken(FloatingToolbarTokens.VibrantContainerColor),
1096                         toolbarContentColor =
1097                             contentColorFor(fromToken(FloatingToolbarTokens.VibrantContainerColor)),
1098                         fabContainerColor = fromToken(ColorSchemeKeyTokens.TertiaryContainer),
1099                         fabContentColor =
1100                             contentColorFor(fromToken(ColorSchemeKeyTokens.TertiaryContainer)),
1101                     )
<lambda>null1102                     .also { defaultFloatingToolbarVibrantColorsCached = it }
1103         }
1104 
1105     /**
1106      * A default threshold in [Dp] for the content's scrolling that defines when the toolbar should
1107      * be collapsed or expanded.
1108      */
1109     val ScrollDistanceThreshold: Dp = 40.dp
1110 
1111     /**
1112      * Size range used for a FAB size in [HorizontalFloatingToolbar] and [VerticalFloatingToolbar].
1113      */
1114     internal val FabSizeRange = FabBaselineTokens.ContainerWidth..FabMediumTokens.ContainerWidth
1115 
1116     /**
1117      * Default gap between the [HorizontalFloatingToolbar] or [VerticalFloatingToolbar] toolbar
1118      * content and its adjacent FAB.
1119      */
1120     // TODO Load this from the component tokens?
1121     internal val ToolbarToFabGap = 8.dp
1122 }
1123 
1124 /**
1125  * Represents the container and content colors used in a the various floating toolbars.
1126  *
1127  * @param toolbarContainerColor the container color for the floating toolbar.
1128  * @param toolbarContentColor the content color for the floating toolbar
1129  * @param fabContainerColor the container color for an adjacent floating action button.
1130  * @param fabContentColor the content color for an adjacent floating action button
1131  */
1132 @ExperimentalMaterial3ExpressiveApi
1133 @Immutable
1134 class FloatingToolbarColors(
1135     val toolbarContainerColor: Color,
1136     val toolbarContentColor: Color,
1137     val fabContainerColor: Color,
1138     val fabContentColor: Color,
1139 ) {
1140 
1141     /**
1142      * Returns a copy of this IconToggleButtonColors, optionally overriding some of the values. This
1143      * uses the Color.Unspecified to mean “use the value from the source”
1144      */
copynull1145     fun copy(
1146         toolbarContainerColor: Color = this.toolbarContainerColor,
1147         toolbarContentColor: Color = this.toolbarContentColor,
1148         fabContainerColor: Color = this.fabContainerColor,
1149         fabContentColor: Color = this.fabContentColor,
1150     ) =
1151         FloatingToolbarColors(
1152             toolbarContainerColor.takeOrElse { this.toolbarContainerColor },
<lambda>null1153             toolbarContentColor.takeOrElse { this.toolbarContentColor },
<lambda>null1154             fabContainerColor.takeOrElse { this.fabContainerColor },
<lambda>null1155             fabContentColor.takeOrElse { this.fabContentColor },
1156         )
1157 
equalsnull1158     override fun equals(other: Any?): Boolean {
1159         if (this === other) return true
1160         if (other == null || other !is FloatingToolbarColors) return false
1161 
1162         if (toolbarContainerColor != other.toolbarContainerColor) return false
1163         if (toolbarContentColor != other.toolbarContentColor) return false
1164         if (fabContainerColor != other.fabContainerColor) return false
1165         if (fabContentColor != other.fabContentColor) return false
1166 
1167         return true
1168     }
1169 
hashCodenull1170     override fun hashCode(): Int {
1171         var result = toolbarContainerColor.hashCode()
1172         result = 31 * result + toolbarContentColor.hashCode()
1173         result = 31 * result + fabContainerColor.hashCode()
1174         result = 31 * result + fabContentColor.hashCode()
1175 
1176         return result
1177     }
1178 }
1179 
1180 /**
1181  * The possible positions for a [FloatingActionButton] attached to a [HorizontalFloatingToolbar]
1182  *
1183  * @see FloatingToolbarDefaults.StandardFloatingActionButton
1184  * @see FloatingToolbarDefaults.VibrantFloatingActionButton
1185  */
1186 @ExperimentalMaterial3ExpressiveApi
1187 @JvmInline
1188 value class FloatingToolbarHorizontalFabPosition
1189 internal constructor(@Suppress("unused") private val value: Int) {
1190     companion object {
1191         /** Position FAB at the start of the toolbar. */
1192         val Start = FloatingToolbarHorizontalFabPosition(0)
1193 
1194         /** Position FAB at the end of the toolbar. */
1195         val End = FloatingToolbarHorizontalFabPosition(1)
1196     }
1197 
toStringnull1198     override fun toString(): String {
1199         return when (this) {
1200             Start -> "FloatingToolbarHorizontalFabPosition.Start"
1201             else -> "FloatingToolbarHorizontalFabPosition.End"
1202         }
1203     }
1204 }
1205 
1206 /**
1207  * The possible positions for a [FloatingActionButton] attached to a [VerticalFloatingToolbar]
1208  *
1209  * @see FloatingToolbarDefaults.StandardFloatingActionButton
1210  * @see FloatingToolbarDefaults.VibrantFloatingActionButton
1211  */
1212 @ExperimentalMaterial3ExpressiveApi
1213 @JvmInline
1214 value class FloatingToolbarVerticalFabPosition
1215 internal constructor(@Suppress("unused") private val value: Int) {
1216     companion object {
1217         /** Position FAB at the top of the toolbar. */
1218         val Top = FloatingToolbarVerticalFabPosition(0)
1219 
1220         /** Position FAB at the bottom of the toolbar. */
1221         val Bottom = FloatingToolbarVerticalFabPosition(1)
1222     }
1223 
toStringnull1224     override fun toString(): String {
1225         return when (this) {
1226             Top -> "FloatingToolbarVerticalFabPosition.Top"
1227             else -> "FloatingToolbarVerticalFabPosition.Bottom"
1228         }
1229     }
1230 }
1231 
1232 /**
1233  * Creates a [FloatingToolbarState] that is remembered across compositions.
1234  *
1235  * @param initialOffsetLimit the initial value for [FloatingToolbarState.offsetLimit], which
1236  *   represents the pixel limit that a floating toolbar is allowed to collapse when the scrollable
1237  *   content is scrolled.
1238  * @param initialOffset the initial value for [FloatingToolbarState.offset]. The initial offset
1239  *   should be between zero and [initialOffsetLimit].
1240  * @param initialContentOffset the initial value for [FloatingToolbarState.contentOffset]
1241  */
1242 @ExperimentalMaterial3ExpressiveApi
1243 @Composable
rememberFloatingToolbarStatenull1244 fun rememberFloatingToolbarState(
1245     initialOffsetLimit: Float = -Float.MAX_VALUE,
1246     initialOffset: Float = 0f,
1247     initialContentOffset: Float = 0f
1248 ): FloatingToolbarState {
1249     return rememberSaveable(saver = FloatingToolbarState.Saver) {
1250         FloatingToolbarState(initialOffsetLimit, initialOffset, initialContentOffset)
1251     }
1252 }
1253 
1254 /**
1255  * A state object that can be hoisted to control and observe the floating toolbar state. The state
1256  * is read and updated by a [FloatingToolbarScrollBehavior] implementation.
1257  *
1258  * In most cases, this state will be created via [rememberFloatingToolbarState].
1259  */
1260 @ExperimentalMaterial3ExpressiveApi
1261 interface FloatingToolbarState {
1262 
1263     /**
1264      * The floating toolbar's offset limit in pixels, which represents the limit that a floating
1265      * toolbar is allowed to collapse to.
1266      *
1267      * Use this limit to coerce the [offset] value when it's updated.
1268      */
1269     var offsetLimit: Float
1270 
1271     /**
1272      * The floating toolbar's current offset in pixels. This offset is applied to the fixed size of
1273      * the toolbar to control the displayed size when content is being scrolled.
1274      *
1275      * Updates to the [offset] value are coerced between zero and [offsetLimit].
1276      */
1277     var offset: Float
1278 
1279     /**
1280      * The total offset of the content scrolled under the floating toolbar.
1281      *
1282      * This value is updated by a [FloatingToolbarScrollBehavior] whenever a nested scroll
1283      * connection consumes scroll events. A common implementation would update the value to be the
1284      * sum of all [NestedScrollConnection.onPostScroll] `consumed` values.
1285      */
1286     var contentOffset: Float
1287 
1288     companion object {
1289         /** The default [Saver] implementation for [FloatingToolbarState]. */
1290         internal val Saver: Saver<FloatingToolbarState, *> =
1291             listSaver(
<lambda>null1292                 save = { listOf(it.offsetLimit, it.offset, it.contentOffset) },
<lambda>null1293                 restore = {
1294                     FloatingToolbarState(
1295                         initialOffsetLimit = it[0],
1296                         initialOffset = it[1],
1297                         initialContentOffset = it[2]
1298                     )
1299                 }
1300             )
1301     }
1302 }
1303 
1304 /**
1305  * Creates a [FloatingToolbarState].
1306  *
1307  * @param initialOffsetLimit the initial value for [FloatingToolbarState.offsetLimit], which
1308  *   represents the pixel limit that a floating toolbar is allowed to collapse when the scrollable
1309  *   content is scrolled.
1310  * @param initialOffset the initial value for [FloatingToolbarState.offset]. The initial offset
1311  *   should be between zero and [initialOffsetLimit].
1312  * @param initialContentOffset the initial value for [FloatingToolbarState.contentOffset]
1313  */
1314 @ExperimentalMaterial3ExpressiveApi
FloatingToolbarStatenull1315 fun FloatingToolbarState(
1316     initialOffsetLimit: Float,
1317     initialOffset: Float,
1318     initialContentOffset: Float
1319 ): FloatingToolbarState =
1320     FloatingToolbarStateImpl(initialOffsetLimit, initialOffset, initialContentOffset)
1321 
1322 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
1323 @Stable
1324 private class FloatingToolbarStateImpl(
1325     initialOffsetLimit: Float,
1326     initialOffset: Float,
1327     initialContentOffset: Float
1328 ) : FloatingToolbarState {
1329 
1330     override var offsetLimit by mutableFloatStateOf(initialOffsetLimit)
1331 
1332     override var offset: Float
1333         get() = _offset.floatValue
1334         set(newOffset) {
1335             _offset.floatValue = newOffset.coerceIn(minimumValue = offsetLimit, maximumValue = 0f)
1336         }
1337 
1338     override var contentOffset by mutableFloatStateOf(initialContentOffset)
1339 
1340     private var _offset = mutableFloatStateOf(initialOffset)
1341 }
1342 
1343 /**
1344  * Settles the toolbar by flinging, in case the given velocity is greater than zero, and snapping
1345  * after the fling settles.
1346  */
1347 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
settleFloatingToolbarnull1348 private suspend fun settleFloatingToolbar(
1349     state: FloatingToolbarState,
1350     velocity: Float,
1351     snapAnimationSpec: AnimationSpec<Float>,
1352     flingAnimationSpec: DecayAnimationSpec<Float>
1353 ): Velocity {
1354     // Check if the toolbar is completely collapsed/expanded. If so, no need to settle the toolbar,
1355     // and just return Zero Velocity.
1356     // Note that we don't check for 0f due to float precision with the collapsedFraction
1357     // calculation.
1358     val collapsedFraction = state.collapsedFraction()
1359     if (collapsedFraction < 0.01f || collapsedFraction == 1f) {
1360         return Velocity.Zero
1361     }
1362     var remainingVelocity = velocity
1363     // In case there is an initial velocity that was left after a previous user fling, animate to
1364     // continue the motion to expand or collapse the toolbar.
1365     if (abs(velocity) > 1f) {
1366         var lastValue = 0f
1367         AnimationState(
1368                 initialValue = 0f,
1369                 initialVelocity = velocity,
1370             )
1371             .animateDecay(flingAnimationSpec) {
1372                 val delta = value - lastValue
1373                 val initialOffset = state.offset
1374                 state.offset = initialOffset + delta
1375                 val consumed = abs(initialOffset - state.offset)
1376                 lastValue = value
1377                 remainingVelocity = this.velocity
1378                 // avoid rounding errors and stop if anything is unconsumed
1379                 if (abs(delta - consumed) > 0.5f) this.cancelAnimation()
1380             }
1381     }
1382 
1383     if (state.offset < 0 && state.offset > state.offsetLimit) {
1384         AnimationState(initialValue = state.offset).animateTo(
1385             if (state.collapsedFraction() < 0.5f) {
1386                 0f
1387             } else {
1388                 state.offsetLimit
1389             },
1390             animationSpec = snapAnimationSpec
1391         ) {
1392             state.offset = value
1393         }
1394     }
1395 
1396     return Velocity(0f, remainingVelocity)
1397 }
1398 
1399 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
collapsedFractionnull1400 private fun FloatingToolbarState.collapsedFraction() =
1401     if (offsetLimit != 0f) {
1402         offset / offsetLimit
1403     } else {
1404         0f
1405     }
1406 
1407 /**
1408  * The possible directions for a [HorizontalFloatingToolbar] or [VerticalFloatingToolbar], used to
1409  * determine the exit direction when a [FloatingToolbarScrollBehavior] is attached.
1410  */
1411 @ExperimentalMaterial3ExpressiveApi
1412 @JvmInline
1413 value class FloatingToolbarExitDirection
1414 internal constructor(@Suppress("unused") private val value: Int) {
1415     companion object {
1416         /** FloatingToolbar exits towards the bottom of the screen */
1417         val Bottom = FloatingToolbarExitDirection(0)
1418 
1419         /** FloatingToolbar exits towards the top of the screen */
1420         val Top = FloatingToolbarExitDirection(1)
1421 
1422         /** FloatingToolbar exits towards the start of the screen */
1423         val Start = FloatingToolbarExitDirection(2)
1424 
1425         /** FloatingToolbar exits towards the end of the screen */
1426         val End = FloatingToolbarExitDirection(3)
1427     }
1428 
toStringnull1429     override fun toString(): String {
1430         return when (this) {
1431             Bottom -> "FloatingToolbarExitDirection.Bottom"
1432             Top -> "FloatingToolbarExitDirection.Top"
1433             Start -> "FloatingToolbarExitDirection.Start"
1434             else -> "FloatingToolbarExitDirection.End"
1435         }
1436     }
1437 }
1438 
1439 /** A layout for a horizontal floating toolbar. */
1440 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
1441 @Composable
HorizontalFloatingToolbarLayoutnull1442 private fun HorizontalFloatingToolbarLayout(
1443     modifier: Modifier,
1444     expanded: Boolean,
1445     onA11yForceCollapse: (Boolean) -> Unit,
1446     colors: FloatingToolbarColors,
1447     contentPadding: PaddingValues,
1448     scrollBehavior: FloatingToolbarScrollBehavior?,
1449     shape: Shape,
1450     leadingContent: @Composable (RowScope.() -> Unit)?,
1451     trailingContent: @Composable (RowScope.() -> Unit)?,
1452     expandedShadowElevation: Dp,
1453     collapsedShadowElevation: Dp,
1454     content: @Composable RowScope.() -> Unit
1455 ) {
1456     val expandToolbarActionLabel = getString(Strings.FloatingToolbarExpand)
1457     val collapseToolbarActionLabel = getString(Strings.FloatingToolbarCollapse)
1458     val expandedState by rememberUpdatedState(expanded)
1459     val shadowElevationState by
1460         animateDpAsState(
1461             if (expanded) expandedShadowElevation else collapsedShadowElevation,
1462             animationSpec = FloatingToolbarDefaults.animationSpec()
1463         )
1464     Row(
1465         modifier =
1466             modifier
1467                 .then(
1468                     scrollBehavior?.let { with(it) { Modifier.floatingScrollBehavior() } }
1469                         ?: Modifier
1470                 )
1471                 .graphicsLayer {
1472                     this.shadowElevation = shadowElevationState.toPx()
1473                     this.shape = shape
1474                     this.clip = true
1475                 }
1476                 .heightIn(min = FloatingToolbarDefaults.ContainerSize)
1477                 .background(color = colors.toolbarContainerColor, shape = shape)
1478                 .padding(contentPadding),
1479         horizontalArrangement = Arrangement.Center,
1480         verticalAlignment = Alignment.CenterVertically
1481     ) {
1482         CompositionLocalProvider(LocalContentColor provides colors.toolbarContentColor) {
1483             leadingContent?.let {
1484                 AnimatedVisibility(
1485                     visible = expandedState,
1486                     enter = horizontalEnterTransition(expandFrom = Alignment.Start),
1487                     exit = horizontalExitTransition(shrinkTowards = Alignment.End),
1488                 ) {
1489                     Row(content = it)
1490                 }
1491             }
1492             Row(
1493                 modifier =
1494                     Modifier.parentSemantics {
1495                             this.customActions =
1496                                 customToolbarActions(
1497                                     expanded = expandedState,
1498                                     expandAction = {
1499                                         onA11yForceCollapse(false)
1500                                         true
1501                                     },
1502                                     collapseAction = {
1503                                         onA11yForceCollapse(true)
1504                                         true
1505                                     },
1506                                     expandActionLabel = expandToolbarActionLabel,
1507                                     collapseActionLabel = collapseToolbarActionLabel
1508                                 )
1509                         }
1510                         .minimumInteractiveBalancedPadding(
1511                             hasVisibleLeadingContent = expanded && leadingContent != null,
1512                             hasVisibleTrailingContent = expanded && trailingContent != null,
1513                             // Ensures this motion will cause the padding to bounce.
1514                             animationSpec = MaterialTheme.motionScheme.defaultEffectsSpec()
1515                         ),
1516                 content = content
1517             )
1518             trailingContent?.let {
1519                 AnimatedVisibility(
1520                     visible = expandedState,
1521                     enter = horizontalEnterTransition(expandFrom = Alignment.End),
1522                     exit = horizontalExitTransition(shrinkTowards = Alignment.Start),
1523                 ) {
1524                     Row(content = it)
1525                 }
1526             }
1527         }
1528     }
1529 }
1530 
1531 /** A layout for a horizontal floating toolbar that has a FAB next to it. */
1532 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
1533 @Composable
HorizontalFloatingToolbarWithFabLayoutnull1534 private fun HorizontalFloatingToolbarWithFabLayout(
1535     modifier: Modifier,
1536     expanded: Boolean,
1537     onA11yForceCollapse: (Boolean) -> Unit,
1538     colors: FloatingToolbarColors,
1539     toolbarToFabGap: Dp,
1540     toolbarContentPadding: PaddingValues,
1541     scrollBehavior: FloatingToolbarScrollBehavior?,
1542     toolbarShape: Shape,
1543     animationSpec: FiniteAnimationSpec<Float>,
1544     fab: @Composable () -> Unit,
1545     fabPosition: FloatingToolbarHorizontalFabPosition,
1546     expandedShadowElevation: Dp,
1547     collapsedShadowElevation: Dp,
1548     toolbar: @Composable RowScope.() -> Unit
1549 ) {
1550     val fabShape = FloatingActionButtonDefaults.shape
1551     val expandTransition = updateTransition(if (expanded) 1f else 0f, label = "expanded state")
1552     val expandedProgress = expandTransition.animateFloat(transitionSpec = { animationSpec }) { it }
1553     val expandToolbarActionLabel = getString(Strings.FloatingToolbarExpand)
1554     val collapseToolbarActionLabel = getString(Strings.FloatingToolbarCollapse)
1555     val expandedState by rememberUpdatedState(expanded)
1556     Layout(
1557         {
1558             Row(
1559                 modifier =
1560                     Modifier.background(colors.toolbarContainerColor)
1561                         .padding(toolbarContentPadding)
1562                         .horizontalScroll(rememberScrollState()),
1563                 verticalAlignment = Alignment.CenterVertically
1564             ) {
1565                 CompositionLocalProvider(LocalContentColor provides colors.toolbarContentColor) {
1566                     toolbar()
1567                 }
1568             }
1569             Box(
1570                 modifier =
1571                     Modifier.parentSemantics {
1572                         this.customActions =
1573                             customToolbarActions(
1574                                 expanded = expandedState,
1575                                 expandAction = {
1576                                     onA11yForceCollapse(false)
1577                                     true
1578                                 },
1579                                 collapseAction = {
1580                                     onA11yForceCollapse(true)
1581                                     true
1582                                 },
1583                                 expandActionLabel = expandToolbarActionLabel,
1584                                 collapseActionLabel = collapseToolbarActionLabel
1585                             )
1586                     },
1587             ) {
1588                 fab()
1589             }
1590         },
1591         modifier =
1592             modifier
1593                 .defaultMinSize(minHeight = FloatingToolbarDefaults.FabSizeRange.endInclusive)
1594                 .then(
1595                     scrollBehavior?.let { with(it) { Modifier.floatingScrollBehavior() } }
1596                         ?: Modifier
1597                 )
1598     ) { measurables, constraints ->
1599         val toolbarMeasurable = measurables[0]
1600         val fabMeasurable = measurables[1]
1601 
1602         // The FAB is in its smallest size when the expanded progress is 1f.
1603         val fabSizeConstraint =
1604             FloatingToolbarDefaults.FabSizeRange.lerp(1f - expandedProgress.value).roundToPx()
1605         val fabPlaceable =
1606             fabMeasurable.measure(
1607                 constraints.copy(
1608                     minWidth = fabSizeConstraint,
1609                     maxWidth = fabSizeConstraint,
1610                     minHeight = fabSizeConstraint,
1611                     maxHeight = fabSizeConstraint
1612                 )
1613             )
1614 
1615         // Compute the toolbar's max intrinsic width. We will use it as a base to determine the
1616         // actual width with the animation progress and the total layout width.
1617         val maxToolbarPlaceableWidth =
1618             toolbarMeasurable.maxIntrinsicWidth(
1619                 height = FloatingToolbarDefaults.ContainerSize.roundToPx()
1620             )
1621         // Constraint the toolbar to the available width while taking into account the FAB width.
1622         val toolbarPlaceable =
1623             toolbarMeasurable.measure(
1624                 constraints.copy(
1625                     maxWidth =
1626                         (maxToolbarPlaceableWidth * expandedProgress.value)
1627                             .coerceAtLeast(0f)
1628                             .toInt(),
1629                     minHeight = FloatingToolbarDefaults.ContainerSize.roundToPx()
1630                 )
1631             )
1632 
1633         val width =
1634             maxToolbarPlaceableWidth +
1635                 toolbarToFabGap.roundToPx() +
1636                 FloatingToolbarDefaults.FabSizeRange.start.roundToPx()
1637         val height = constraints.minHeight
1638 
1639         val toolbarTopOffset = (height - toolbarPlaceable.height) / 2
1640         val fapTopOffset = (height - fabPlaceable.height) / 2
1641 
1642         val fabX =
1643             if (fabPosition == FloatingToolbarHorizontalFabPosition.End) {
1644                 width - fabPlaceable.width
1645             } else {
1646                 0
1647             }
1648         val toolbarX =
1649             if (fabPosition == FloatingToolbarHorizontalFabPosition.End) {
1650                 maxToolbarPlaceableWidth - toolbarPlaceable.width
1651             } else {
1652                 width - maxToolbarPlaceableWidth
1653             }
1654 
1655         layout(width, height) {
1656             toolbarPlaceable.placeRelativeWithLayer(x = toolbarX, y = toolbarTopOffset) {
1657                 shadowElevation = expandedShadowElevation.toPx()
1658                 shape = toolbarShape
1659                 clip = true
1660             }
1661             val fabElevation =
1662                 lerp(
1663                     expandedShadowElevation,
1664                     collapsedShadowElevation,
1665                     1f - expandedProgress.value.coerceAtMost(1f)
1666                 )
1667 
1668             fabPlaceable.placeRelativeWithLayer(x = fabX, y = fapTopOffset) {
1669                 shape = fabShape
1670                 shadowElevation = fabElevation.toPx()
1671                 clip = true
1672             }
1673         }
1674     }
1675 }
1676 
1677 /** A layout for a vertical floating toolbar. */
1678 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
1679 @Composable
VerticalFloatingToolbarLayoutnull1680 private fun VerticalFloatingToolbarLayout(
1681     modifier: Modifier,
1682     expanded: Boolean,
1683     onA11yForceCollapse: (Boolean) -> Unit,
1684     colors: FloatingToolbarColors,
1685     contentPadding: PaddingValues,
1686     scrollBehavior: FloatingToolbarScrollBehavior?,
1687     shape: Shape,
1688     leadingContent: @Composable (ColumnScope.() -> Unit)?,
1689     trailingContent: @Composable (ColumnScope.() -> Unit)?,
1690     expandedShadowElevation: Dp,
1691     collapsedShadowElevation: Dp,
1692     content: @Composable ColumnScope.() -> Unit
1693 ) {
1694     val expandToolbarActionLabel = getString(Strings.FloatingToolbarExpand)
1695     val collapseToolbarActionLabel = getString(Strings.FloatingToolbarCollapse)
1696     val expandedState by rememberUpdatedState(expanded)
1697     val shadowElevationState by
1698         animateDpAsState(
1699             if (expanded) expandedShadowElevation else collapsedShadowElevation,
1700             animationSpec = FloatingToolbarDefaults.animationSpec()
1701         )
1702 
1703     Column(
1704         modifier =
1705             modifier
1706                 .then(
1707                     scrollBehavior?.let { with(it) { Modifier.floatingScrollBehavior() } }
1708                         ?: Modifier
1709                 )
1710                 .graphicsLayer {
1711                     this.shadowElevation = shadowElevationState.toPx()
1712                     this.shape = shape
1713                     this.clip = true
1714                 }
1715                 .widthIn(min = FloatingToolbarDefaults.ContainerSize)
1716                 .background(color = colors.toolbarContainerColor, shape = shape)
1717                 .padding(contentPadding),
1718         verticalArrangement = Arrangement.Center,
1719         horizontalAlignment = Alignment.CenterHorizontally
1720     ) {
1721         CompositionLocalProvider(LocalContentColor provides colors.toolbarContentColor) {
1722             leadingContent?.let {
1723                 AnimatedVisibility(
1724                     visible = expandedState,
1725                     enter = verticalEnterTransition(expandFrom = Alignment.Bottom),
1726                     exit = verticalExitTransition(shrinkTowards = Alignment.Bottom),
1727                 ) {
1728                     Column(content = it)
1729                 }
1730             }
1731             Column(
1732                 modifier =
1733                     Modifier.parentSemantics {
1734                             this.customActions =
1735                                 customToolbarActions(
1736                                     expanded = expandedState,
1737                                     expandAction = {
1738                                         onA11yForceCollapse(false)
1739                                         true
1740                                     },
1741                                     collapseAction = {
1742                                         onA11yForceCollapse(true)
1743                                         true
1744                                     },
1745                                     expandActionLabel = expandToolbarActionLabel,
1746                                     collapseActionLabel = collapseToolbarActionLabel
1747                                 )
1748                         }
1749                         .minimumInteractiveBalancedPadding(
1750                             hasVisibleLeadingContent = expanded && leadingContent != null,
1751                             hasVisibleTrailingContent = expanded && trailingContent != null,
1752                             // Ensures this motion will cause the padding to bounce.
1753                             animationSpec = MaterialTheme.motionScheme.defaultEffectsSpec()
1754                         ),
1755                 content = content
1756             )
1757             trailingContent?.let {
1758                 AnimatedVisibility(
1759                     visible = expandedState,
1760                     enter = verticalEnterTransition(expandFrom = Alignment.Top),
1761                     exit = verticalExitTransition(shrinkTowards = Alignment.Top),
1762                 ) {
1763                     Column(content = it)
1764                 }
1765             }
1766         }
1767     }
1768 }
1769 
1770 /** A layout for a vertical floating toolbar that has a FAB above or below it. */
1771 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
1772 @Composable
VerticalFloatingToolbarWithFabLayoutnull1773 private fun VerticalFloatingToolbarWithFabLayout(
1774     modifier: Modifier,
1775     expanded: Boolean,
1776     onA11yForceCollapse: (Boolean) -> Unit,
1777     colors: FloatingToolbarColors,
1778     toolbarToFabGap: Dp,
1779     toolbarContentPadding: PaddingValues,
1780     scrollBehavior: FloatingToolbarScrollBehavior?,
1781     toolbarShape: Shape,
1782     animationSpec: FiniteAnimationSpec<Float>,
1783     fab: @Composable () -> Unit,
1784     fabPosition: FloatingToolbarVerticalFabPosition,
1785     expandedShadowElevation: Dp,
1786     collapsedShadowElevation: Dp,
1787     toolbar: @Composable ColumnScope.() -> Unit
1788 ) {
1789     val fabShape = FloatingActionButtonDefaults.shape
1790     val expandTransition = updateTransition(if (expanded) 1f else 0f, label = "expanded state")
1791     val expandedProgress = expandTransition.animateFloat(transitionSpec = { animationSpec }) { it }
1792     val expandToolbarActionLabel = getString(Strings.FloatingToolbarExpand)
1793     val collapseToolbarActionLabel = getString(Strings.FloatingToolbarCollapse)
1794     val expandedState by rememberUpdatedState(expanded)
1795     Layout(
1796         {
1797             Column(
1798                 modifier =
1799                     Modifier.background(colors.toolbarContainerColor)
1800                         .padding(toolbarContentPadding)
1801                         .verticalScroll(rememberScrollState()),
1802                 horizontalAlignment = Alignment.CenterHorizontally
1803             ) {
1804                 CompositionLocalProvider(LocalContentColor provides colors.toolbarContentColor) {
1805                     toolbar()
1806                 }
1807             }
1808             Box(
1809                 modifier =
1810                     Modifier.parentSemantics {
1811                         customActions =
1812                             customToolbarActions(
1813                                 expanded = expandedState,
1814                                 expandAction = {
1815                                     onA11yForceCollapse(false)
1816                                     true
1817                                 },
1818                                 collapseAction = {
1819                                     onA11yForceCollapse(true)
1820                                     true
1821                                 },
1822                                 expandActionLabel = expandToolbarActionLabel,
1823                                 collapseActionLabel = collapseToolbarActionLabel
1824                             )
1825                     },
1826             ) {
1827                 fab()
1828             }
1829         },
1830         modifier =
1831             modifier
1832                 .defaultMinSize(minWidth = FloatingToolbarDefaults.FabSizeRange.endInclusive)
1833                 .then(
1834                     scrollBehavior?.let { with(it) { Modifier.floatingScrollBehavior() } }
1835                         ?: Modifier
1836                 )
1837     ) { measurables, constraints ->
1838         val toolbarMeasurable = measurables[0]
1839         val fabMeasurable = measurables[1]
1840 
1841         // The FAB is in its smallest size when the expanded progress is 1f.
1842         val fabSizeConstraint =
1843             FloatingToolbarDefaults.FabSizeRange.lerp(1f - expandedProgress.value).roundToPx()
1844         val fabPlaceable =
1845             fabMeasurable.measure(
1846                 constraints.copy(
1847                     minWidth = fabSizeConstraint,
1848                     maxWidth = fabSizeConstraint,
1849                     minHeight = fabSizeConstraint,
1850                     maxHeight = fabSizeConstraint
1851                 )
1852             )
1853         // Compute the toolbar's max intrinsic height. We will use it as a base to determine the
1854         // actual height with the animation progress and the total layout height.
1855         val maxToolbarPlaceableHeight =
1856             toolbarMeasurable.maxIntrinsicHeight(
1857                 width = FloatingToolbarDefaults.ContainerSize.roundToPx()
1858             )
1859         // Constraint the toolbar to the available height while taking into account the FAB height.
1860         val toolbarPlaceable =
1861             toolbarMeasurable.measure(
1862                 constraints.copy(
1863                     maxHeight =
1864                         (maxToolbarPlaceableHeight * expandedProgress.value)
1865                             .coerceAtLeast(0f)
1866                             .toInt(),
1867                     minWidth = FloatingToolbarDefaults.ContainerSize.roundToPx()
1868                 )
1869             )
1870 
1871         val width = constraints.minWidth
1872         val height =
1873             maxToolbarPlaceableHeight +
1874                 toolbarToFabGap.roundToPx() +
1875                 FloatingToolbarDefaults.FabSizeRange.start.roundToPx()
1876 
1877         val toolbarEdgeOffset = (width - toolbarPlaceable.width) / 2
1878         val fapEdgeOffset = (width - fabPlaceable.width) / 2
1879 
1880         val fabY =
1881             if (fabPosition == FloatingToolbarVerticalFabPosition.Bottom) {
1882                 height - fabPlaceable.height
1883             } else {
1884                 0
1885             }
1886         val toolbarY =
1887             if (fabPosition == FloatingToolbarVerticalFabPosition.Bottom) {
1888                 maxToolbarPlaceableHeight - toolbarPlaceable.height
1889             } else {
1890                 height - maxToolbarPlaceableHeight
1891             }
1892 
1893         layout(width, height) {
1894             toolbarPlaceable.placeRelativeWithLayer(
1895                 x = toolbarEdgeOffset,
1896                 y = toolbarY,
1897             ) {
1898                 shadowElevation = expandedShadowElevation.toPx()
1899                 shape = toolbarShape
1900                 clip = true
1901             }
1902             val fabElevation =
1903                 lerp(
1904                     expandedShadowElevation,
1905                     collapsedShadowElevation,
1906                     1f - expandedProgress.value.coerceAtMost(1f)
1907                 )
1908             fabPlaceable.placeRelativeWithLayer(x = fapEdgeOffset, y = fabY) {
1909                 shape = fabShape
1910                 shadowElevation = fabElevation.toPx()
1911                 clip = true
1912             }
1913         }
1914     }
1915 }
1916 
1917 /**
1918  * A [Modifier] that adds padding to visually balance the layout of a clickable component that was
1919  * modified by a [minimumInteractiveComponentSize] modifier. It ensures consistent padding across
1920  * both axes, particularly when leading or trailing content is hidden.
1921  *
1922  * The Modifier reads the [AlignmentLine] values generated by [minimumInteractiveComponentSize] to
1923  * determine the necessary padding adjustments. These adjustments are animated to provide a smooth
1924  * transition when content visibility changes.
1925  *
1926  * Note that this modifier should be applied *after* a `minimumInteractiveComponentSize` in the
1927  * modifier chain.
1928  *
1929  * @param hasVisibleLeadingContent whether the leading content is visible.
1930  * @param hasVisibleTrailingContent whether the trailing content is visible.
1931  * @param animationSpec the [AnimationSpec] used to animate the padding.
1932  */
minimumInteractiveBalancedPaddingnull1933 private fun Modifier.minimumInteractiveBalancedPadding(
1934     hasVisibleLeadingContent: Boolean,
1935     hasVisibleTrailingContent: Boolean,
1936     animationSpec: AnimationSpec<Float>
1937 ): Modifier =
1938     this then
1939         MinimumInteractiveBalancedPaddingElement(
1940             hasVisibleLeadingContent,
1941             hasVisibleTrailingContent,
1942             animationSpec
1943         )
1944 
1945 private data class MinimumInteractiveBalancedPaddingElement(
1946     val hasVisibleLeadingContent: Boolean,
1947     val hasVisibleTrailingContent: Boolean,
1948     val animationSpec: AnimationSpec<Float>
1949 ) : ModifierNodeElement<MinimumInteractiveBalancedPaddingNode>() {
1950 
1951     override fun create(): MinimumInteractiveBalancedPaddingNode =
1952         MinimumInteractiveBalancedPaddingNode(
1953             hasVisibleLeadingContent,
1954             hasVisibleTrailingContent,
1955             animationSpec
1956         )
1957 
1958     override fun update(node: MinimumInteractiveBalancedPaddingNode) {
1959         node.hasVisibleLeadingContent = hasVisibleLeadingContent
1960         node.hasVisibleTrailingContent = hasVisibleTrailingContent
1961         node.animationSpec = animationSpec
1962         node.updateAnimation()
1963     }
1964 
1965     override fun InspectorInfo.inspectableProperties() {
1966         name = "minimumInteractiveBalancedPadding"
1967         properties["hasVisibleLeadingContent"] = hasVisibleLeadingContent
1968         properties["hasVisibleTrailingContent"] = hasVisibleTrailingContent
1969         properties["animationSpec"] = animationSpec
1970     }
1971 }
1972 
1973 private class MinimumInteractiveBalancedPaddingNode(
1974     var hasVisibleLeadingContent: Boolean,
1975     var hasVisibleTrailingContent: Boolean,
1976     var animationSpec: AnimationSpec<Float>
1977 ) : Modifier.Node(), LayoutModifierNode {
1978 
1979     private var paddingAnimation: Animatable<Float, AnimationVector1D> =
1980         Animatable(if (hasVisibleLeadingContent || hasVisibleTrailingContent) 0f else 1f)
1981 
measurenull1982     override fun MeasureScope.measure(
1983         measurable: Measurable,
1984         constraints: Constraints
1985     ): MeasureResult {
1986         val placeable = measurable.measure(constraints)
1987         var verticalAlignmentOffset = 0f
1988         var horizontalAlignmentOffset = 0f
1989 
1990         // Resolve the top and left paddings from the alignment lines whenever either of the leading
1991         // or trailing content is missing.
1992         if (!hasVisibleLeadingContent || !hasVisibleTrailingContent) {
1993             val progress = paddingAnimation.value
1994             verticalAlignmentOffset =
1995                 placeable[MinimumInteractiveTopAlignmentLine].let {
1996                     if (it != AlignmentLine.Unspecified) (it * progress) else 0f
1997                 }
1998             horizontalAlignmentOffset =
1999                 placeable[MinimumInteractiveLeftAlignmentLine].let {
2000                     if (it != AlignmentLine.Unspecified) (it * progress) else 0f
2001                 }
2002         }
2003         // Add padding to balance the alignment by ensuring that the horizontal and vertical
2004         // alignment offsets are visually similar.
2005         // In case the vertical alignment offset is greater than the horizontal alignment
2006         // offset, we add additional horizontal padding to balance the paddings.
2007         val totalWidth =
2008             placeable.width +
2009                 ((verticalAlignmentOffset - horizontalAlignmentOffset) * 2)
2010                     .coerceAtLeast(0f)
2011                     .fastRoundToInt()
2012         // In case the horizontal alignment offset is greater than the vertical alignment
2013         // offset, we add additional vertical padding to balance the paddings.
2014         val totalHeight =
2015             placeable.height +
2016                 ((horizontalAlignmentOffset - verticalAlignmentOffset) * 2)
2017                     .coerceAtLeast(0f)
2018                     .fastRoundToInt()
2019 
2020         return layout(width = totalWidth, height = totalHeight) {
2021             placeable.place(
2022                 (totalWidth - placeable.width) / 2,
2023                 (totalHeight - placeable.height) / 2
2024             )
2025         }
2026     }
2027 
updateAnimationnull2028     fun updateAnimation() {
2029         coroutineScope.launch {
2030             if (!(hasVisibleLeadingContent || hasVisibleTrailingContent)) {
2031                 paddingAnimation.animateTo(1f, animationSpec)
2032             } else {
2033                 paddingAnimation.animateTo(0f, animationSpec)
2034             }
2035         }
2036     }
2037 }
2038 
2039 /** Creates a list of custom accessibility actions for a toolbar. */
customToolbarActionsnull2040 private fun customToolbarActions(
2041     expanded: Boolean,
2042     expandAction: () -> Boolean,
2043     collapseAction: () -> Boolean,
2044     expandActionLabel: String,
2045     collapseActionLabel: String,
2046 ): List<CustomAccessibilityAction> {
2047     return listOf(
2048         if (expanded) {
2049             CustomAccessibilityAction(label = collapseActionLabel, action = collapseAction)
2050         } else {
2051             CustomAccessibilityAction(label = expandActionLabel, action = expandAction)
2052         }
2053     )
2054 }
2055 
ClosedRangenull2056 private fun ClosedRange<Dp>.lerp(progress: Float): Dp = lerp(start, endInclusive, progress)
2057 
2058 /** Returns the current accessibility touch exploration service [State]. */
2059 @Composable
2060 private fun rememberTouchExplorationService(): State<Boolean> =
2061     rememberAccessibilityServiceState(
2062         listenToTouchExplorationState = true,
2063         listenToSwitchAccessState = false,
2064         listenToVoiceAccessState = false
2065     )
2066