• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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