Is not having a built-in scrollbar for lazy column...
# compose
r
Is not having a built-in scrollbar for lazy column an intrinsic problem of Compose? I tried a few solutions but they either don't work well (unstable height and flickering) or they're very complex (i.e. more than a thousand lines). And even then they don't work perfectly - the height changes as you scroll downwards, as new items come in the composition. Perhaps my issue is that my items don't have static height but that's quite a limitation.
s
You got imperfect information, so you will get an imperfect scroll-bar, I think that just makes sense, nothing to do with compose itself. If you know the number of total items, and you know all of their height you can get an accurate scroll-bar, otherwise you either have to start making assumptions to make it feel nicer, which always gives the risk of making wrong assumptions and getting a bad result anyway. Or you just make do with what is visible to you and you get a scrollbar which will move around in size as more items come into the list.
r
Thank you! I'll take a look.
y
this works fine for me
Copy code
@Composable
fun Modifier.drawVerticalScrollbar(
    state: LazyListState,
    reverseScrolling: Boolean = false,
    thickness: Dp = 4.dp,
    color: Color = MaterialTheme.colorScheme.onPrimaryContainer,
): Modifier = drawScrollbar(state, Orientation.Vertical, reverseScrolling, thickness, color)

@Composable
private fun Modifier.drawScrollbar(
    state: LazyListState,
    orientation: Orientation,
    reverseScrolling: Boolean,
    thickness: Dp = 4.dp,
    color: Color = MaterialTheme.colorScheme.onPrimaryContainer,
): Modifier =
    drawScrollbar(
        orientation,
        reverseScrolling,
        color,
    ) { reverseDirection, atEnd, indicatorColor, alpha ->
        val layoutInfo = state.layoutInfo
        val viewportSize = layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset
        val items = layoutInfo.visibleItemsInfo
        val itemsSize = items.sumOf { it.size }
        if (items.size < layoutInfo.totalItemsCount || itemsSize > viewportSize) {
            val estimatedItemSize = if (items.isEmpty()) 0f else itemsSize.toFloat() / items.size
            val totalSize = estimatedItemSize * layoutInfo.totalItemsCount
            val canvasSize = if (orientation == Orientation.Horizontal) size.width else size.height
            val thumbSize = viewportSize / totalSize * canvasSize
            val startOffset =
                if (items.isEmpty())
                    0f
                else
                    items.first().run {
                        (estimatedItemSize * index - offset) / totalSize * canvasSize
                    }
            drawScrollbar(
                orientation,
                reverseDirection,
                atEnd,
                indicatorColor,
                alpha,
                thumbSize,
                startOffset,
                thickness,
            )
        }
    }

@Composable
private fun Modifier.drawScrollbar(
    orientation: Orientation,
    reverseScrolling: Boolean,
    barColor: Color = MaterialTheme.colorScheme.onPrimaryContainer,
    onDraw: DrawScope.(
        reverseDirection: Boolean,
        atEnd: Boolean,
        color: Color,
        alpha: () -> Float,
    ) -> Unit,
): Modifier =
    composed {
        val scrolled =
            remember {
                MutableSharedFlow<Unit>(
                    extraBufferCapacity = 1,
                    onBufferOverflow = BufferOverflow.DROP_OLDEST,
                )
            }
        val nestedScrollConnection =
            remember(orientation, scrolled) {
                object : NestedScrollConnection {
                    override fun onPostScroll(
                        consumed: Offset,
                        available: Offset,
                        source: NestedScrollSource,
                    ): Offset {
                        val delta =
                            if (orientation == Orientation.Horizontal) consumed.x else consumed.y
                        if (delta != 0f) scrolled.tryEmit(Unit)
                        return Offset.Zero
                    }
                }
            }

        val alpha = remember { Animatable(0f) }
        LaunchedEffect(scrolled, alpha) {
            scrolled.collectLatest {
                alpha.snapTo(1f)
                delay(ScrollBarFadeDuration.toLong())
                alpha.animateTo(0f, animationSpec = FadeOutAnimationSpec)
            }
        }

        val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
        val reverseDirection =
            if (orientation == Orientation.Horizontal) {
                if (isLtr) reverseScrolling else !reverseScrolling
            } else {
                reverseScrolling
            }
        val atEnd = if (orientation == Orientation.Vertical) isLtr else true

        Modifier
            .nestedScroll(nestedScrollConnection)
            .drawWithContent {
                drawContent()
                onDraw(reverseDirection, atEnd, barColor, alpha::value)
            }
    }
you just use drawVerticalScrollbar in the modifier of a lazyColumn
s
This also jumps if there's more items added to your list right? Also it doesn't handle dragging the bar itself as far as I can tell?
👍 2
y
yes and when you add a sticky header it doesn't work too