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?

LazyColumn でアイテムが削除されたときに、アニメーションを行う

Last updated at Posted at 2024-10-19

はじめに

ある日 LazyColumn に表示されているアイテムに対して、特定の操作を行うとアイテムを削除する実装を行う機会がありました。

今回は、自作した Apple 公式サンプル「Handling user input」と同じ仕様の Android アプリを用いて解説します。

Favorites only スイッチを ON にすると、「★」が付いてるアイテムのみを表示します。言い換えると、「★」が付いていないアイテムは削除されます。同様にスイッチを ON にするとアイテムの追加が行われます。

lazy1.gif

この動画では、アイテムの削除と追加、それ以外のアイテムの移動がカットインで行われています。

ここで私は思いました。Jetpack Compose 登場前の RecyclerView による実装ではアニメーションしていました。アニメーションがあることで、ユーザから見たときに「こだわりのあるアプリ」に感じられ、より良い体験につながりそうです。

よってこの記事では LazyColumn でアイテムの削除および追加時にアニメーションを行う実装方法を紹介します。また、RecyclerView の時代はどのように該当アニメーションを実装していたかについても、簡単に紹介します。

Lazy リストのアイテムアニメーションを設定する

公式ドキュメントのアイテム アニメーションにやり方が載っていました。必要な手順は以下の2つだけです。

アイテムにキーが設定されていないと、どの要素が削除または追加されたのか Compose から判別できなくなります。

よって今回のサンプルアプリの改修内容は、このようになります。

LazyColumn(
    modifier =
        Modifier
            .fillMaxSize(),
) {
    // 省略
    items(
        // state は ViewModel より提供された画面の状態
        // landmarks は Lazy リストを構成するためのデータ
        items = state.landmarks,
        key = { landmark ->
            // 【追加】アイテムにキーを設定
            landmark.id
        },
    ) { landmark ->
        // 1アイテム分のデータを表示する Composable 関数
        LandmarkListItem(
            // 【追加】
            modifier = Modifier.animateItem(),
            landmark = landmark,
            onClick = { /* 省略 */},
        )
    }
}
@Composable
fun LandmarkListItem(
    landmark: Landmark,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
) {
    // 呼び出し元から渡された Modifier を適用する
    Column(
        modifier =
            modifier.fillMaxWidth(),
    ) { /* 省略 */ }
}

その結果、アイテムの削除および追加時にアニメーションが行われるようになりました。

lazy2.gif

animateItem Modifier が追加されたのは比較的最近

意外にも animateItem Modifier が追加されたのは Compose 1.7.0 (2024年9月4日)、アルファ版を含めても Compose 1.7.0-alpha06(2024年4月3日)と、比較的最近のアップデートでした。

以前の animateItemPlacement Modifier

バージョン 1.7.0-alpha06 以前では animateItemPlacement Modifier が似たようなアニメーションを提供していました。しかし、これは削除および追加されるアイテム以外の移動にのみ対応しており、削除および追加されるアイテムについてはカットインとなっていたため、物足りないアニメーションに感じられました。

lazy3.gif

そのことは、こちらの記事でも紹介されています。

RecyclerView 時代のアイテム削除および追加のアニメーション実装方法

冒頭で Jetpack Compose 登場前の RecyclerView による実装ではアニメーションが適用されていたと述べましたが、ここではその具体的な実装方法について簡単に紹介します。

まず RecyclerView は、RecyclerView.Adapter, ViewHolder, ListAdapter, DiffUtil.ItemCallback など多くのクラスが登場し、難解な部分が多いです。そのため、GroupieEpoxy といった直感的に使える補助ライブラリがよく使われていました。

Groupie では1アイテムに相当する View を提供するためのクラスを BindableItem クラスを継承して作成します。例として、4年前の Android Advent Calendar 向けに作成したこの記事のために作成したコードを以下に紹介します。

// メッセージアプリの1メッセージを表すクラス
data class Message(
    val id: Long,
    val name: String,
    @DrawableRes val iconResId: Int,
    val time: Long,
    val body: String
)
class MessageBindableItem(private val message: Message) : BindableItem<ListItemMessageBinding>() {

    private val sdf = SimpleDateFormat("HH:mm", Locale.JAPAN)

    override fun bind(viewBinding: ListItemMessageBinding, position: Int) {
        // コンストラクタで渡された message から View を設定する
        viewBinding.icon.setImageResource(message.iconResId)
        viewBinding.name.text = message.name
        viewBinding.time.text = sdf.format(Date(message.time))
        viewBinding.body.text = message.body
    }

    // 1アイテム分の XML レイアウト
    override fun getLayout() = R.layout.list_item_message

    override fun initializeViewBinding(view: View): ListItemMessageBinding {
        return ListItemMessageBinding.bind(view)
    }

    override fun isSameAs(other: Item<*>): Boolean {
        // Item クラスは BindableItem クラスの親クラス
        // 別の Item である other と自分を比較して
        // 同じものを表している場合は true を返却する
        return if (other is MessageBindableItem) {
            message.id == other.message.id
        } else {
            false
        }
    }

    override fun hasSameContentAs(other: Item<*>): Boolean {
        // さらにコンテンツも同じ場合はこちらで true を返却する
        // ItemAnimator の設定次第で、コンテンツの変更に伴うアニメーションもできる
        return if (other is MessageBindableItem) {
            message == other.message
        } else {
            false
        }
    }
}

重要なのは isSameAs メソッドの実装です。Compose のアイテムのキーに相当する部分です。これを適切に実装することで、リストが更新されたとき、削除や追加されたアイテムがあるときは、自動でアニメーションが行われます。1

リストの更新は GroupieAdapter.update メソッドに BindableItem のリストを渡すことで行えます。

val adapter = GroupieAdapter()
recyclerView.adapter = adapter
// ViewModel が管理する画面の状態 state を更新監視している。
// 現代だと StateFlow で使うところがだが、当時は LiveData を使っていた。
viewModel.state.observe(this) { state ->
    val items = state.messages.map { message ->
        MessageBindableItem(message)
    }
    adapter.update(items)
}

まとめ

LazyColumn や LazyRow でアイテムが削除または追加されたときには、自然なアニメーションが欲しいところです。以下の手順で、自然なアニメーションを実装することができます。

また Compose 登場前から活動している Android エンジニアが、このような実装が無いと、ユーザ体験的にもコード的にも物足りなく感じる理由として、Compose 登場前の RecyclerView における同様のアニメーションの実装方法を簡単に紹介しました。

  1. デフォルトで DefaultItemAnimator が使われます

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?