0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

1. この記事の目的

チャットアプリのように、LazyColumnの最下段アイテムを
IME(ソフトウェアキーボード)の開閉に合わせて自然に追従させたい。

この挙動を実現する方法として、offset方式scroll方式 の2種類を比較した。

2. 方式1:offset方式

▼ビュー全体を持ち上げるシンプルな方法

IMEのbottom insetを取得して、その分だけビューを持ち上げる方法。
LazyColumn全体とbottomBarを同じ量だけoffsetするだけなので、実装はかなり簡単。

val imeInsetPx = WindowInsets.ime
    .exclude(WindowInsets.systemBars).getBottom(density)
val imeInsetDp = with(density) { imeInsetPx.toDp() }

bottomBar側はこう動かす:

bottomBar = {
    Box(
        Modifier
            .fillMaxWidth()
            .navigationBarsPadding()
            .offset(y = -imeInsetDp) // レイアウト層ではないので
                                     // innerPaddingに影響しない
    ) {
        InputBar { text -> messages.add(text) }
    }
}

LazyColumn側も同じようにoffsetする:

LazyColumn(
    modifier = Modifier
        .fillMaxSize()
        .padding(innerPadding)
        .offset(y = -imeInsetDp),
    state = listState
) {
    items(messages) { msg ->
        MessageBubble(text = msg)
    }
}

:star: ビュー全体を持ち上げるだけなので、実装は簡単。

3. 方式2:scroll方式

▼IMEが押し上げた分だけLazyColumnへそのまま反映させる方法

IMEの表示・非表示で描画領域が押し上げられた量(inset bottomの変化量)を
LazyColumnのスクロール量としてそのまま流し込む方法。
端末差やinsetの仕様により誤差が発生しやすいため、補正を入れながら動作させる。

AndroidView(
    modifier = Modifier
        .fillMaxSize()
        .alpha(0f), // 完全透過(当たり判定なし)
    factory = { context ->
        View(context).apply {
            ViewCompat.setWindowInsetsAnimationCallback(
                this,
                object: WindowInsetsAnimationCompat.Callback(
                    DISPATCH_MODE_CONTINUE_ON_SUBTREE
                ) {
                    override fun onPrepare(
                        animation: WindowInsetsAnimationCompat
                    ) {
                        super.onPrepare(animation)
                        // 初期化(前回のfractionやinsetのリセット)
                    }

                    override fun onProgress(
                        insets: WindowInsetsCompat,
                        runningAnimations: List<WindowInsetsAnimationCompat>
                    ): WindowInsetsCompat {
                        // inset bottomの変化量(delta)を計算して
                        // LazyColumnへdispatchRawDelta()として流す
                        return insets
                    }

                    override fun onEnd(animation: WindowInsetsAnimationCompat) {
                        super.onEnd(animation)
                        // 最終フレームの補正
                    }
                }
            )
        }
    }
)

:star: 描画遅延を避けるため、AndroidViewで全ての処理を行う。

4. 比較まとめ

offset方式 scroll方式
実装難易度 ★☆☆ 簡単 ★★★ 難しい
端末差の影響 ほぼ受けない
(Composeが保証)
受けやすい
(補正必須)
誤差の出やすさ 出ない
(毎フレーム最新値)
出る
(currentInsetの更新タイミング差)
方式の性質 最新の inset を
そのまま使う方式
押し上げ量の差分(delta)を
そのままスクロール量に使う方式
総評 手軽で安定。まずはこれ。 差分方式で正確だが扱いが難しい。
補正必須。

:star: 難易度:offset方式 <<<<<(超えられない壁)<<<<< scroll方式

5. まとめ

IME追従には offset方式scroll方式 の2つがある。

  • offset方式は最新のinsetをそのまま使うシンプルな方法で、
    実装が簡単で安定している。
  • scroll方式は押し上げ量の差分(delta)を使う方式で、
    正確だが端末差の影響を受けやすく、補正が必要になる。

まずは offset方式 を使うのが無難。
scroll方式は、より細かい制御が必要な場合に選択する。

scroll方式は試行錯誤が多いので、実装するときは覚悟して挑んでください。
俺はもう だ め ぽ :tired_face:

0
0
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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?