androidscrollandroid-jetpack-composescrollbar

How to add scrollbars to Column?


I made scrollable content, but how to add scrollbarThumbVertical?

Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.SpaceBetween,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .verticalScroll(rememberScrollState())//how to add here scrollbar?
                .weight(1f, fill = false)
        ) {
            //content
        }
        //another not scrollable content
}

Solution

  • To add a scrollbar along with verticalScroll or horizontalScroll modifier (Supports both LTR and RTL Layout directions). You can configure the scrollbar using scrollbarConfig parameter.

    fun Modifier.scrollbar(
        scrollState: ScrollState,
        direction: Orientation,
        config: ScrollbarConfig = ScrollbarConfig(),
    ): Modifier = composed {
        var (
            indicatorThickness, indicatorColor, indicatorCornerRadius,
            alpha, alphaAnimationSpec, padding
        ) = config
    
        val isScrollingOrPanning = scrollState.isScrollInProgress
        val isVertical = direction == Orientation.Vertical
    
        alpha = alpha ?: if (isScrollingOrPanning) 0.8f else 0f
        alphaAnimationSpec = alphaAnimationSpec ?: tween(
            delayMillis = if (isScrollingOrPanning) 0 else 1500,
            durationMillis = if (isScrollingOrPanning) 150 else 500
        )
    
        val scrollbarAlpha by animateFloatAsState(alpha, alphaAnimationSpec)
    
        drawWithContent {
            drawContent()
    
            val showScrollbar = isScrollingOrPanning || scrollbarAlpha > 0.0f
    
            // Draw scrollbar only if currently scrolling or if scroll animation is ongoing.
            if (showScrollbar) {
                val (topPadding, bottomPadding, startPadding, endPadding) = arrayOf(
                    padding.calculateTopPadding().toPx(), padding.calculateBottomPadding().toPx(),
                    padding.calculateStartPadding(layoutDirection).toPx(),
                    padding.calculateEndPadding(layoutDirection).toPx()
                )
    
                val isLtr = layoutDirection == LayoutDirection.Ltr
                val contentOffset = scrollState.value
                val viewPortLength = if (isVertical) size.height else size.width
                val viewPortCrossAxisLength = if (isVertical) size.width else size.height
                val contentLength = max(viewPortLength + scrollState.maxValue, 0.001f /* To prevent divide by zero error */)
                val scrollbarLength = viewPortLength -
                        (if (isVertical) topPadding + bottomPadding else startPadding + endPadding)
                val indicatorThicknessPx = indicatorThickness.toPx()
                val indicatorLength = max((scrollbarLength / contentLength) * viewPortLength, 20f.dp.toPx())
                val indicatorOffset = (scrollbarLength / contentLength) * contentOffset
                val scrollIndicatorSize = if (isVertical) Size(indicatorThicknessPx, indicatorLength)
                else Size(indicatorLength, indicatorThicknessPx)
    
                val scrollIndicatorPosition = if (isVertical)
                    Offset(
                        x = if (isLtr) viewPortCrossAxisLength - indicatorThicknessPx - endPadding
                            else startPadding,
                        y = indicatorOffset + topPadding
                    )
                else
                    Offset(
                        x = if (isLtr) indicatorOffset + startPadding
                            else viewPortLength - indicatorOffset - indicatorLength - endPadding,
                        y = viewPortCrossAxisLength - indicatorThicknessPx - bottomPadding
                    )
    
                drawRoundRect(
                    color = indicatorColor,
                    cornerRadius = indicatorCornerRadius.let { CornerRadius(it.toPx(), it.toPx()) },
                    topLeft = scrollIndicatorPosition,
                    size = scrollIndicatorSize,
                    alpha = scrollbarAlpha
                )
            }
        }
    }
    
    data class ScrollbarConfig(
        val indicatorThickness: Dp = 8.dp,
        val indicatorColor: Color = Color.Gray.copy(alpha = 0.7f),
        val indicatorCornerRadius: Dp = indicatorThickness / 2,
        val alpha: Float? = null,
        val alphaAnimationSpec: AnimationSpec<Float>? = null,
        val padding: PaddingValues = PaddingValues(all = 0.dp),
    )
    
    fun Modifier.verticalScrollWithScrollbar(
        scrollState: ScrollState,
        enabled: Boolean = true,
        flingBehavior: FlingBehavior? = null,
        reverseScrolling: Boolean = false,
        scrollbarConfig: ScrollbarConfig = ScrollbarConfig()
    ): Modifier = this
        .scrollbar(scrollState, direction = Orientation.Vertical, config = scrollbarConfig)
        .verticalScroll(scrollState, enabled, flingBehavior, reverseScrolling)
    
    
    fun Modifier.horizontalScrollWithScrollbar(
        scrollState: ScrollState,
        enabled: Boolean = true,
        flingBehavior: FlingBehavior? = null,
        reverseScrolling: Boolean = false,
        scrollbarConfig: ScrollbarConfig = ScrollbarConfig()
    ): Modifier = this
        .scrollbar(scrollState, direction = Orientation.Horizontal, config = scrollbarConfig)
        .horizontalScroll(scrollState, enabled, flingBehavior, reverseScrolling)
    

    Usage example:

    Column(
        Modifier
            .fillMaxWidth()
            .heightIn(max = 300.dp)
            .padding(4.dp)
            .border(1.dp, Color.Gray, RoundedCornerShape(8.dp))
            .verticalScrollWithScrollbar(
                rememberScrollState(),
                scrollbarConfig = ScrollbarConfig(
                    padding = PaddingValues(4.dp, 4.dp, 4.dp, 4.dp)
                )
            )
    
    ) {
        Text(
            text = "Some very long scrollable text",
            color = Color.Gray,
            modifier = Modifier.padding(vertical = 4.dp)
        )
    }