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