1 /*
<lambda>null2 * Copyright 2022 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 com.android.settingslib.spa.widget.scaffold
18
19 import androidx.compose.animation.core.AnimationSpec
20 import androidx.compose.animation.core.AnimationState
21 import androidx.compose.animation.core.CubicBezierEasing
22 import androidx.compose.animation.core.DecayAnimationSpec
23 import androidx.compose.animation.core.FastOutLinearInEasing
24 import androidx.compose.animation.core.animateDecay
25 import androidx.compose.animation.core.animateTo
26 import androidx.compose.foundation.gestures.Orientation
27 import androidx.compose.foundation.gestures.draggable
28 import androidx.compose.foundation.gestures.rememberDraggableState
29 import androidx.compose.foundation.layout.Arrangement
30 import androidx.compose.foundation.layout.Box
31 import androidx.compose.foundation.layout.Column
32 import androidx.compose.foundation.layout.Row
33 import androidx.compose.foundation.layout.RowScope
34 import androidx.compose.foundation.layout.WindowInsets
35 import androidx.compose.foundation.layout.WindowInsetsSides
36 import androidx.compose.foundation.layout.only
37 import androidx.compose.foundation.layout.padding
38 import androidx.compose.foundation.layout.safeDrawing
39 import androidx.compose.foundation.layout.windowInsetsPadding
40 import androidx.compose.material3.ExperimentalMaterial3Api
41 import androidx.compose.material3.LocalContentColor
42 import androidx.compose.material3.MaterialTheme
43 import androidx.compose.material3.ProvideTextStyle
44 import androidx.compose.material3.Text
45 import androidx.compose.material3.TopAppBarScrollBehavior
46 import androidx.compose.material3.TopAppBarState
47 import androidx.compose.runtime.Composable
48 import androidx.compose.runtime.CompositionLocalProvider
49 import androidx.compose.runtime.LaunchedEffect
50 import androidx.compose.runtime.NonRestartableComposable
51 import androidx.compose.runtime.Stable
52 import androidx.compose.runtime.derivedStateOf
53 import androidx.compose.runtime.getValue
54 import androidx.compose.runtime.mutableFloatStateOf
55 import androidx.compose.runtime.remember
56 import androidx.compose.ui.Alignment
57 import androidx.compose.ui.Modifier
58 import androidx.compose.ui.draw.clipToBounds
59 import androidx.compose.ui.draw.drawBehind
60 import androidx.compose.ui.graphics.Color
61 import androidx.compose.ui.graphics.graphicsLayer
62 import androidx.compose.ui.graphics.lerp
63 import androidx.compose.ui.input.pointer.pointerInput
64 import androidx.compose.ui.layout.AlignmentLine
65 import androidx.compose.ui.layout.LastBaseline
66 import androidx.compose.ui.layout.Layout
67 import androidx.compose.ui.layout.layoutId
68 import androidx.compose.ui.layout.onGloballyPositioned
69 import androidx.compose.ui.platform.LocalDensity
70 import androidx.compose.ui.semantics.clearAndSetSemantics
71 import androidx.compose.ui.semantics.heading
72 import androidx.compose.ui.semantics.isTraversalGroup
73 import androidx.compose.ui.semantics.semantics
74 import androidx.compose.ui.text.TextStyle
75 import androidx.compose.ui.text.style.TextOverflow
76 import androidx.compose.ui.unit.Constraints
77 import androidx.compose.ui.unit.Density
78 import androidx.compose.ui.unit.Dp
79 import androidx.compose.ui.unit.Velocity
80 import androidx.compose.ui.unit.dp
81 import com.android.settingslib.spa.framework.theme.SettingsDimension
82 import com.android.settingslib.spa.framework.theme.isSpaExpressiveEnabled
83 import com.android.settingslib.spa.framework.theme.settingsBackground
84 import com.android.settingslib.spa.framework.theme.toSemiBoldWeight
85 import kotlin.math.abs
86 import kotlin.math.max
87 import kotlin.math.roundToInt
88
89 private val safeDrawingWindowInsets: WindowInsets
90 @Composable
91 @NonRestartableComposable
92 get() = WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top)
93
94 @Composable
95 internal fun CustomizedTopAppBar(
96 title: @Composable () -> Unit,
97 navigationIcon: @Composable () -> Unit = {},
<lambda>null98 actions: @Composable RowScope.() -> Unit = {},
99 ) {
100 SingleRowTopAppBar(
101 title = title,
102 titleTextStyle = MaterialTheme.typography.titleMedium,
103 navigationIcon = navigationIcon,
104 actions = actions,
105 windowInsets = safeDrawingWindowInsets,
106 colors = topAppBarColors(),
107 )
108 }
109
110 /** The customized LargeTopAppBar for Settings. */
111 @OptIn(ExperimentalMaterial3Api::class)
112 @Composable
CustomizedLargeTopAppBarnull113 internal fun CustomizedLargeTopAppBar(
114 title: String,
115 modifier: Modifier = Modifier,
116 navigationIcon: @Composable () -> Unit = {},
<lambda>null117 actions: @Composable RowScope.() -> Unit = {},
118 scrollBehavior: TopAppBarScrollBehavior? = null,
119 ) {
120 TwoRowsTopAppBar(
<lambda>null121 title = { Title(title = title, maxLines = 3) },
122 titleTextStyle =
123 if (isSpaExpressiveEnabled) MaterialTheme.typography.displaySmall.toSemiBoldWeight()
124 else MaterialTheme.typography.displaySmall,
125 smallTitleTextStyle =
126 if (isSpaExpressiveEnabled) MaterialTheme.typography.titleLarge.toSemiBoldWeight()
127 else MaterialTheme.typography.titleLarge,
128 titleBottomPadding = LargeTitleBottomPadding,
<lambda>null129 smallTitle = { Title(title = title, maxLines = 1) },
130 modifier = modifier,
131 navigationIcon = navigationIcon,
132 actions = actions,
133 colors = topAppBarColors(),
134 windowInsets = safeDrawingWindowInsets,
135 pinnedHeight = ContainerHeight,
136 scrollBehavior = scrollBehavior,
137 )
138 }
139
140 @Composable
Titlenull141 private fun Title(title: String, maxLines: Int = Int.MAX_VALUE) {
142 Text(
143 text = title,
144 modifier =
145 Modifier
146 .padding(
147 start =
148 if (isSpaExpressiveEnabled) SettingsDimension.paddingExtraSmall
149 else SettingsDimension.itemPaddingAround,
150 end = SettingsDimension.itemPaddingEnd,
151 )
152 .semantics { heading() },
153 overflow = TextOverflow.Ellipsis,
154 maxLines = maxLines,
155 )
156 }
157
158 @Composable
topAppBarColorsnull159 private fun topAppBarColors() =
160 if (isSpaExpressiveEnabled)
161 TopAppBarColors(
162 containerColor = MaterialTheme.colorScheme.surfaceContainer,
163 scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer,
164 navigationIconContentColor = MaterialTheme.colorScheme.onSurface,
165 titleContentColor = MaterialTheme.colorScheme.onSurface,
166 actionIconContentColor = MaterialTheme.colorScheme.primary,
167 )
168 else
169 TopAppBarColors(
170 containerColor = MaterialTheme.colorScheme.settingsBackground,
171 scrolledContainerColor = MaterialTheme.colorScheme.surfaceVariant,
172 navigationIconContentColor = MaterialTheme.colorScheme.onSurface,
173 titleContentColor = MaterialTheme.colorScheme.onSurface,
174 actionIconContentColor = MaterialTheme.colorScheme.onSurfaceVariant,
175 )
176
177 /**
178 * Represents the colors used by a top app bar in different states.
179 *
180 * This implementation animates the container color according to the top app bar scroll state. It
181 * does not animate the leading, headline, or trailing colors.
182 *
183 * @param containerColor the color used for the background of this BottomAppBar. Use
184 * [Color.Transparent] to have no color.
185 * @param scrolledContainerColor the container color when content is scrolled behind it
186 * @param navigationIconContentColor the content color used for the navigation icon
187 * @param titleContentColor the content color used for the title
188 * @param actionIconContentColor the content color used for actions
189 * @constructor create an instance with arbitrary colors, see [TopAppBarColors] for a factory method
190 * using the default material3 spec
191 */
192 @Stable
193 private class TopAppBarColors(
194 val containerColor: Color,
195 val scrolledContainerColor: Color,
196 val navigationIconContentColor: Color,
197 val titleContentColor: Color,
198 val actionIconContentColor: Color,
199 ) {
200
201 /**
202 * Represents the container color used for the top app bar.
203 *
204 * A [colorTransitionFraction] provides a percentage value that can be used to generate a color.
205 * Usually, an app bar implementation will pass in a [colorTransitionFraction] read from the
206 * [TopAppBarState.collapsedFraction] or the [TopAppBarState.overlappedFraction].
207 *
208 * @param colorTransitionFraction a `0.0` to `1.0` value that represents a color transition
209 * percentage
210 */
211 @Stable
212 fun containerColor(colorTransitionFraction: Float): Color {
213 return lerp(
214 containerColor,
215 scrolledContainerColor,
216 FastOutLinearInEasing.transform(colorTransitionFraction),
217 )
218 }
219
220 override fun equals(other: Any?): Boolean {
221 if (this === other) return true
222 if (other == null || other !is TopAppBarColors) return false
223
224 if (containerColor != other.containerColor) return false
225 if (scrolledContainerColor != other.scrolledContainerColor) return false
226 if (navigationIconContentColor != other.navigationIconContentColor) return false
227 if (titleContentColor != other.titleContentColor) return false
228 if (actionIconContentColor != other.actionIconContentColor) return false
229
230 return true
231 }
232
233 override fun hashCode(): Int {
234 var result = containerColor.hashCode()
235 result = 31 * result + scrolledContainerColor.hashCode()
236 result = 31 * result + navigationIconContentColor.hashCode()
237 result = 31 * result + titleContentColor.hashCode()
238 result = 31 * result + actionIconContentColor.hashCode()
239
240 return result
241 }
242 }
243
244 /**
245 * A single-row top app bar that is designed to be called by the small and center aligned top app
246 * bar composables.
247 */
248 @Composable
SingleRowTopAppBarnull249 private fun SingleRowTopAppBar(
250 title: @Composable () -> Unit,
251 titleTextStyle: TextStyle,
252 navigationIcon: @Composable () -> Unit,
253 actions: @Composable (RowScope.() -> Unit),
254 windowInsets: WindowInsets,
255 colors: TopAppBarColors,
256 ) {
257 // Wrap the given actions in a Row.
258 val actionsRow =
259 @Composable {
260 Row(
261 horizontalArrangement = Arrangement.End,
262 verticalAlignment = Alignment.CenterVertically,
263 content = actions,
264 )
265 }
266
267 // Compose a Surface with a TopAppBarLayout content.
268 Box(
269 modifier =
270 Modifier
271 .drawBehind { drawRect(color = colors.scrolledContainerColor) }
272 .semantics { isTraversalGroup = true }
273 .pointerInput(Unit) {}
274 ) {
275 val height = LocalDensity.current.run { ContainerHeight.toPx() }
276 TopAppBarLayout(
277 modifier =
278 Modifier
279 .windowInsetsPadding(windowInsets)
280 // clip after padding so we don't show the title over the inset area
281 .clipToBounds(),
282 heightPx = height,
283 navigationIconContentColor = colors.navigationIconContentColor,
284 titleContentColor = colors.titleContentColor,
285 actionIconContentColor = colors.actionIconContentColor,
286 title = title,
287 titleTextStyle = titleTextStyle,
288 titleAlpha = { 1f },
289 titleVerticalArrangement = Arrangement.Center,
290 titleBottomPadding = 0,
291 hideTitleSemantics = false,
292 navigationIcon = navigationIcon,
293 actions = actionsRow,
294 titleScaleDisabled = false,
295 )
296 }
297 }
298
299 /**
300 * A two-rows top app bar that is designed to be called by the Large and Medium top app bar
301 * composables.
302 *
303 * @throws [IllegalArgumentException] if the given [MaxHeightWithoutTitle] is equal or smaller than
304 * the [pinnedHeight]
305 */
306 @OptIn(ExperimentalMaterial3Api::class)
307 @Composable
TwoRowsTopAppBarnull308 private fun TwoRowsTopAppBar(
309 modifier: Modifier = Modifier,
310 title: @Composable () -> Unit,
311 titleTextStyle: TextStyle,
312 titleBottomPadding: Dp,
313 smallTitle: @Composable () -> Unit,
314 smallTitleTextStyle: TextStyle,
315 navigationIcon: @Composable () -> Unit,
316 actions: @Composable RowScope.() -> Unit,
317 windowInsets: WindowInsets,
318 colors: TopAppBarColors,
319 pinnedHeight: Dp,
320 scrollBehavior: TopAppBarScrollBehavior?,
321 ) {
322 if (MaxHeightWithoutTitle <= pinnedHeight) {
323 throw IllegalArgumentException(
324 "A TwoRowsTopAppBar max height should be greater than its pinned height"
325 )
326 }
327 val pinnedHeightPx: Float
328 val titleBottomPaddingPx: Int
329 val defaultMaxHeightPx: Float
330 val density = LocalDensity.current
331 density.run {
332 pinnedHeightPx = pinnedHeight.toPx()
333 titleBottomPaddingPx = titleBottomPadding.roundToPx()
334 defaultMaxHeightPx = (MaxHeightWithoutTitle + DefaultTitleHeight).toPx()
335 }
336
337 val maxHeightPx = remember(density) { mutableFloatStateOf(defaultMaxHeightPx) }
338
339 // Sets the app bar's height offset limit to hide just the bottom title area and keep top title
340 // visible when collapsed.
341 scrollBehavior?.state?.heightOffsetLimit = pinnedHeightPx - maxHeightPx.floatValue
342 if (isSpaExpressiveEnabled) {
343 LaunchedEffect(scrollBehavior?.state?.heightOffsetLimit) { scrollBehavior?.collapse() }
344 }
345
346 // Obtain the container Color from the TopAppBarColors using the `collapsedFraction`, as the
347 // bottom part of this TwoRowsTopAppBar changes color at the same rate the app bar expands or
348 // collapse.
349 // This will potentially animate or interpolate a transition between the container color and the
350 // container's scrolled color according to the app bar's scroll state.
351 val colorTransitionFraction = { scrollBehavior?.state?.collapsedFraction ?: 0f }
352 val appBarContainerColor = { colors.containerColor(colorTransitionFraction()) }
353
354 // Wrap the given actions in a Row.
355 val actionsRow =
356 @Composable {
357 Row(
358 horizontalArrangement = Arrangement.End,
359 verticalAlignment = Alignment.CenterVertically,
360 content = actions,
361 )
362 }
363 val topTitleAlpha = { TopTitleAlphaEasing.transform(colorTransitionFraction()) }
364 val bottomTitleAlpha = { 1f - colorTransitionFraction() }
365 // Hide the top row title semantics when its alpha value goes below 0.5 threshold.
366 // Hide the bottom row title semantics when the top title semantics are active.
367 val hideTopRowSemantics by
368 remember(colorTransitionFraction) { derivedStateOf { colorTransitionFraction() < 0.5f } }
369 val hideBottomRowSemantics = !hideTopRowSemantics
370
371 // Set up support for resizing the top app bar when vertically dragging the bar itself.
372 val appBarDragModifier =
373 if (scrollBehavior != null && !scrollBehavior.isPinned) {
374 Modifier.draggable(
375 orientation = Orientation.Vertical,
376 state =
377 rememberDraggableState { delta -> scrollBehavior.state.heightOffset += delta },
378 onDragStopped = { velocity ->
379 settleAppBar(
380 scrollBehavior.state,
381 velocity,
382 scrollBehavior.flingAnimationSpec,
383 scrollBehavior.snapAnimationSpec,
384 )
385 },
386 )
387 } else {
388 Modifier
389 }
390
391 Box(
392 modifier =
393 modifier
394 .then(appBarDragModifier)
395 .drawBehind { drawRect(color = appBarContainerColor()) }
396 .semantics { isTraversalGroup = true }
397 .pointerInput(Unit) {}
398 ) {
399 Column {
400 TopAppBarLayout(
401 modifier =
402 Modifier
403 .windowInsetsPadding(windowInsets)
404 // clip after padding so we don't show the title over the inset area
405 .clipToBounds(),
406 heightPx = pinnedHeightPx,
407 navigationIconContentColor = colors.navigationIconContentColor,
408 titleContentColor = colors.titleContentColor,
409 actionIconContentColor = colors.actionIconContentColor,
410 title = smallTitle,
411 titleTextStyle = smallTitleTextStyle,
412 titleAlpha = topTitleAlpha,
413 titleVerticalArrangement = Arrangement.Center,
414 titleBottomPadding = 0,
415 hideTitleSemantics = hideTopRowSemantics,
416 navigationIcon = navigationIcon,
417 actions = actionsRow,
418 )
419 TopAppBarLayout(
420 modifier =
421 Modifier
422 // only apply the horizontal sides of the window insets padding, since the
423 // top
424 // padding will always be applied by the layout above
425 .windowInsetsPadding(windowInsets.only(WindowInsetsSides.Horizontal))
426 .clipToBounds(),
427 heightPx =
428 maxHeightPx.floatValue - pinnedHeightPx +
429 (scrollBehavior?.state?.heightOffset ?: 0f),
430 navigationIconContentColor = colors.navigationIconContentColor,
431 titleContentColor = colors.titleContentColor,
432 actionIconContentColor = colors.actionIconContentColor,
433 title = {
434 Box(
435 modifier =
436 Modifier.onGloballyPositioned { coordinates ->
437 val measuredMaxHeightPx =
438 density.run {
439 MaxHeightWithoutTitle.toPx() +
440 coordinates.size.height.toFloat() +
441 titleBaselineHeight.toPx()
442 }
443 // Allow larger max height for multi-line title, but do not reduce
444 // max height to prevent flaky.
445 if (measuredMaxHeightPx > defaultMaxHeightPx) {
446 maxHeightPx.floatValue = measuredMaxHeightPx
447 }
448 }
449 ) {
450 title()
451 }
452 },
453 titleTextStyle = titleTextStyle,
454 titleAlpha = bottomTitleAlpha,
455 titleVerticalArrangement = Arrangement.Bottom,
456 titleBottomPadding = titleBottomPaddingPx,
457 hideTitleSemantics = hideBottomRowSemantics,
458 navigationIcon = {},
459 actions = {},
460 )
461 }
462 }
463 }
464
465 /**
466 * The base [Layout] for all top app bars. This function lays out a top app bar navigation icon
467 * (leading icon), a title (header), and action icons (trailing icons). Note that the navigation and
468 * the actions are optional.
469 *
470 * @param modifier a [Modifier]
471 * @param heightPx the total height this layout is capped to
472 * @param navigationIconContentColor the content color that will be applied via a
473 * [LocalContentColor] when composing the navigation icon
474 * @param titleContentColor the color that will be applied via a [LocalContentColor] when composing
475 * the title
476 * @param actionIconContentColor the content color that will be applied via a [LocalContentColor]
477 * when composing the action icons
478 * @param title the top app bar title (header)
479 * @param titleTextStyle the title's text style
480 * @param modifier a [Modifier]
481 * @param titleAlpha the title's alpha
482 * @param titleVerticalArrangement the title's vertical arrangement
483 * @param titleBottomPadding the title's bottom padding
484 * @param hideTitleSemantics hides the title node from the semantic tree. Apply this boolean when
485 * this layout is part of a [TwoRowsTopAppBar] to hide the title's semantics from accessibility
486 * services. This is needed to avoid having multiple titles visible to accessibility services at
487 * the same time, when animating between collapsed / expanded states.
488 * @param navigationIcon a navigation icon [Composable]
489 * @param actions actions [Composable]
490 * @param titleScaleDisabled whether the title font scaling is disabled. Default is disabled.
491 */
492 @Composable
TopAppBarLayoutnull493 private fun TopAppBarLayout(
494 modifier: Modifier,
495 heightPx: Float,
496 navigationIconContentColor: Color,
497 titleContentColor: Color,
498 actionIconContentColor: Color,
499 title: @Composable () -> Unit,
500 titleTextStyle: TextStyle,
501 titleAlpha: () -> Float,
502 titleVerticalArrangement: Arrangement.Vertical,
503 titleBottomPadding: Int,
504 hideTitleSemantics: Boolean,
505 navigationIcon: @Composable () -> Unit,
506 actions: @Composable () -> Unit,
507 titleScaleDisabled: Boolean = true,
508 ) {
509 Layout(
510 {
511 Box(Modifier
512 .layoutId("navigationIcon")
513 .padding(start = TopAppBarHorizontalPadding)) {
514 CompositionLocalProvider(
515 LocalContentColor provides navigationIconContentColor,
516 content = navigationIcon,
517 )
518 }
519 Box(
520 Modifier
521 .layoutId("title")
522 .padding(horizontal = TopAppBarHorizontalPadding)
523 .then(if (hideTitleSemantics) Modifier.clearAndSetSemantics {} else Modifier)
524 .graphicsLayer { alpha = titleAlpha() }
525 ) {
526 ProvideTextStyle(value = titleTextStyle) {
527 CompositionLocalProvider(
528 LocalContentColor provides titleContentColor,
529 LocalDensity provides
530 with(LocalDensity.current) {
531 Density(
532 density = density,
533 fontScale = if (titleScaleDisabled) 1f else fontScale,
534 )
535 },
536 content = title,
537 )
538 }
539 }
540 Box(Modifier
541 .layoutId("actionIcons")
542 .padding(end = TopAppBarHorizontalPadding)) {
543 CompositionLocalProvider(
544 LocalContentColor provides actionIconContentColor,
545 content = actions,
546 )
547 }
548 },
549 modifier = modifier,
550 ) { measurables, constraints ->
551 val navigationIconPlaceable =
552 measurables
553 .first { it.layoutId == "navigationIcon" }
554 .measure(constraints.copy(minWidth = 0))
555 val actionIconsPlaceable =
556 measurables
557 .first { it.layoutId == "actionIcons" }
558 .measure(constraints.copy(minWidth = 0))
559
560 val maxTitleWidth =
561 if (constraints.maxWidth == Constraints.Infinity) {
562 constraints.maxWidth
563 } else {
564 (constraints.maxWidth - navigationIconPlaceable.width - actionIconsPlaceable.width)
565 .coerceAtLeast(0)
566 }
567 val titlePlaceable =
568 measurables
569 .first { it.layoutId == "title" }
570 .measure(constraints.copy(minWidth = 0, maxWidth = maxTitleWidth))
571
572 // Locate the title's baseline.
573 val titleBaseline =
574 if (titlePlaceable[LastBaseline] != AlignmentLine.Unspecified) {
575 titlePlaceable[LastBaseline]
576 } else {
577 0
578 }
579
580 val layoutHeight = if (heightPx > 0) heightPx.roundToInt() else 0
581
582 layout(constraints.maxWidth, layoutHeight) {
583 // Navigation icon
584 navigationIconPlaceable.placeRelative(
585 x = 0,
586 y = (layoutHeight - navigationIconPlaceable.height) / 2,
587 )
588
589 // Title
590 titlePlaceable.placeRelative(
591 x = max(TopAppBarTitleInset.roundToPx(), navigationIconPlaceable.width),
592 y =
593 when (titleVerticalArrangement) {
594 Arrangement.Center -> (layoutHeight - titlePlaceable.height) / 2
595 // Apply bottom padding from the title's baseline only when the Arrangement
596 // is "Bottom".
597 Arrangement.Bottom ->
598 if (titleBottomPadding == 0) layoutHeight - titlePlaceable.height
599 else
600 layoutHeight -
601 titlePlaceable.height -
602 max(
603 0,
604 titleBottomPadding - titlePlaceable.height + titleBaseline,
605 )
606 // Arrangement.Top
607 else -> 0
608 },
609 )
610
611 // Action icons
612 actionIconsPlaceable.placeRelative(
613 x = constraints.maxWidth - actionIconsPlaceable.width,
614 y = (layoutHeight - actionIconsPlaceable.height) / 2,
615 )
616 }
617 }
618 }
619
620 /**
621 * Settles the app bar by flinging, in case the given velocity is greater than zero, and snapping
622 * after the fling settles.
623 */
624 @OptIn(ExperimentalMaterial3Api::class)
settleAppBarnull625 private suspend fun settleAppBar(
626 state: TopAppBarState,
627 velocity: Float,
628 flingAnimationSpec: DecayAnimationSpec<Float>?,
629 snapAnimationSpec: AnimationSpec<Float>?,
630 ): Velocity {
631 // Check if the app bar is completely collapsed/expanded. If so, no need to settle the app bar,
632 // and just return Zero Velocity.
633 // Note that we don't check for 0f due to float precision with the collapsedFraction
634 // calculation.
635 if (state.collapsedFraction < 0.01f || state.collapsedFraction == 1f) {
636 return Velocity.Zero
637 }
638 var remainingVelocity = velocity
639 // In case there is an initial velocity that was left after a previous user fling, animate to
640 // continue the motion to expand or collapse the app bar.
641 if (flingAnimationSpec != null && abs(velocity) > 1f) {
642 var lastValue = 0f
643 AnimationState(initialValue = 0f, initialVelocity = velocity).animateDecay(
644 flingAnimationSpec
645 ) {
646 val delta = value - lastValue
647 val initialHeightOffset = state.heightOffset
648 state.heightOffset = initialHeightOffset + delta
649 val consumed = abs(initialHeightOffset - state.heightOffset)
650 lastValue = value
651 remainingVelocity = this.velocity
652 // avoid rounding errors and stop if anything is unconsumed
653 if (abs(delta - consumed) > 0.5f) this.cancelAnimation()
654 }
655 }
656 // Snap if animation specs were provided.
657 if (snapAnimationSpec != null) {
658 if (state.heightOffset < 0 && state.heightOffset > state.heightOffsetLimit) {
659 AnimationState(initialValue = state.heightOffset).animateTo(
660 if (state.collapsedFraction < 0.5f) {
661 0f
662 } else {
663 state.heightOffsetLimit
664 },
665 animationSpec = snapAnimationSpec,
666 ) {
667 state.heightOffset = value
668 }
669 }
670 }
671
672 return Velocity(0f, remainingVelocity)
673 }
674
675 // An easing function used to compute the alpha value that is applied to the top title part of a
676 // Medium or Large app bar.
677 private val TopTitleAlphaEasing = CubicBezierEasing(.8f, 0f, .8f, .15f)
678
679 internal val MaxHeightWithoutTitle = if (isSpaExpressiveEnabled) 84.dp else 124.dp
680 internal val DefaultTitleHeight = 52.dp
681 internal val ContainerHeight = 56.dp
682 private val titleBaselineHeight = if (isSpaExpressiveEnabled) 8.dp else 0.dp
683 private val LargeTitleBottomPadding = 28.dp
684 private val TopAppBarHorizontalPadding = 4.dp
685
686 // A title inset when the App-Bar is a Medium or Large one. Also used to size a spacer when the
687 // navigation icon is missing.
688 private val TopAppBarTitleInset = 16.dp - TopAppBarHorizontalPadding
689