Last active
September 9, 2022 11:33
-
-
Save racka98/1d0a6f6016495167fbd4389021dd5540 to your computer and use it in GitHub Desktop.
Collapsing Toolbar with Jetpack Compose using NestedScrollConnection
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
/** | |
* Collapsing Toolbar that can be used in a topBar slot of Scaffold. | |
* It has a back button, default bottom rounded corners | |
* & a box scope which holds content centered by default. | |
* You need to implement nestedScrollConnection to set the offset values | |
* See Usage of this in DashboardScreen or TasksScreen or GoalsScreen | |
* | |
* To use this Toolbar without a heading text just make toolbarHeading `null` | |
* To Disable the back button at the top set showBackButton to false | |
* | |
* With nestedScrollConnection know that the maximum offset that can be | |
* reached is -132.0 | |
*/ | |
@Composable | |
fun CollapsingToolbarBase( | |
modifier: Modifier = Modifier, | |
toolbarHeading: String?, | |
showBackButton: Boolean = true, | |
onBackButtonPressed: () -> Unit = { }, | |
contentAlignment: Alignment = Alignment.Center, | |
shape: Shape = Shapes.large, | |
collapsedBackgroundColor: Color = MaterialTheme.colorScheme.background, | |
backgroundColor: Color = MaterialTheme.colorScheme.background, | |
toolbarHeight: Dp, | |
minShrinkHeight: Dp = 100.dp, | |
toolbarOffset: Float, | |
onCollapsed: (Boolean) -> Unit, | |
content: @Composable BoxScope.() -> Unit, | |
) { | |
val scrollDp = toolbarHeight + toolbarOffset.dp | |
val collapsed by remember(scrollDp) { | |
mutableStateOf( | |
scrollDp < minShrinkHeight + 20.dp | |
) | |
} | |
val animatedCardSize by animateDpAsState( | |
targetValue = if (scrollDp <= minShrinkHeight) minShrinkHeight else scrollDp, | |
animationSpec = tween(300, easing = LinearOutSlowInEasing) | |
) | |
val animatedElevation by animateDpAsState( | |
targetValue = if (scrollDp < minShrinkHeight + 20.dp) 10.dp else 0.dp, | |
animationSpec = tween(500, easing = LinearOutSlowInEasing) | |
) | |
val animatedTitleAlpha by animateFloatAsState( | |
targetValue = if (!toolbarHeading.isNullOrBlank()) { | |
if (scrollDp <= minShrinkHeight + 20.dp) 1f else 0f | |
} else 0f, | |
animationSpec = tween(300, easing = LinearOutSlowInEasing) | |
) | |
val animatedColor by animateColorAsState( | |
targetValue = if (scrollDp < minShrinkHeight + 20.dp) collapsedBackgroundColor | |
else backgroundColor, | |
animationSpec = tween(300, easing = LinearOutSlowInEasing) | |
) | |
LaunchedEffect(key1 = collapsed) { | |
onCollapsed(collapsed) | |
} | |
Box( | |
modifier = Modifier | |
.fillMaxWidth() | |
.shadow( | |
elevation = animatedElevation, | |
shape = shape | |
) | |
.background( | |
color = animatedColor, | |
shape = shape | |
) | |
) { | |
Box( | |
modifier = modifier | |
.height(animatedCardSize) | |
) { | |
Row( | |
horizontalArrangement = Arrangement.Start, | |
verticalAlignment = Alignment.CenterVertically | |
) { | |
if (showBackButton) { | |
IconButton( | |
onClick = onBackButtonPressed, | |
modifier = Modifier | |
.padding(Dimens.SmallPadding.size) | |
) { | |
Icon( | |
imageVector = Icons.Rounded.ArrowBack, | |
contentDescription = stringResource(id = R.string.back_icon), | |
tint = MaterialTheme.colorScheme.onSecondaryContainer | |
) | |
} | |
} | |
toolbarHeading?.let { | |
Text( | |
text = toolbarHeading, | |
color = MaterialTheme.colorScheme.onSecondaryContainer.copy( | |
alpha = animatedTitleAlpha | |
), | |
style = MaterialTheme.typography.headlineLarge, | |
modifier = Modifier | |
.padding(horizontal = Dimens.SmallPadding.size) | |
) | |
} | |
} | |
Box( | |
modifier = Modifier | |
.fillMaxSize() | |
.alpha(1 - animatedTitleAlpha), | |
contentAlignment = contentAlignment, | |
content = 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
@Composable | |
private fun SomeScreenTopBar( | |
tabPage: TabDestinations, | |
profilePicUrl: String, | |
toolbarOffset: Float, | |
toolbarCollapsed: Boolean, | |
onCollapsed: (Boolean) -> Unit, | |
updateTabPage: (TabDestinations) -> Unit, | |
) { | |
CollapsingToolbarBase( | |
modifier = Modifier | |
.statusBarsPadding(), | |
toolbarHeading = null, | |
toolbarHeight = 120.dp, | |
toolbarOffset = toolbarOffset, | |
showBackButton = false, | |
minShrinkHeight = 60.dp, | |
shape = RectangleShape, | |
onCollapsed = { | |
onCollapsed(it) | |
} | |
) { | |
Column( | |
modifier = Modifier | |
.animateContentSize() | |
.fillMaxWidth(), | |
verticalArrangement = Arrangement | |
.spacedBy(16.dp) | |
) { | |
AnimatedVisibility( | |
visible = !toolbarCollapsed, | |
enter = fadeIn(), | |
exit = fadeOut() | |
) { | |
FancySearchBar( | |
extraButton = { | |
ProfilePicture( | |
pictureUrl = profilePicUrl | |
) | |
} | |
) | |
} | |
LazyRow { | |
item { | |
TasksTabBar( | |
modifier = Modifier | |
.padding(horizontal = 16.dp), | |
tabPage = tabPage, | |
onTabSelected = { | |
updateTabPage(it) | |
} | |
) | |
} | |
} | |
} | |
} | |
} |
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
@Composable | |
fun YourScreen() { | |
// CollapsingToolbar NestedScrollConnection Impl | |
val toolbarHeight = 120.dp | |
val toolbarCollapsed = rememberSaveable { mutableStateOf(false) } | |
val toolbarHeightPx = with(LocalDensity.current) { toolbarHeight.roundToPx().toFloat() } | |
val toolbarOffsetHeightPx = remember { mutableStateOf(0f) } | |
val nestedScrollConnection = remember { | |
object : NestedScrollConnection { | |
override fun onPreScroll( | |
available: Offset, | |
source: NestedScrollSource, | |
): Offset { | |
val delta = available.y | |
val newOffset = toolbarOffsetHeightPx.value + delta | |
toolbarOffsetHeightPx.value = newOffset.coerceIn(-toolbarHeightPx, 0f) | |
// Returning Zero so we just observe the scroll but don't execute it | |
return Offset.Zero | |
} | |
} | |
} | |
Scaffold( | |
topBar = { | |
SomeScreenTopBar( | |
tabPage = tabPage, | |
profilePicUrl = "https://via.placeholder.com/150", | |
toolbarOffset = toolbarOffsetHeightPx.value, | |
toolbarCollapsed = toolbarCollapsed.value, | |
onCollapsed = { | |
toolbarCollapsed.value = it | |
}, | |
updateTabPage = { | |
navController.navigate(it.route) | |
}, | |
) | |
} | |
) { | |
LazyColumn( | |
modifier = Modifier | |
.nestedScroll(nestedScrollConnection) // Attach the nestedScrollConnection | |
.fillMaxSize(), | |
verticalArrangement = Arrangement | |
.spacedBy(Dimens.MediumPadding.size) | |
) { | |
items(someList) { item -> | |
SomeEntry( | |
entry = item | |
) | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment