[Jetpack Compose] LazyListStateに連動したScrollBarの実装

[\[Jetpack Compose\] scrollStateに連動したScrollBarの実装](https://qiita.com/yasukotelin/items/fcf5b538fac922cb08a5) の記事にあるScrollBarを元に LazyListState に対応した ScrollBar を作ってみました。

https://qiita.com/yasukotelin/items/fcf5b538fac922cb08a5

# 動き

![Screen_recording_20240330_165405 縦スクロールバーの実装_1_4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39221/0fe97b1a-1c72-fa38-f748-b6d18b619f0d.gif)


# ScrollBar.kt

```kotlin
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay

private val scrollBarWidth = 4.dp

/**
 * ScrollStateに連動したスクロールバーを表示するComposable
 *
 * https://qiita.com/yasukotelin/items/fcf5b538fac922cb08a5 を参考に作成
 *
 * 親のBoxの右端に表示されます。高さはコンテンツ量に応じて可変します。
 * スクロールできるだけのコンテンツがない場合は表示されません。
 *
 * @param isAlwaysShowScrollBar trueの場合はスクロールバーを常に表示します。
 * falseの場合はスクロール中のみ表示します。
 */
@Composable
fun BoxScope.ScrollBar(
    modifier: Modifier = Modifier,
    listState: LazyListState,
    isAlwaysShowScrollBar: Boolean = false,
) {
    var isVisible by remember { mutableStateOf(isAlwaysShowScrollBar) }

    LaunchedEffect(isAlwaysShowScrollBar, listState.isScrollInProgress) {
        isVisible = if (isAlwaysShowScrollBar || listState.isScrollInProgress) {
            true
        } else {
            delay(800) // スクロールが止まってから800ms後に非表示にする
            false
        }
    }

    AnimatedVisibility(
        visible = isVisible,
        enter = fadeIn(),
        exit = fadeOut(),
    ) {
        Canvas(
            modifier = modifier
                .align(Alignment.CenterEnd)
                .fillMaxSize()
        ) {
            val viewHeight = size.height
            val totalCount = listState.layoutInfo.totalItemsCount
            if (totalCount == 0) return@Canvas

            val firstVisibleItemIndex = listState.firstVisibleItemIndex
            val firstVisibleItemScrollOffset = listState.firstVisibleItemScrollOffset
            val visibleItemCount = listState.layoutInfo.visibleItemsInfo.size

            val scrollRatio = firstVisibleItemIndex.toFloat() / totalCount

            // スクロールバーの位置とサイズを計算
            val scrollbarHeight = viewHeight * (visibleItemCount.toFloat() / totalCount)
            val scrollbarTopY1 = scrollRatio * viewHeight

            // 次のアイテムの位置とサイズを計算
            val scrollRatio2 = (firstVisibleItemIndex + 1).toFloat() / totalCount
            val scrollbarTopY2 = scrollRatio2 * viewHeight


            // 表示中の先頭アイテムの高さ
            val firstVisibleItemHeight = listState.layoutInfo.visibleItemsInfo.getOrNull(0)?.size

            // スクロールバー位置の微調整(スクロール量をスクロールバーのoffsetに変換する。offsetの範囲はこのアイテムと次のアイテムのスクロールバーの位置)
            val scrollbarTopOffset = if (firstVisibleItemHeight == null || firstVisibleItemHeight == 0) {
                // 先頭アイテムの高さが不明なので微調整なし
                0f
            } else {
                firstVisibleItemScrollOffset.toFloat() / firstVisibleItemHeight * (scrollbarTopY2 - scrollbarTopY1)
            }

            drawRect(
                color = Color.Gray,
                topLeft = Offset(size.width - scrollBarWidth.toPx(), scrollbarTopY1 + scrollbarTopOffset),
                size = Size(scrollBarWidth.toPx(), scrollbarHeight)
            )
        }
    }
}
```

# 使い方など

元の記事と同じような感じで LazyColumn と同列に Box で配置してください。

```kotlin
val listState = rememberLazyListState()

Box() {
    LazyColumn(
        state = listState,
    ) {
        ...
    }

    ScrollBar(
        listState = listState,
        isAlwaysShowScrollBar = false
    )
}
```