Hey there, I'm trying to sync the elevation of a ...
# compose
d
Hey there, I'm trying to sync the elevation of a
TopAppBar
based on the scroll position within a
LazyList
. The
TopAppBar
is within a
Scaffold
, and the Scaffold content is a
NavGraph
. This NavGraph contains a composable which contains my LazyList. I tried to do it by creating an
AppBarState
which holds the current elevation as a
MutableState<Float>
and also the elevationRange (
0f..4f
). I also created a
LocalAppBarState
which holds the current
AppBarState
. The elevation of my
TopAppBar
is defined using
LocalAppBarState.current.elevation.dp
. To define the elevation I created a composable that return a
LazyListState
. It uses the
LocalAppBarState
to define the calculated elevation based on the
LazyListState
. Unfortunately, doing this cause my app to lag when scrolling. But I'm not sure I know any other way to do it. I read some things about
Modifier.graphicsLayer
, but even after trying that the issue remains the same. Is there a better way to do it? I will leave as the first comment in this thread the code for all of that.
The code:
Copy code
class AppBarState {
    var elevationRange = 0f..4f
    var elevation by mutableStateOf(elevationRange.start)
}

@Composable
fun rememberAppBarState(): AppBarState = remember { AppBarState() }

val LocalAppBarState = compositionLocalOf<AppBarState> { error("LocalAppBarState not provided") }

@Composable
fun rememberAppBarAwareLazyListState(
    initialFirstVisibleItemIndex: Int = 0,
    initialFirstVisibleItemScrollOffset: Int = 0
): LazyListState {
    val appBarState = LocalAppBarState.current

    val lazyListState =
        rememberLazyListState(initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset)

    if (lazyListState.isScrollInProgress) {
        val elevation = if (lazyListState.firstVisibleItemIndex == 0) {
            (lazyListState.firstVisibleItemScrollOffset
                .toFloat() / OffsetDivider)
                .coerceIn(appBarState.elevationRange)
        } else {
            appBarState.elevationRange.endInclusive
        }

        appBarState.elevation = elevation
    }

    return lazyListState
}
a
Please keep the main body short and put the details in the thread https://kotlinlang.slack.com/archives/CJLTWPH7S/p1614355175421600
d
That's what I did, the code snippet is in the thread. Thanks for the reminder though.
g
This is how I did it. Probably may be improved and optimized, but it’s working.
Copy code
private val MAX_OFFSET = 16.dp
private val MAX_ELEVATION = AppBarDefaults.TopAppBarElevation

private fun calculateTopBarElevation(
    offset: Float,
    maxOffset: Float,
    maxElevation: Float,
): Float {
    return offset.coerceIn(0f..maxOffset) * maxElevation / maxOffset
}

@Composable
fun rememberTopBarElevation(
    scrollState: ScrollState
): Dp {
    val offset = scrollState.value
    return rememberTopBarElevation(offset = offset.toFloat())
}

@Composable
fun rememberTopBarElevation(
    lazyListState: LazyListState
): Dp {
    with(LocalDensity.current) {
        /*
        Unfortunately for LazyListState there is no way to get the offset from
        the beginning of the list, only from the beginning of the last visible
        item. That's why we're using this workaround. It won't work 100% correctly
        if the first item will be smaller than MAX_OFFSET, but it shouldn't
        happen in practice.
         */
        val offset = if (lazyListState.firstVisibleItemIndex == 0) {
            lazyListState.firstVisibleItemScrollOffset
        } else MAX_OFFSET.toPx()
        return rememberTopBarElevation(offset = offset.toFloat())
    }
}

@Composable
private fun rememberTopBarElevation(
    offset: Float
): Dp {
    with(LocalDensity.current) {
        val maxOffset = remember { MAX_OFFSET.toPx() }
        val maxElevation = remember { MAX_ELEVATION.toPx() }
        return remember(offset) {
            calculateTopBarElevation(
                offset = offset,
                maxOffset = maxOffset,
                maxElevation = maxElevation,
            ).toDp()
        }
    }
}
And then, the usage:
Copy code
val listScrollState = rememberLazyListState()

    Scaffold(
        ...,
        topBar = {
            AppBar(
                ...,
                elevation = rememberTopBarElevation(listScrollState),
            )
        }
    ) {
            LazyPagingColumn(
                ...,
                lazyListState = listScrollState
            )
    }
It supports both
LazyListState
and
ScrollState
, but you may just ignore the second
d
I see thanks, though it looks more or less like what I'm currently doing. I just can't initiate my LazyListState above the Scaffold (as it would mean passing it in many Composables), but otherwise it's almost like what I'm already doing (see my first comment in this thread).