Last active
May 27, 2023 03:31
-
-
Save axiel7/54417f67858a0d98c5860940493e0020 to your computer and use it in GitHub Desktop.
Custom implementation of Material3 TopAppBar with custom content
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import androidx.compose.animation.animateColorAsState | |
import androidx.compose.animation.core.FastOutLinearInEasing | |
import androidx.compose.animation.core.Spring | |
import androidx.compose.animation.core.spring | |
import androidx.compose.foundation.layout.Box | |
import androidx.compose.foundation.layout.WindowInsets | |
import androidx.compose.foundation.layout.windowInsetsPadding | |
import androidx.compose.material3.ExperimentalMaterial3Api | |
import androidx.compose.material3.MaterialTheme | |
import androidx.compose.material3.Surface | |
import androidx.compose.material3.TopAppBarDefaults | |
import androidx.compose.material3.TopAppBarScrollBehavior | |
import androidx.compose.material3.surfaceColorAtElevation | |
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.SideEffect | |
import androidx.compose.runtime.getValue | |
import androidx.compose.ui.Modifier | |
import androidx.compose.ui.draw.clipToBounds | |
import androidx.compose.ui.graphics.Color | |
import androidx.compose.ui.graphics.lerp | |
import androidx.compose.ui.layout.Layout | |
import androidx.compose.ui.layout.layoutId | |
import androidx.compose.ui.platform.LocalDensity | |
import androidx.compose.ui.unit.dp | |
import kotlin.math.roundToInt | |
private val TopAppBarHeight = 64.dp | |
/** | |
* Custom implementation of Material3 TopAppBar with custom content | |
*/ | |
@OptIn(ExperimentalMaterial3Api::class) | |
@Composable | |
fun TopAppBarWithContent( | |
modifier: Modifier = Modifier, | |
windowInsets: WindowInsets = TopAppBarDefaults.windowInsets, | |
containerColor: Color = MaterialTheme.colorScheme.surface, | |
scrolledContainerColor: Color = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp), | |
scrollBehavior: TopAppBarScrollBehavior, | |
applyHeightConstraint: Boolean = true, | |
content: @Composable () -> Unit, | |
) { | |
// Sets the app bar's height offset to collapse the entire bar's height when content is | |
// scrolled. | |
val heightOffsetLimit = with(LocalDensity.current) { -TopAppBarHeight.toPx() } | |
SideEffect { | |
if (scrollBehavior.state.heightOffsetLimit != heightOffsetLimit) { | |
scrollBehavior.state.heightOffsetLimit = heightOffsetLimit | |
} | |
} | |
// Obtain the container color from the TopAppBarColors using the `overlapFraction`. This | |
// ensures that the colors will adjust whether the app bar behavior is pinned or scrolled. | |
// This may potentially animate or interpolate a transition between the container-color and the | |
// container's scrolled-color according to the app bar's scroll state. | |
val colorTransitionFraction = scrollBehavior.state.overlappedFraction | |
val fraction = if (colorTransitionFraction > 0.01f) 1f else 0f | |
val appBarContainerColor by animateColorAsState( | |
targetValue = lerp( | |
containerColor, | |
scrolledContainerColor, | |
FastOutLinearInEasing.transform(fraction) | |
), | |
animationSpec = spring(stiffness = Spring.StiffnessMediumLow) | |
) | |
// The surface's background color is animated as specified above. | |
// The height of the app bar is determined by subtracting the bar's height offset from the | |
// app bar's defined constant height value. | |
Surface(modifier = modifier, color = appBarContainerColor) { | |
val height = LocalDensity.current.run { | |
TopAppBarHeight.toPx() + scrollBehavior.state.heightOffset | |
} | |
Layout( | |
content = { | |
Box( | |
modifier = Modifier.layoutId("content"), | |
) { | |
content() | |
} | |
}, | |
modifier = if (applyHeightConstraint) | |
Modifier | |
.windowInsetsPadding(windowInsets) | |
// clip after padding so we don't show the title over the inset area | |
.clipToBounds() | |
else Modifier, | |
measurePolicy = { measurables, constraints -> | |
val contentPlaceable = | |
measurables.first { it.layoutId == "content" } | |
.measure(constraints.copy(minWidth = 0, maxWidth = constraints.maxWidth)) | |
val layoutHeight = if (applyHeightConstraint) height.roundToInt() else constraints.maxHeight | |
layout(constraints.maxWidth, layoutHeight) { | |
contentPlaceable.place(0, 0) | |
} | |
} | |
) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment