Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

スペースを入れない通常のレイアウトを作成する 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) } }

Slide 11

Slide 11 text

スペースを入れない通常のレイアウトを作成する 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) } }

Slide 12

Slide 12 text

スペースを入れない通常のレイアウトを作成する 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) } }

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

contentPaddingを使う

Slide 15

Slide 15 text

contentPaddingを使う LazyVerticalGrid( … contentPadding = PaddingValues(8.dp) ) {

Slide 16

Slide 16 text

実行結果

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

spacedByを使ってスペースを設定する LazyVerticalGrid( … verticalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp) ) {

Slide 20

Slide 20 text

実行結果

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

アイテムごとに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) ) {

Slide 27

Slide 27 text

アイテムごとに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) ) {

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

アイテムによって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()

Slide 33

Slide 33 text

アイテムによって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, ) ...

Slide 34

Slide 34 text

アイテムによって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, ) ...

Slide 35

Slide 35 text

アイテムによって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, ) ...

Slide 36

Slide 36 text

アイテムによって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, ) ...

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

ならば自分で作ろう💡

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

自前でグリッドを作ってみる 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)) } }

Slide 51

Slide 51 text

自前でグリッドを作ってみる 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)) } }

Slide 52

Slide 52 text

自前でグリッドを作ってみる 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)) } }

Slide 53

Slide 53 text

自前でグリッドを作ってみる 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)) } }

Slide 54

Slide 54 text

自前でグリッドを作ってみる 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)) } }

Slide 55

Slide 55 text

自前でグリッドを作ってみる 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)) } }

Slide 56

Slide 56 text

自前でグリッドを作ってみる 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)) } }

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

自前でグリッドを作ってみる 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, )

Slide 62

Slide 62 text

自前でグリッドを作ってみる 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, )

Slide 63

Slide 63 text

自前でグリッドを作ってみる 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, )

Slide 64

Slide 64 text

自前でグリッドを作ってみる 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)) }

Slide 65

Slide 65 text

自前でグリッドを作ってみる 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)) }

Slide 66

Slide 66 text

自前でグリッドを作ってみる 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)) }

Slide 67

Slide 67 text

実行結果 3列の場合

Slide 68

Slide 68 text

実行結果 4列の場合

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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