🎨

【Android】Composeで横スクロールできる登録画像リストを作成する

2025/01/19に公開

概要

どの様なものかは👇こちらの動画を見て頂ければイメージしやすいと思います。これをAndroid向けにComposeを使って実装していきたいと思います。

image1.gif

機能としては

  • リストを横スクロールできる
  • リストに画像を追加できる
  • リストの画像を削除できる

上記の機能を持つ前提で実装していきます。

動作環境

  • Android Studio Ladybug | 2024.2.1 Patch 2
  • MBA M3 Sonoma 14.6.1
  • kotlin 2.1.0
  • compose-bom 2024.12.01

必要なライブラリ追加

今回は画像表示する部分に CoilAsyncImage を使うのでそちらを追加していきます。

gradle/libs.versions.toml に以下を追加します。

[versions]
coil = "2.7.0"

[libraries]
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }

build.gradle.kts に以下を追加します。

dependencies {
  implementation(libs.coil.compose)
}

シンプルな横スクロール実装

適当にAndroidアプリ用にプロジェクトを作成し HorizontalImageList.kt としてファイルを作成します。まずはシンプルに横スクロール表示を実装していきたいと思います。

@Composable
fun HorizontalImageList(images: List<String>) {
    LazyRow {
        items(images.size, key = { it }) {
            val image = images[it]
            AsyncImage(
                model = image,
                contentDescription = null,
                modifier = Modifier
                    .size(150.dp)
                    .background(Color.Gray),

                )
        }
    }
}

@Preview
@Composable
fun HorizontalImageListPreview() {
    // なるべくランダムな画像になるようにidを連番にしない様にしてます
    val images = List(10) { "https://picsum.photos/id/${it * 10}/150/150" }
    Box(
        Modifier
            .background(Color.White)
            .padding(16.dp)
    ) {
        HorizontalImageList(images)
    }
}

Previewの実行結果が👇になります。(※ Run Previewしてエミュレータで実行しています)

image2.gif

最後に画像追加できるように「+」をリストの最後に表示する

横スクロールの最後に、画像を追加できるような「+」ボタンを設置してみたいと思います。

HorizontalImageList を少し修正します。

@Composable
fun HorizontalImageList(images: List<String>) {
    LazyRow {
        items(images.size + 1, key = { it }) {
            if (images.size > it) {
                val image = images[it]
                AsyncImage(
                    model = image,
                    contentDescription = null,
                    modifier = Modifier
                        .size(150.dp)
                        .background(Color.Gray),

                    )

            } else {
                IconButton(
                    modifier = Modifier
                        .size(150.dp)
                        .background(
                            color = Color.Gray,
                            shape = RoundedCornerShape(8.dp)
                        ),
                    onClick = {}
                ) {
                    Icon(
                        imageVector = Icons.Filled.Add,
                        contentDescription = "Add",
                        tint = Color.White
                    )
                }
            }
        }
    }
}

Previewの実行結果が👇になります。

image3.gif

各画像に削除用のボタンを表示する

さらにHorizontalImageList を修正して、各画像の上に削除用のボタンを設置します。

@Composable
fun HorizontalImageList(images: List<String>) {
    LazyRow {
        items(images.size + 1, key = { it }) {
            if (images.size > it) {
                val image = images[it]
                Box {
                    AsyncImage(
                        model = image,
                        contentDescription = null,
                        modifier = Modifier
                            .size(150.dp)
                            .background(Color.Gray),

                        )
                    // ここからIconButton追加
                    IconButton(
                        modifier = Modifier
                            .align(Alignment.TopEnd)
                            .padding(2.dp)
                            .clip(CircleShape)
                            .size(25.dp, 25.dp)
                            .background(Color.White),
                        onClick = {}
                    ) {
                        Icon(
                            imageVector = Icons.Filled.Close,
                            contentDescription = "Delete",
                            tint = Color.Unspecified
                        )
                    }
                }
            } else {
                IconButton(
                    modifier = Modifier
                        .size(150.dp)
                        .background(
                            color = Color.Gray,
                            shape = RoundedCornerShape(8.dp)
                        ),
                    onClick = {}
                ) {
                    Icon(
                        imageVector = Icons.Filled.Add,
                        contentDescription = "Add",
                        tint = Color.White
                    )
                }
            }
        }
    }
}

Previewの実行結果が👇になります。

image4.gif

完成版

最後に各画像間にスペースを入れたり、画像を角丸にした完成形が以下になります。

@Composable
fun HorizontalImageList(images: List<String>) {
    LazyRow {
        items(images.size + 1, key = { it }) {
            if (images.size > it) {
                val image = images[it]
                Box(modifier = Modifier.padding(end = 6.dp)) { // スペース追加
                    // 各画像を角丸に
                    AsyncImage(
                        model = image,
                        contentDescription = null,
                        contentScale = ContentScale.Crop,
                        modifier = Modifier
                            .size(150.dp)
                            .clip(RoundedCornerShape(8.dp))
                            .background(Color.Gray),

                        )
                    IconButton(
                        modifier = Modifier
                            .align(Alignment.TopEnd)
                            .padding(2.dp)
                            .clip(CircleShape)
                            .size(25.dp, 25.dp)
                            .background(Color.White),
                        onClick = {}
                    ) {
                        Icon(
                            imageVector = Icons.Filled.Close,
                            contentDescription = "Delete",
                            tint = Color.Unspecified
                        )
                    }
                }
            } else {
                IconButton(
                    modifier = Modifier
                        .size(150.dp)
                        .background(
                            color = Color.Gray,
                            shape = RoundedCornerShape(8.dp)
                        ),
                    onClick = {}
                ) {
                    Icon(
                        imageVector = Icons.Filled.Add,
                        contentDescription = "Add",
                        tint = Color.White
                    )
                }
            }
        }
    }
}

Previewの実行結果が👇になります。(冒頭の動画と同じです)

image1.gif

参考URL

https://qiita.com/yasukotelin/items/0cde47d85d58d46017e9

Discussion