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