LoginSignup
0
1

JetpackComposeのscrollについて

Last updated at Posted at 2023-08-03

はじめに

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

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

scroll 修飾子

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

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

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

Modifier.verticalScroll(rememberScrollState())

scrollable 修飾子

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

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

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

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

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

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

nestedScroll修飾子

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

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

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

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

NestedScrollを構成する要素

NestedScrollの呼び出し方

Modifier.nestedScroll(nestedScrollConnection, nestedScrollDispatcher)

NestedScrollDispatcher

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

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の処理だけを書けばよい。

内部で行われている処理

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

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

発火順

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

scrollableC(スクロール検出)

NestedScrollDipatcherC(onPreScroll)

NestedScrollConnectionB(onPreScroll)

NestedScrollConnectionA(onPreScroll)

scrollableC(スクロール処理)

NestedScrollDipatcherC(onPostScroll)

NestedScrollConnectionB(onPostScroll)

NestedScrollConnectionA(onPostScroll)

イベントの処理順

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

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で縦横両方のスクロール量を取得し反映する必要がある。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1