$30 off During Our Annual Pro Sale. View Details »

[shibuya.apk #41] Jetpack Composeでグリッドに柔軟にスペースを入れたい

syarihu
April 21, 2023

[shibuya.apk #41] Jetpack Composeでグリッドに柔軟にスペースを入れたい

syarihu

April 21, 2023
Tweet

More Decks by syarihu

Other Decks in Technology

Transcript

  1. Jetpack Composeでグリッドに柔
    軟にスペースを入れたい
    shibuya.apk #41
    2023/04/21 (Fri.)
    Taichi Sato / @syarihu
    Giftmall, Inc.
    Android Engineer

    View Slide

  2. Taichi Sato (@syarihu)
    ● Giftmall, Inc. (LUCHE GROUP)
    ○ Android App Engineer

    View Slide

  3. shibuya.apkは3年ぶり9回目の発表みたいです

    View Slide

  4. 今回実現したい
    レイアウト

    View Slide

  5. 今回実現したい
    レイアウト
    ● あるアイテムではスペー
    スなしで1行で表示したい

    View Slide

  6. 今回実現したい
    レイアウト
    ● あるアイテムではスペー
    スなしで1行で表示したい
    ● あるアイテムでは1行に3
    列表示するグリッドを作
    成し、そのグリッド内では
    スペースを入れたい

    View Slide

  7. 一見簡単そうに見えるが果たして…

    View Slide

  8. LazyVerticalGridの標準機能で
    頑張ってみる

    View Slide

  9. スペースを入れない通常のレイアウトを作成する
    LazyVerticalGrid(
    modifier = Modifier.fillMaxSize(),
    columns = GridCells.Fixed(count = GRID_COLUMN_SIZE)
    ) {

    View Slide

  10. スペースを入れない通常のレイアウトを作成する
    LazyVerticalGrid(...) {
    item(span = { GridItemSpan(GRID_COLUMN_SIZE) }) {
    Box(
    modifier = Modifier
    .fillMaxWidth()
    .height(100.dp)
    .background(Color.DarkGray),
    contentAlignment = Alignment.Center
    ) {
    Text(text = "Header", color = Color.White)
    }
    }

    View Slide

  11. スペースを入れない通常のレイアウトを作成する
    LazyVerticalGrid(...) {

    items(items = items, span = { GridItemSpan(1) }) {
    Column(
    horizontalAlignment = Alignment.CenterHorizontally,
    modifier = Modifier.fillMaxWidth()
    .background(color = Color.LightGray)
    .padding(8.dp)
    ) {
    Image(painter = painterResource(R.drawable.ic_launcher_foreground), null)
    Text(text = it)
    }
    }

    View Slide

  12. スペースを入れない通常のレイアウトを作成する
    LazyVerticalGrid(...) {

    item(span = { GridItemSpan(GRID_COLUMN_SIZE) }) {
    Box(
    modifier = Modifier
    .fillMaxWidth()
    .height(100.dp)
    .background(Color.DarkGray),
    contentAlignment = Alignment.Center
    ) {
    Text(text = "Footer", color = Color.White)
    }
    }

    View Slide

  13. スペースを入れない
    通常のレイアウト
    これをベースにして、グリッ
    ド間にスペースを入れられ
    ないか考えてみる

    View Slide

  14. contentPaddingを使う

    View Slide

  15. contentPaddingを使う
    LazyVerticalGrid(

    contentPadding = PaddingValues(8.dp)
    ) {

    View Slide

  16. 実行結果

    View Slide

  17. 実行結果
    LazyVerticalGridの上下左
    右にpaddingが設定される

    View Slide

  18. verticalArrangementや
    horizontalArrangementでspacedByを
    使ってスペースを設定する

    View Slide

  19. spacedByを使ってスペースを設定する
    LazyVerticalGrid(

    verticalArrangement = Arrangement.spacedBy(8.dp),
    horizontalArrangement = Arrangement.spacedBy(8.dp)
    ) {

    View Slide

  20. 実行結果

    View Slide

  21. 実行結果
    ● 左端と右端のアイテムに
    スペースが入らない

    View Slide

  22. 実行結果
    ● 左端と右端のアイテムに
    スペースが入らない
    ● すべてのアイテムにス
    ペースが適用されるた
    め、意図しないところにま
    でスペースが入ることが
    ある

    View Slide

  23. アイテムごとにpaddingを設定する

    View Slide

  24. アイテムごとにpaddingを設定する
    items(
    items = items,
    span = { GridItemSpan(1) },
    ) {
    Column(
    horizontalAlignment = Alignment.CenterHorizontally,
    modifier = Modifier
    .fillMaxWidth()
    .background(color = Color.LightGray)
    .padding(8.dp)
    ) {

    View Slide

  25. アイテムごとにpaddingを設定する
    items(
    items = items,
    span = { GridItemSpan(1) },
    ) {
    Column(
    horizontalAlignment = Alignment.CenterHorizontally,
    modifier = Modifier
    .fillMaxWidth()
    .background(color = Color.LightGray)
    .padding(8.dp)
    ) {

    View Slide

  26. アイテムごとにpaddingを設定する
    items(
    items = items,
    span = { GridItemSpan(1) },
    ) {
    Column(
    horizontalAlignment = Alignment.CenterHorizontally,
    modifier = Modifier
    .fillMaxWidth()
    .padding(8.dp) // <- 背景色の前にpaddingを入れる
    .background(color = Color.LightGray)
    .padding(8.dp)
    ) {

    View Slide

  27. アイテムごとにpaddingを設定する
    items(
    items = items,
    span = { GridItemSpan(1) },
    ) {
    Column(
    horizontalAlignment = Alignment.CenterHorizontally,
    modifier = Modifier
    .fillMaxWidth()
    .padding(8.dp) // <- 背景色の前にpaddingを入れる
    .background(color = Color.LightGray)
    .padding(8.dp)
    ) {

    View Slide

  28. 実行結果
    ● 1つのアイテムの上下左
    右にpaddingが設定され
    ているため、アイテムとア
    イテムの間のpaddingが
    大きくなる

    View Slide

  29. アイテムによってpaddingを動的に変える

    View Slide

  30. アイテムによって
    paddingを動的に変
    える
    ● 左端と右端だけ8dpにす

    View Slide

  31. アイテムによって
    paddingを動的に変
    える
    ● 左端と右端だけ8dpにす

    ● アイテム間は4dpにする

    View Slide

  32. アイテムによってpaddingを動的に変える
    private const val GRID_COLUMN_SIZE = 3
    val startIndexes = (1..items.size)
    .map { GRID_COLUMN_SIZE * it - (GRID_COLUMN_SIZE - 1) }
    .toList()
    val endIndexes = (1..items.size)
    .map { GRID_COLUMN_SIZE * it }
    .toList()

    View Slide

  33. アイテムによってpaddingを動的に変える
    itemsIndexed(...) { index, item ->
    Column(
    horizontalAlignment = Alignment.CenterHorizontally,
    modifier = Modifier
    .fillMaxWidth()
    .padding(
    top = if (index < GRID_COLUMN_SIZE) 8.dp else 4.dp,
    bottom = if (index > items.size - GRID_COLUMN_SIZE + 1) 8.dp else 4.dp,
    start = if (index == 0 || startIndexes.contains(index + 1)) 8.dp else 4.dp,
    end = if (endIndexes.contains(index + 1)) 8.dp else 4.dp,
    )
    ...

    View Slide

  34. アイテムによってpaddingを動的に変える
    itemsIndexed(...) { index, item ->
    Column(
    horizontalAlignment = Alignment.CenterHorizontally,
    modifier = Modifier
    .fillMaxWidth()
    .padding(
    top = if (index < GRID_COLUMN_SIZE) 8.dp else 4.dp,
    bottom = if (index > items.size - GRID_COLUMN_SIZE + 1) 8.dp else 4.dp,
    start = if (index == 0 || startIndexes.contains(index + 1)) 8.dp else 4.dp,
    end = if (endIndexes.contains(index + 1)) 8.dp else 4.dp,
    )
    ...

    View Slide

  35. アイテムによってpaddingを動的に変える
    itemsIndexed(...) { index, item ->
    Column(
    horizontalAlignment = Alignment.CenterHorizontally,
    modifier = Modifier
    .fillMaxWidth()
    .padding(
    top = if (index < GRID_COLUMN_SIZE) 8.dp else 4.dp,
    bottom = if (index > items.size - GRID_COLUMN_SIZE + 1) 8.dp else 4.dp,
    start = if (index == 0 || startIndexes.contains(index + 1)) 8.dp else 4.dp,
    end = if (endIndexes.contains(index + 1)) 8.dp else 4.dp,
    )
    ...

    View Slide

  36. アイテムによってpaddingを動的に変える
    itemsIndexed(...) { index, item ->
    Column(
    horizontalAlignment = Alignment.CenterHorizontally,
    modifier = Modifier
    .fillMaxWidth()
    .padding(
    top = if (index < GRID_COLUMN_SIZE) 8.dp else 4.dp,
    bottom = if (index > items.size - GRID_COLUMN_SIZE + 1) 8.dp else 4.dp,
    start = if (index == 0 || startIndexes.contains(index + 1)) 8.dp else 4.dp,
    end = if (endIndexes.contains(index + 1)) 8.dp else 4.dp,
    )
    ...

    View Slide

  37. 実行結果
    ● 一見うまくいったように見
    えるが…

    View Slide

  38. 実行結果
    ● 一見うまくいったように見
    えるが…
    ● 画面幅を変えるとズレる

    View Slide

  39. 実行結果
    ● 一見うまくいったように見
    えるが…
    ● 画面幅を変えるとズレる
    ● 列数を変えてもズレる

    View Slide

  40. アイテムによってpaddingを動的に変える
    ● verticalArrangementやhorizontalArrangementで
    spacedByを使ってスペースを設定した上で左端と右端に動
    的にpaddingを設定した場合も、アイテムサイズのズレが発
    生する

    View Slide

  41. LazyVerticalGridの標準機能では
    実現は難しい…🤔

    View Slide

  42. ならば自分で作ろう💡

    View Slide

  43. 1行ごとにRowを生成し、
    自前でグリッドを作ってみる

    View Slide

  44. 自前でグリッドを作ってみる
    ● グリッド表示したいアイテムを1行ごとの塊に分割する
    ● そのアイテムの塊を自分でRowを使ってグリッドを表現する

    View Slide

  45. 自前でグリッドを作ってみる
    ● Rowを使えばRowにpaddingを設定し、自分で列と列の間
    にSpacerを追加できる
    ● これならアイテムがずれることがないため、期待通りのレイ
    アウトが実現できると考えた

    View Slide

  46. 自前でグリッドを作ってみる
    const val chunkSize = 3
    val items = buildList { repeat(7) { add("item$it") } }
    val chunkedItems = items.chunked(chunkSize)

    View Slide

  47. 自前でグリッドを作ってみる
    const val chunkSize = 3
    val items = buildList { repeat(7) { add("item$it") } }
    val chunkedItems = items.chunked(chunkSize)

    View Slide

  48. 自前でグリッドを作ってみる
    items(
    items = chunkedItems,
    span = { GridItemSpan(chunkSize) },
    ) { chunkedItems ->

    View Slide

  49. 自前でグリッドを作ってみる
    items(...) { chunkedItems ->
    Row(
    Modifier.fillMaxWidth()
    // Rowの左右にpaddingを入れる。
    // 上下でpaddingが重ならないように上下ではtopのみにpaddingを入れる
    .padding(top = 8.dp, start = 8.dp, end = 8.dp),
    ) {

    View Slide

  50. 自前でグリッドを作ってみる
    items(...) { chunkedItems ->
    Row(...) {
    chunkedItems.forEachIndexed { index, item ->
    Box(modifier = Modifier.fillMaxWidth().weight(1f)) {
    itemContent(item)
    }
    if (index < chunkedItems.lastIndex || chunkedItems.size < chunkSize) {
    Spacer(modifier = Modifier.size(rowSpace))
    }
    }

    View Slide

  51. 自前でグリッドを作ってみる
    items(...) { chunkedItems ->
    Row(...) {
    chunkedItems.forEachIndexed { index, item ->
    Box(modifier = Modifier.fillMaxWidth().weight(1f)) {
    itemContent(item)
    }
    if (index < chunkedItems.lastIndex || chunkedItems.size < chunkSize) {
    Spacer(modifier = Modifier.size(rowSpace))
    }
    }

    View Slide

  52. 自前でグリッドを作ってみる
    items(...) { chunkedItems ->
    Row(...) {
    chunkedItems.forEachIndexed { index, item ->
    Box(modifier = Modifier.fillMaxWidth().weight(1f)) {
    itemContent(item)
    }
    if (index < chunkedItems.lastIndex || chunkedItems.size < chunkSize) {
    Spacer(modifier = Modifier.size(rowSpace))
    }
    }

    View Slide

  53. 自前でグリッドを作ってみる
    items(...) { chunkedItems ->
    Row(...) {
    chunkedItems.forEachIndexed { index, item ->
    Box(modifier = Modifier.fillMaxWidth().weight(1f)) {
    itemContent(item)
    }
    if (index < chunkedItems.lastIndex || chunkedItems.size < chunkSize) {
    Spacer(modifier = Modifier.size(rowSpace))
    }
    }

    View Slide

  54. 自前でグリッドを作ってみる
    items(...) { chunkedItems ->
    Row(...) {
    ...
    val spacerLastIndex = chunkSize - chunkedItems.size - 1
    repeat(chunkSize - chunkedItems.size) { index ->
    Box(modifier = Modifier.fillMaxWidth().weight(1f))
    if (index < spacerLastIndex) {
    Spacer(modifier = Modifier.size(rowSpace))
    }
    }

    View Slide

  55. 自前でグリッドを作ってみる
    items(...) { chunkedItems ->
    Row(...) {
    ...
    val spacerLastIndex = chunkSize - chunkedItems.size - 1
    repeat(chunkSize - chunkedItems.size) { index ->
    Box(modifier = Modifier.fillMaxWidth().weight(1f))
    if (index < spacerLastIndex) {
    Spacer(modifier = Modifier.size(rowSpace))
    }
    }

    View Slide

  56. 自前でグリッドを作ってみる
    items(...) { chunkedItems ->
    Row(...) {
    ...
    val spacerLastIndex = chunkSize - chunkedItems.size - 1
    repeat(chunkSize - chunkedItems.size) { index ->
    Box(modifier = Modifier.fillMaxWidth().weight(1f))
    if (index < spacerLastIndex) {
    Spacer(modifier = Modifier.size(rowSpace))
    }
    }

    View Slide

  57. 自前でグリッドを作ってみる
    ● アイテムをchunked(決まった数ごと)で取り出す

    View Slide

  58. 自前でグリッドを作ってみる
    ● アイテムをchunked(決まった数ごと)で取り出す
    ● chunkごとにRowを生成し、取り出したアイテムをそのRow
    内でweight=1fで均等に表示する

    View Slide

  59. 自前でグリッドを作ってみる
    ● アイテムをchunked(決まった数ごと)で取り出す
    ● chunkごとにRowを生成し、取り出したアイテムをそのRow
    内でweight=1fで均等に表示する
    ● アイテムが列数より少なくなるケースでは足りないアイテム
    分、weight=1fのSpacerで埋める

    View Slide

  60. 自前でグリッドを作ってみる
    ● 自分でRowを使ってグリッドを表現しているため、これでグ
    リッド内のスペースを自分でコントロールできるようになった
    ● あとはこれを汎用的に使えるように拡張関数にまとめる

    View Slide

  61. 自前でグリッドを作ってみる
    inline fun LazyGridScope.chunkedItems(
    items: List,
    chunkSize: Int,
    chunkedContentPadding: PaddingValues = PaddingValues(0.dp),
    rowSpace: Dp = 0.dp,
    noinline key: ((item: List) -> Any)? = null,
    noinline span: (LazyGridItemSpanScope.(item: List) -> GridItemSpan)? = null,
    noinline contentType: (item: List) -> Any? = { null },
    crossinline itemContent: @Composable LazyGridItemScope.(item: T) -> Unit,
    )

    View Slide

  62. 自前でグリッドを作ってみる
    inline fun LazyGridScope.chunkedItems(
    items: List,
    chunkSize: Int,
    chunkedContentPadding: PaddingValues = PaddingValues(0.dp),
    rowSpace: Dp = 0.dp,
    noinline key: ((item: List) -> Any)? = null,
    noinline span: (LazyGridItemSpanScope.(item: List) -> GridItemSpan)? = null,
    noinline contentType: (item: List) -> Any? = { null },
    crossinline itemContent: @Composable LazyGridItemScope.(item: T) -> Unit,
    )

    View Slide

  63. 自前でグリッドを作ってみる
    inline fun LazyGridScope.chunkedItems(
    items: List,
    chunkSize: Int,
    chunkedContentPadding: PaddingValues = PaddingValues(0.dp),
    rowSpace: Dp = 0.dp,
    noinline key: ((item: List) -> Any)? = null,
    noinline span: (LazyGridItemSpanScope.(item: List) -> GridItemSpan)? = null,
    noinline contentType: (item: List) -> Any? = { null },
    crossinline itemContent: @Composable LazyGridItemScope.(item: T) -> Unit,
    )

    View Slide

  64. 自前でグリッドを作ってみる
    chunkedItems(
    items = items,
    chunkSize = GRID_COLUMN_SIZE,
    span = { GridItemSpan(GRID_COLUMN_SIZE) },
    rowSpace = 8.dp,
    chunkedContentPadding = PaddingValues(top = 8.dp, start = 8.dp, end = 8.dp)
    ) { item ->
    // ここにアイテムのレイアウトを入れる
    }
    // 下部のスペース
    item(span = { GridItemSpan(GRID_COLUMN_SIZE) }) {
    Spacer(modifier = Modifier.size(8.dp))
    }

    View Slide

  65. 自前でグリッドを作ってみる
    chunkedItems(
    items = items,
    chunkSize = GRID_COLUMN_SIZE,
    span = { GridItemSpan(GRID_COLUMN_SIZE) },
    rowSpace = 8.dp,
    chunkedContentPadding = PaddingValues(top = 8.dp, start = 8.dp, end = 8.dp)
    ) { item ->
    // ここにアイテムのレイアウトを入れる
    }
    // 下部のスペース
    item(span = { GridItemSpan(GRID_COLUMN_SIZE) }) {
    Spacer(modifier = Modifier.size(8.dp))
    }

    View Slide

  66. 自前でグリッドを作ってみる
    chunkedItems(
    items = items,
    chunkSize = GRID_COLUMN_SIZE,
    span = { GridItemSpan(GRID_COLUMN_SIZE) },
    rowSpace = 8.dp,
    chunkedContentPadding = PaddingValues(top = 8.dp, start = 8.dp, end = 8.dp)
    ) { item ->
    // ここにアイテムのレイアウトを入れる
    }
    // 下部のスペース
    item(span = { GridItemSpan(GRID_COLUMN_SIZE) }) {
    Spacer(modifier = Modifier.size(8.dp))
    }

    View Slide

  67. 実行結果
    3列の場合

    View Slide

  68. 実行結果
    4列の場合

    View Slide

  69. 無事に期待通りのレイアウトを実現できた 🎉

    View Slide

  70. おわりに
    ● 今回実装したコードは以下のgistに公開しています
    ○ https://sh.syarihu.net/gist-chunked-items
    ● itemsIndexedのようにindexを取得したいケースのサンプル
    コードも記載しているので、参考までにどうぞ

    View Slide

  71. Jetpack Composeでグリッドに柔
    軟にスペースを入れたい
    shibuya.apk #41
    2023/04/21 (Fri.)
    Taichi Sato / @syarihu
    Giftmall, Inc.
    Android Engineer

    View Slide