JetpackComposeのscrollについて

# はじめに

Jetpack Composeのscrollについて調べた。

Jetpackでスクロールする場合、2種類の修飾子が利用できる

# scroll 修飾子
https://developer.android.com/jetpack/compose/gestures?hl=ja#scroll-modifiers

verticalScroll 修飾子と horizontalScroll 修飾子の2種類があり、要素の中身が最大サイズを超えた場合にスクロールする。

実装は簡単だが、拡張性はあまりない。

内部でScrollable修飾子が使われている。

```kotlin
Modifier.verticalScroll(rememberScrollState())
```

# scrollable 修飾子
https://developer.android.com/jetpack/compose/gestures?hl=ja#scrollable-modifier

scrollableはscroll修飾子と違い、スクロールしても自動で要素のオフセットを動かしてくれない。

スクロールのデルタ値を取得し、その値を元に自ら処理を書く必要がある。

```kotlin
Modifier
	.scrollable(
		  orientation = Orientation.Vertical,
		  state = rememberScrollableState { delta ->
		      // ここでスクロール処理
		      delta
		  }
	) 
```

Scrollableは内部的にDraggable修飾子を使っている。

同じOrientationのDraggableがネストすると子にイベントを吸われる。

その為、スクロールをネストさせたい場合はNestedScrollを用いる必要がある。



# nestedScroll修飾子

スクロールをネストさせるためのもの。

スクロールの中にスクロールを入れる感じ

基本的には子が先にスクロールを消費し、消費しきれなかった量を親に伝え、親は伝わってきた量だけスクロールする。

上記のverticalScroll、horizontalScroll、scrollableに関しては内部でnestedScrollが実装されており、単に入れ子にするだけでスクロールのネストが可能。

https://developer.android.com/jetpack/compose/gestures?hl=ja#auto-nested-scrolling

### NestedScrollを構成する要素

NestedScrollの呼び出し方

```kotlin
Modifier.nestedScroll(nestedScrollConnection, nestedScrollDispatcher)
```

**NestedScrollDispatcher**

NestedScrollを動かす大元になるもの
scrollableではネストされている一番下の子がNestedScrollDispatcherを発火させ、NestedScrollDispatcherが親のNestedScrollConnectionを発火させる実装になっている。

```kotlin
class NestedScrollDispatcher {

    // ---中略---
    
    fun dispatchPreScroll(available: Offset, source: NestedScrollSource): Offset {
        return parent?.onPreScroll(available, source) ?: Offset.Zero
    }

    fun dispatchPostScroll(
        consumed: Offset,
        available: Offset,
        source: NestedScrollSource
    ): Offset {
        return parent?.onPostScroll(consumed, available, source) ?: Offset.Zero
    }

    suspend fun dispatchPreFling(available: Velocity): Velocity {
        return parent?.onPreFling(available) ?: Velocity.Zero
    }

    suspend fun dispatchPostFling(consumed: Velocity, available: Velocity): Velocity {
        return parent?.onPostFling(consumed, available) ?: Velocity.Zero
    }
}
```

**NestedScrollConnection**

NestedScrollの親と子をつなぐもの
scrollableにおいては子のNestedScrollDispatcherまたはNestedScrollConnectionから発火され、親のNestedScrollConnectionを発火する。

実際には親の発火処理などはnestedScrollの内部で行われているので、nestedScrollに渡すnestedScrollConnectionでは自身のflingとscrollの処理だけを書けばよい。

内部で行われている処理

```kotlin
internal class NestedScrollModifierLocal(
    val dispatcher: NestedScrollDispatcher,
    val connection: NestedScrollConnection
) : ModifierLocalConsumer, ModifierLocalProvider<NestedScrollModifierLocal?>,
    NestedScrollConnection {
    
    // ---中略---

    override fun onPreScroll(
        available: Offset,
        source: NestedScrollSource
    ): Offset {
        val parentPreConsumed = parent?.onPreScroll(available, source) ?: Offset.Zero
        val selfPreConsumed = connection.onPreScroll(available - parentPreConsumed, source)
        return parentPreConsumed + selfPreConsumed
    }

    override fun onPostScroll(
        consumed: Offset,
        available: Offset,
        source: NestedScrollSource
    ): Offset {
        val selfConsumed = connection.onPostScroll(consumed, available, source)
        val parentConsumed =
            parent?.onPostScroll(consumed + selfConsumed, available - selfConsumed, source)
                ?: Offset.Zero
        return selfConsumed + parentConsumed
    }

    override suspend fun onPreFling(available: Velocity): Velocity {
        val parentPreConsumed = parent?.onPreFling(available) ?: Velocity.Zero
        val selfPreConsumed = connection.onPreFling(available - parentPreConsumed)
        return parentPreConsumed + selfPreConsumed
    }

    override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
        val selfConsumed = connection.onPostFling(consumed, available)
        val parentConsumed =
            parent?.onPostFling(consumed + selfConsumed, available - selfConsumed) ?: Velocity.Zero
        return selfConsumed + parentConsumed
    }
}
```

実装してnestedScrollに渡すNestedScrollConnection

```kotlin
val connection = object : NestedScrollConnection {

		override suspend fun onPreFling(available: Velocity): Velocity {
		    // Fling処理
		    // 子要素よりも先に呼ばれる
		    return Velocity.Zero // 消費量
		}
		
		override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
		    // Fling処理
		    // 子要素よりも後に呼ばれる
		    return Velocity.Zero // 消費量
		}
		
		
		override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
		    // Scroll処理
		    // 子要素よりも先に呼ばれる
		    return Offset.Zero // 消費量
		}
		
		override fun onPostScroll(
		    consumed: Offset,
		    available: Offset,
		    source: NestedScrollSource
		): Offset {
		    // Scroll処理
		    // 子要素よりも後に呼ばれる
		    return Offset.Zero // 消費量
		}
}
```

下記のような3つのComposable A-B-C（Aが一番上の親） が存在し、それぞれにscrollable修飾子をつけた場合、下記のような順でスクロールが発火される

![nest.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/914937/085f74d6-fd8c-130d-3835-1b693d8d2fd4.png)


### 発火順
scrollableがスクロールを検知すると、NestedScrollDipatcherを発火し、それが親に伝播していく。

scrollableC（スクロール検出）
↓
NestedScrollDipatcherC（onPreScroll)
↓
NestedScrollConnectionB（onPreScroll)
↓
NestedScrollConnectionA（onPreScroll)
↓
scrollableC（スクロール処理）
↓
NestedScrollDipatcherC（onPostScroll)
↓
NestedScrollConnectionB（onPostScroll)
↓
NestedScrollConnectionA（onPostScroll)

### イベントの処理順

内部的には下記のように親のonPreScrollを先に処理したりしているので実際の処理順は変わって来る。

```kotlin
override fun onPreScroll(
    available: Offset,
    source: NestedScrollSource
): Offset {
    val parentPreConsumed = parent?.onPreScroll(available, source) ?: Offset.Zero
    val selfPreConsumed = connection.onPreScroll(available - parentPreConsumed, source)
    return parentPreConsumed + selfPreConsumed
}
```

PreScrollA
↓
PreScrollB
↓
scrollableC（スクロール処理）
↓
PostScrollB
↓
PostScrollA

### Orientation

scrollableは方向を持っており、縦横両方のスクロール量を取得できない。
縦横自由にスクロールさせる場合は、scrollableを使わずにdraggableで縦横両方のスクロール量を取得し反映する必要がある。

https://developer.android.com/jetpack/compose/gestures?hl=ja#dragging