5

The attribute app:liftOnScroll="true" in AppBarLayout allows the view to appear lifted (basically adding a small line of elevation shadow at the bottom) when an associated view with app:layout_behavior="@string/appbar_scrolling_view_behavior" is scrolled (usually a RecyclerView or anything with inside a ScrolelView).

Is there anything like this in Jetpack Compose? I haven't been able to find the compatible behavior, neither with TopAppBar nor with a regular Text (in any of their parameters or modifiers).

In short - is there any way to elevate a toolbar-like view but only when the underlying list/scrollable content is scrolled (not at it's top)?

MichaelThePotato
  • 1,523
  • 2
  • 10
  • 19

1 Answers1

2

Unfortunately It's not yet supported. But you can create it yourself. You need to wrap your main content with scrollable component and animate AppBar's eleveation according to scrollState of main content.

Here is example:

/**
 * AppBarScaffold displays TopAppBar above specified content. Content is wrapped with
 * scrollable Column. TopAppBar's elevation is animated depending on content scroll state.
 *
 * Back arrow is shown only if [onBackPress] is not null
 *
 * @param onBackPress Call back when back arrow icon is pressed
 */
@Composable
fun AppBarScaffold(
    modifier: Modifier = Modifier,
    title: String = "",
    onBackPress: (() -> Unit)? = null,
    actions: @Composable RowScope.() -> Unit = {},
    content: @Composable () -> Unit
) {
    val scrollState = rememberScrollState()
    val contentColor = contentColorFor(MaterialTheme.colors.background)

    val elevation by animateDpAsState(if (scrollState.value == 0) 0.dp else AppBarDefaults.TopAppBarElevation)

    Surface(
        modifier = modifier.statusBarsPadding(),
        color = MaterialTheme.colors.background,
        contentColor = contentColor
    ) {
        val topBar = @Composable { AppBar(title, onBackPress, actions, elevation) }
        val body = @Composable {
            Column(modifier = Modifier.verticalScroll(state = scrollState)) {
                content()
            }
        }

        SubcomposeLayout { constraints ->
            val layoutWidth = constraints.maxWidth
            val layoutHeight = constraints.maxHeight
            val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)

            layout(layoutWidth, layoutHeight) {
                val topBarPlaceables = subcompose(AppBarContent.AppBar, topBar).map {
                    it.measure(looseConstraints)
                }
                val topBarHeight = topBarPlaceables.maxByOrNull { it.height }?.height ?: 0
                val bodyContentHeight = layoutHeight - topBarHeight

                val bodyContentPlaceables = subcompose(AppBarContent.MainContent) {
                    body()
                }.map { it.measure(looseConstraints.copy(maxHeight = bodyContentHeight)) }


                bodyContentPlaceables.forEach {
                    it.place(0, topBarHeight)
                }
                topBarPlaceables.forEach {
                    it.place(0, 0)
                }
            }
        }
    }
}

@Composable
fun BackArrow(onclick: () -> Unit) {
    IconButton(onClick = { onclick() }) {
        Icon(imageVector = Icons.Default.ArrowBack, "Back Arrow")
    }
}

@Composable
private fun AppBar(
    title: String = "",
    onBackPress: (() -> Unit)? = null,
    actions: @Composable RowScope.() -> Unit = {},
    elevation: Dp
) {

    var navigationIcon: @Composable (() -> Unit)? = null
    onBackPress?.let {
        navigationIcon = { BackArrow { onBackPress.invoke() } }
    }

    Surface(
        color = MaterialTheme.colors.background,
        contentColor = MaterialTheme.colors.onBackground,
        elevation = elevation,
        shape = RectangleShape
    ) {
        Box(
            contentAlignment = Alignment.Center,
            modifier = Modifier.fillMaxWidth().padding(AppBarDefaults.ContentPadding).height(AppBarHeight)
        ) {
            navigationIcon?.let {
                Row(TitleIconModifier.align(Alignment.CenterStart), verticalAlignment = Alignment.CenterVertically) {
                    CompositionLocalProvider(
                        LocalContentAlpha provides ContentAlpha.high,
                        content = it
                    )
                }
            }

            Row(
                horizontalArrangement = Arrangement.Center,
                modifier = Modifier.fillMaxWidth(0.5f).fillMaxHeight(),
                verticalAlignment = Alignment.CenterVertically
            ) {
                Text(
                    text = title,
                    style = MaterialTheme.typography.h4,
                    textAlign = TextAlign.Center
                )
            }

            CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
                Row(
                    Modifier.align(Alignment.CenterEnd).fillMaxHeight(),
                    horizontalArrangement = Arrangement.End,
                    verticalAlignment = Alignment.CenterVertically,
                    content = actions
                )
            }

        }
    }

}

private val AppBarHorizontalPadding = 4.dp

private val TitleIconModifier = Modifier.fillMaxHeight().width(72.dp - AppBarHorizontalPadding)

private val AppBarHeight = 56.dp

enum class AppBarContent {
    AppBar,
    MainContent
}
Vygintas B
  • 1,624
  • 13
  • 31