鯰の住処

Namazuが真面目な話を書きます

Jetpack ComposeのNestedScrollによるスクロール幅を自由に制御する

Jetpack ComposeのLazyListやLazyColumnはNestedScrollがデフォルトで有効になっています。 ところが、部分的に無効にしたい場合やスクロールイベントを要素のスクロール以外に使いたい場合もあり得ます。 このブログではNestedScrollの挙動を解説しつつ、実現したい挙動とその実現方法を逆引き式に紹介します。

NestedScrollを構成する二つの要素

NestedScrollの実現において、NestedScrollに含まれるComposableが必ず持つ必要がある重要な要素は以下の二つです。

  • NestedScrollDispatcher
  • NestedScrollConnection

NestedScrollDispatcherはユーザーがスクロールをしようと試みたComposable、すなわちスクロールイベントを受け取った(NestedScrollで最も子供にあたる)要素が用いるもので、NestedScrollイベントを発火します。このクラスはfinalで定義されており、継承による挙動の変更などは出来ません。(する必要もないはずです)

NestedScrollConnectionはNestedScrollDispatcherによって発火されたスクロールイベントを親へと伝えていく役割を果たします。interfaceとして定義されており、スクロールの前後やフリングの前後に呼ばれるメソッドを実装することが出来ます。

今回重要になるのはNestedScrollConnectionです。

NestedScrollConnectionの各メソッドとその役割

NestedScrollConnectionは以下の4つのメソッドを持っています。

  • onPreScroll
  • onPostScroll
  • onPreFling
  • onPostFling

onPreScroll

    /**
     * Pre scroll event chain. Called by children to allow parents to consume a portion of a drag
     * event beforehand
     *
     * @param available the delta available to consume for pre scroll
     * @param source the source of the scroll event
     *
     * @see NestedScrollSource
     *
     * @return the amount this connection consumed
     */
    fun onPreScroll(available: Offset, source: NestedScrollSource): Offset = Offset.Zero

onPreScrollはスクロールを行う(実際にタップした要素を移動させる)前に呼ばれるメソッドで、スクロール幅(available)を用いて何らかの処理を行うことが出来ます。 返り値はonPreScroll関数によって消費されたスクロール幅となっています。デフォルト実装では何もせずスクロール幅も消費しないという意味で、Offset.Zeroがただ返されるだけの関数となっています。

onPostScroll

    /**
     * Post scroll event pass. This pass occurs when the dispatching (scrolling) descendant made
     * their consumption and notifies ancestors with what's left for them to consume.
     *
     * @param consumed the amount that was consumed by all nested scroll nodes below the hierarchy
     * @param available the amount of delta available for this connection to consume
     * @param source source of the scroll
     *
     * @see NestedScrollSource
     *
     * @return the amount that was consumed by this connection
     */
    fun onPostScroll(
        consumed: Offset,
        available: Offset,
        source: NestedScrollSource
    ): Offset = Offset.Zero

onPostScrollはスクロールを行った後に呼ばれるメソッドで、この要素とその子孫要素により消費されたスクロール幅(consumed)と残されたスクロール幅(available)を用いて何らかの処理を行うことが出来ます。 返り値は親のonPostScroll関数がavailableとして受け取ることのできるスクロール幅となっています。デフォルト実装では何もせずスクロール幅も消費しないという意味で、Offset.Zeroがただ返されるだけの関数となっています。

onPreFling

    /**
     * Pre fling event chain. Called by children when they are about to perform fling to
     * allow parents to intercept and consume part of the initial velocity
     *
     * @param available the velocity which is available to pre consume and with which the child
     * is about to fling
     *
     * @return the amount this connection wants to consume and take from the child
     */
    suspend fun onPreFling(available: Velocity): Velocity = Velocity.Zero

onPreFlingはフリング(スクロールの途中で指を離した後、速度を徐々に落としながらスクロールを継続する動作)を始める前に呼ばれます。 親要素が受け取ることが可能なフリング速度(available)を受け取り、onPreFlingで消費された速度を返り値として返します。 デフォルト実装では何もせず速度も消費しないという意味で、Velocity.Zeroがただ返されるだけの関数となっています。

onPostFling

    /**
     * Post fling event chain. Called by the child when it is finished flinging (and sending
     * [onPreScroll] & [onPostScroll] events)
     *
     * @param consumed the amount of velocity consumed by the child
     * @param available the amount of velocity left for a parent to fling after the child (if
     * desired)
     * @return the amount of velocity consumed by the fling operation in this connection
     */
    suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
        return Velocity.Zero
    }

onPostFlingはフリングを終了する際に呼ばれます。 この要素とその子孫要素により消費されたフリング速度(consumed)と親要素のために残されたフリング速度(available)を受け取り、親がavailableとして受け取ることが可能な速度を返り値として返します。 デフォルト実装では何もせず速度も消費しないという意味で、Velocity.Zeroがただ返されるだけの関数となっています。

NestedScrollのカスタマイズとその実装

サンプルとして、LazyColumnの中に3つのLazyColumnが含まれているリストを作成しました。デフォルトの挙動は以下の通りです。

親がNestedScrollされないようにしたい

親のスクロールイベントは**発火されます。 そのため、以下のようにこの要素のonPostScrollで全てのスクロール幅を消費することにより、親が消費可能なスクロール幅がないと伝えれば良いです。

override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset = available

Flingを無効化したい

Flingが実行される前に、自身が受け取ることが出来るフリング速度を0にすればよいです。 すなわち、以下のようにonPreFlingで全てのフリング速度を消費したことにすれば実現できます。

override suspend fun onPreFling(available: Velocity): Velocity = available

折り畳みツールバーと連携させたい

折り畳みツールバーの挙動は、スクロールに連動してツールバーの高さを変えます。 この場合はonPrescroll内でツールバーの高さを変更する処理を記述しつつスクロール幅は消費しなければ期待する挙動が実現できます。 この例は以下の通りandroid developers公式のページで紹介されているものです。

developer.android.com

追記

サンプルコード上げておきました

gist.github.com