How to add scrollbars to Column?
Asked Answered
T

3

5

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
}
Thermidor answered 6/1, 2023 at 20:46 Comment(3)
I guess it will be useful https://mcmap.net/q/342824/-jetpack-compose-scrollbarsRecessive
@SergeiS there is answer for LazyList, but I want to add scrollbar for ColumnThermidor
#71838779Beatnik
E
10

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(
    state: ScrollState,
    direction: Orientation,
    indicatorThickness: Dp = 8.dp,
    indicatorColor: Color = Color.LightGray,
    alpha: Float = if (state.isScrollInProgress) 0.8f else 0f,
    alphaAnimationSpec: AnimationSpec<Float> = tween(
        delayMillis = if (state.isScrollInProgress) 0 else 1500,
        durationMillis = if (state.isScrollInProgress) 150 else 500
    ),
    padding: PaddingValues = PaddingValues(all = 0.dp)
): Modifier = composed {
    val scrollbarAlpha by animateFloatAsState(
        targetValue = alpha,
        animationSpec = alphaAnimationSpec
    )

    drawWithContent {
        drawContent()

        val showScrollBar = state.isScrollInProgress || scrollbarAlpha > 0.0f

        // Draw scrollbar only if currently scrolling or if scroll animation is ongoing.
        if (showScrollBar) {
            val (topPadding, bottomPadding, startPadding, endPadding) = listOf(
                padding.calculateTopPadding().toPx(), padding.calculateBottomPadding().toPx(),
                padding.calculateStartPadding(layoutDirection).toPx(),
                padding.calculateEndPadding(layoutDirection).toPx()
            )
            val contentOffset = state.value
            val viewPortLength = if (direction == Orientation.Vertical)
                size.height else size.width
            val viewPortCrossAxisLength = if (direction == Orientation.Vertical)
                size.width else size.height
            val contentLength = max(viewPortLength + state.maxValue, 0.001f)  // To prevent divide by zero error
            val indicatorLength = ((viewPortLength / contentLength) * viewPortLength) - (
                    if (direction == Orientation.Vertical) topPadding + bottomPadding
                    else startPadding + endPadding
            )
            val indicatorThicknessPx = indicatorThickness.toPx()

            val scrollOffsetViewPort = viewPortLength * contentOffset / contentLength

            val scrollbarSizeWithoutInsets = if (direction == Orientation.Vertical)
                Size(indicatorThicknessPx, indicatorLength)
            else Size(indicatorLength, indicatorThicknessPx)

            val scrollbarPositionWithoutInsets = if (direction == Orientation.Vertical)
                Offset(
                    x = if (layoutDirection == LayoutDirection.Ltr)
                        viewPortCrossAxisLength - indicatorThicknessPx - endPadding
                    else startPadding,
                    y = scrollOffsetViewPort + topPadding
                )
            else
                Offset(
                    x = if (layoutDirection == LayoutDirection.Ltr)
                        scrollOffsetViewPort + startPadding
                    else viewPortLength - scrollOffsetViewPort - indicatorLength - endPadding,
                    y = viewPortCrossAxisLength - indicatorThicknessPx - bottomPadding
                )

            drawRoundRect(
                color = indicatorColor,
                cornerRadius = CornerRadius(
                    x = indicatorThicknessPx / 2, y = indicatorThicknessPx / 2
                ),
                topLeft = scrollbarPositionWithoutInsets,
                size = scrollbarSizeWithoutInsets,
                alpha = scrollbarAlpha
            )
        }
    }
}

data class ScrollBarConfig(
    val indicatorThickness: Dp = 8.dp,
    val indicatorColor: Color = Color.LightGray,
    val alpha: Float? = null,
    val alphaAnimationSpec: AnimationSpec<Float>? = null,
    val padding: PaddingValues = PaddingValues(all = 0.dp)
)

fun Modifier.verticalScrollWithScrollbar(
    state: ScrollState,
    enabled: Boolean = true,
    flingBehavior: FlingBehavior? = null,
    reverseScrolling: Boolean = false,
    scrollbarConfig: ScrollBarConfig = ScrollBarConfig()
) = this
    .scrollbar(
        state, Orientation.Vertical,
        indicatorThickness = scrollbarConfig.indicatorThickness,
        indicatorColor = scrollbarConfig.indicatorColor,
        alpha = scrollbarConfig.alpha ?: if (state.isScrollInProgress) 0.8f else 0f,
        alphaAnimationSpec = scrollbarConfig.alphaAnimationSpec ?: tween(
            delayMillis = if (state.isScrollInProgress) 0 else 1500,
            durationMillis = if (state.isScrollInProgress) 150 else 500
        ),
        padding = scrollbarConfig.padding
    )
    .verticalScroll(state, enabled, flingBehavior, reverseScrolling)



fun Modifier.horizontalScrollWithScrollbar(
    state: ScrollState,
    enabled: Boolean = true,
    flingBehavior: FlingBehavior? = null,
    reverseScrolling: Boolean = false,
    scrollbarConfig: ScrollBarConfig = ScrollBarConfig()
) = this
    .scrollbar(
        state, Orientation.Horizontal,
        indicatorThickness = scrollbarConfig.indicatorThickness,
        indicatorColor = scrollbarConfig.indicatorColor,
        alpha = scrollbarConfig.alpha ?: if (state.isScrollInProgress) 0.8f else 0f,
        alphaAnimationSpec = scrollbarConfig.alphaAnimationSpec ?: tween(
            delayMillis = if (state.isScrollInProgress) 0 else 1500,
            durationMillis = if (state.isScrollInProgress) 150 else 500
        ),
        padding = scrollbarConfig.padding
    )
    .horizontalScroll(state, enabled, flingBehavior, reverseScrolling)

Usage example:

Column(
    Modifier
        .fillMaxWidth()
        .heightIn(max = 300.dp)
        .padding(4.dp)
        .border(1.dp, Color.Gray, RoundedCornerShape(8.dp))
        .verticalScrollWithScrollbar(
            scrollState,
            scrollbarConfig = ScrollBarConfig(
                padding = PaddingValues(4.dp, 4.dp, 4.dp, 4.dp)
            )
        )
        .padding(2.dp)

) {
    Text(
        text = "Some very long scrollable text",
        color = Color.Gray,
        modifier = Modifier.padding(vertical = 4.dp)
    )
}
Egerton answered 6/7, 2023 at 16:0 Comment(0)
A
2

This is my implementation of a minimally designed vertical scrollbar inspired by this answer:

@Composable
fun Modifier.verticalScrollBar(
    state: ScrollState,
    color: Color = Color.Gray,
    ratio: Float = 3f,
    width: Dp = 8.dp
): Modifier {
    val targetAlpha = if (state.isScrollInProgress) 1f else 0f
    val duration = if (state.isScrollInProgress) 150 else 500

    val alpha by animateFloatAsState(
        targetValue = targetAlpha,
        animationSpec = tween(durationMillis = duration)
    )

    return drawWithContent {
        drawContent()

        val needDrawScrollbar = state.isScrollInProgress || alpha > 0.0f
        val barHeight = (this.size.height / ratio)
        val barRange = (this.size.height - barHeight) / state.maxValue
        if (needDrawScrollbar) {
            val position = state.value * barRange
            drawRect(
                color = color.copy(alpha = alpha),
                topLeft = Offset(this.size.width - width.toPx(), position),
                size = Size(width.toPx(), barHeight)
            )
        }
    }
}

the ratio parameter can be used to change the bar height, i use it in relation to the amount of elements inside the column. To use it just add this modifier to a Column that has the verticalScroll() modifier and pass the same ScrollState to both of them.

Arquit answered 11/3 at 22:55 Comment(0)
A
1

Complementing the Abhijith Shambu answer I want to add that in order to make the indicator bar bigger you should add the desired size to the viewPortLength and the scrollBarSizeWithoutInsets variables in this way:

     val scrollbarSizeWithoutInsets = if (direction == Orientation.Vertical)
            Size(indicatorThicknessPx, (indicatorLength + indicatorSize.toPx()))
        else Size(indicatorLength, indicatorThicknessPx)

val viewPortLength = if (direction == Orientation.Vertical)
            size.height - indicatorSize.toPx() else size.width
Absorptivity answered 21/11, 2023 at 17:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.