🤖

Jetpack Composeでレイアウト組んでみた

2021/09/05に公開

Jetpack Composeの修行のためにこちらのデザインの配置を真似して書いてみた記録

ゴール

コードはこちらのcommit

LazyColumn

@Composable
fun TestContent() {
    val items = listOf("TAB", "PROFILE", "TITLE", "ITEM", "ITEM", "ITEM", "ITEM", "ITEM", "ITEM")
    LazyColumn(modifier = Modifier.fillMaxWidth()) {
        items(items, itemContent = { item ->
            when (item) {
                "TAB" -> TextTabs()
                "PROFILE" -> Profile()
                "TITLE" -> TitleRow()
                "ITEM" -> ItemRow()
            }
        })
    }
}

コンテンツをスクロールで表示します。
RecyclerViewの代わりになります。

ToolBar

ハンバーガーアイコンとタイトルと+ボタンを表示します。

topBar = {
    TopAppBar(
        title = { Text("Title") },
        navigationIcon = {
            IconButton(onClick = {}) {
                Icon(painterResource(sobaya.app.resources.R.drawable.ic_menu_black_24dp), "menu")
            }
        },
        actions = {
            IconButton(onClick = {}) {
                Icon(Icons.Filled.Add, contentDescription = "")
            }
        }
    )
}

Scaffoldのtoolbarに指定します。
navigationIconにハンバーガー、actionsに+ボタンを配置しました。

DrawerContent

中身はないけど用意はしてました。

drawerContent = { Text(text = "drawerContent") }

ScaffoldのdrawerContentに指定します。

Tab

@Composable
fun TextTabs() {
    var tabIndex by remember { mutableStateOf(0) }
    val tabData = listOf(
        "そば",
        "うどん"
    )
    TabRow(selectedTabIndex = tabIndex) {
        tabData.forEachIndexed { index, text ->
            Tab(selected = tabIndex == index, onClick = {
                tabIndex = index
            }, text = {
                Text(text = text)
            })
        }
    }
}

初めて使ったから時間かかったけど、次からはコピペでよさそう

ConstraintLayout

ConstraintLayout(modifier = Modifier
    .fillMaxWidth()
    .height(200.dp)) {
    val (iconImage, userName, mail, iconD, point, help) = createRefs()
    Image(
        painter = rememberImagePainter("https://avatars.githubusercontent.com/u/45986582?v=4"),
        contentDescription = null,
        contentScale = ContentScale.Crop,
        modifier = Modifier
            .fillMaxSize()
    )
    Image(
        painter = rememberImagePainter(
            data = "https://avatars.githubusercontent.com/u/45986582?v=4",
            builder = {
                transformations(CircleCropTransformation())
            }
        ),
        contentDescription = null,
        modifier = Modifier
            .size(64.dp)
            .constrainAs(iconImage) {
                top.linkTo(parent.top)
                bottom.linkTo(parent.bottom)
                start.linkTo(parent.start, margin = 16.dp)
            },
    )
    Text(
        text = "Sobaya-0141",
        color = Color.White,
        fontSize = 24.sp,
        modifier = Modifier.constrainAs(userName) {
            start.linkTo(mail.start)
            bottom.linkTo(mail.top, margin = 16.dp)
        }
    )
    Text(
        text = "soba.ha.kenkou@gmail.com",
        color = Color.LightGray,
        fontSize = 16.sp,
        modifier = Modifier.constrainAs(mail) {
            top.linkTo(parent.top)
            bottom.linkTo(parent.bottom)
            start.linkTo(iconImage.end, margin = 16.dp)
        }
    )
    Icon(
        imageVector = Icons.Default.Star,
        contentDescription = "star",
        modifier = Modifier.constrainAs(iconD) {
            top.linkTo(mail.bottom, margin = 16.dp)
            start.linkTo(mail.start)
        }
    )
    Text(
        text = "0141 points",
        color = Color.White,
        modifier = Modifier.constrainAs(point) {
            top.linkTo(iconD.top)
            start.linkTo(iconD.end, margin = 16.dp)
        }
    )
    Icon(
        imageVector = Icons.Default.Info,
        contentDescription = "info",
        modifier = Modifier.constrainAs(help) {
            top.linkTo(iconD.top)
            start.linkTo(point.end, margin = 16.dp)
        }
    )
}

xmlと近い感覚で書けたので最初の2個くらいが書けたら一瞬でした。
val (iconImage, userName, mail, iconD, point, help) = createRefs()
で何を配置するか宣言しておき
modifier = Modifier.constrainAs(iconD)
のように自分が宣言された誰だかを宣言して

top.linkTo(iconD.top)
start.linkTo(point.end, margin = 16.dp)

誰とどう繋がりたいか指定します。
個人的にはxmlよりも好きかもしれません

その他(一番苦労した)

Box(modifier = Modifier
    .fillMaxWidth()
    .background(Color.LightGray)) {
    Box(modifier = Modifier
        .fillMaxWidth()
        .padding(start = 16.dp, end = 16.dp)
        .background(Color.Gray)) {
        Box(modifier = Modifier
            .fillMaxWidth()
            .padding(start = 1.dp, end = 1.dp, bottom = 1.dp)
            .background(Color.White)
        ) {
            Column(
                Modifier
                    .padding(start = 84.dp)
                    .fillMaxWidth()) {
                Text(
                    text = "そばや",
                    color = Color.Black,
                    fontSize = 18.sp,
                    modifier = Modifier
                        .padding(top = 16.dp)
                )
                Text(
                    text = "食べ放題",
                    color = Color.Gray,
                    fontSize = 14.sp,
                    modifier = Modifier
                        .padding(end = 16.dp, top = 8.dp)
                )
                Text(
                    text = "飲み放題",
                    color = Color.Gray,
                    fontSize = 14.sp,
                    modifier = Modifier
                        .padding(end = 16.dp, top = 8.dp)
                )
                Text(
                    text = "トイレ無し",
                    color = Color.Gray,
                    fontSize = 14.sp,
                    modifier = Modifier
                        .padding(end = 16.dp, top = 8.dp, bottom = 8.dp)
                )
            }
        }
    }
    Card(modifier = Modifier
        .size(84.dp)
        .padding(start = 8.dp)
        .align(Alignment.CenterStart)) {
        Image(
            painter = rememberImagePainter(
                data = "https://avatars.githubusercontent.com/u/45986582?v=4",
            ),
            contentDescription = null,
            contentScale = ContentScale.Crop,
            modifier = Modifier.fillMaxSize()
        )
    }
}

画像の角丸対応

Card(modifier = Modifier
    .size(84.dp)
    .padding(start = 8.dp)
    .align(Alignment.CenterStart)) {
    Image(
        painter = rememberImagePainter(
            data = "https://avatars.githubusercontent.com/u/45986582?v=4",
        ),
        contentDescription = null,
        contentScale = ContentScale.Crop,
        modifier = Modifier.fillMaxSize()
    )
}

coilで変形させられるかと思ったけどradius指定はなかったのでCardに最大サイズ+ContentScale.Cropを指定することで解決

背景を薄い灰色、濃い灰色のボーダー、白背景にする

Box(modifier = Modifier
    .fillMaxWidth()
    .background(Color.LightGray)) {
    Box(modifier = Modifier
        .fillMaxWidth()
        .padding(start = 16.dp, end = 16.dp)
        .background(Color.Gray)) {
        Box(modifier = Modifier
            .fillMaxWidth()
            .padding(start = 1.dp, end = 1.dp, bottom = 1.dp)
            .background(Color.White)
        ) {

modifierにborderというオプションがあり、線を引くことは可能なのですが、
全部の辺に線を引いてしまいます。
なので、今回のように連続するアイテムに使用すると1つ目のアイテムの下線と2つ目のアイテムの上線が引かれ区切り部分が2倍の太さになってしまうので使えません

backgroundにdrawable/xmlを指定できると勝手に思い込んでいたけど、それもダメでした。

なので、xmlで任意の辺に線を引くのと同様に
1. 描画したい線の色で塗りつぶした四角を用意
2. 線を引きたい場所にだけpaddingを入れた四角を用意
の方法で線を引きました。

さらに今回は薄いグレーを背景に指定する必要があったので、

  1. LightGrayの塗りつぶし
  2. その上にGrayの塗りつぶし(枠線)
  3. その上にWhiteの塗りつぶし(コンテンツエリア)
    となっています。

最後に

スクロール重いの何が悪いんだろ???
何かもっと効率いい書き方あれば教えて頂けると嬉しいです。

参考

https://foso.github.io/Jetpack-Compose-Playground/foundation/lazycolumn/
https://coil-kt.github.io/coil/compose/

Discussion