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