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