BASEプロダクトチームブログ

ネットショップ作成サービス「BASE ( https://thebase.in )」、ショッピングアプリ「BASE ( https://thebase.in/sp )」のプロダクトチームによるブログです。

Jetpack Compose上でのconstraintLayoutの利用事例紹介

この記事は BASE アドベントカレンダー 17日目の記事です。

はじめに

Pay IDアプリチームの小林です。「ショッピングアプリ Pay ID」のAndroidアプリを開発しています。

今回はJetpack ComposeでConstraintLayoutを使った事例紹介をしたいと思います。

ConstraintLayoutとは

まず、ConstraintLayoutとは、複雑なレイアウト、特に相対的なレイアウトを書きたいときに使うことになるクラスです。

始まりはComposeによるレイアウト作成の前、XMLにてレイアウトを書いていた頃に登場しました。

その頃はXMLだったので、複雑なレイアウトはネストして書かなければならず、分かりづらいものになっていました。

また、ネストしたレイアウトはパフォーマンス上の問題もありました。

フラットにネストなく書けるようになるConstraintLayoutは、設定のとっつきづらさはあったものの、ネストによる分かりづらさに比べたら有効なLayoutだったと感じています。

ConstraintLayoutだけで使えるレイアウト表現もありました。

その後、Composeが誕生しKotlinコードでレイアウトが書くことが可能になりました。

ネストによる分かりづらさはメソッド分け等のKotlinコードで書くことでできる手法である程度解消することができ、パフォーマンス問題もComposeはネストしても悪化しないようになりました。

注: View システムでは、大規模で複雑なレイアウトを作成する場合、
ConstraintLayout を使用することが推奨されていました。
これは、ネストされたビューよりもフラットなビュー階層の方が
パフォーマンスに優れているためです。
しかし、深いレイアウト階層を効率的に扱える Compose では、
このような懸念はありません。

( https://developer.android.com/develop/ui/compose/layouts/constraintlayout?hl=ja より転載)

前述したConstraintLayoutだけで使える表現も、RowやColumn等でも表現できるようになったりと、ConstraintLayoutを使わなければならない部分は大きく減りました。

(例: ガイドラインという表現方法の場合)

注: Rows と Columns で同様の効果を実現するには、Spacer の使用をご検討ください。

( https://developer.android.com/develop/ui/compose/layouts/constraintlayout?hl=ja#guidelines より転載)

ComposeでConstraintLayoutを使ったケース

そのため、普段はComposeでConstraintLayoutを使わないのですが、Pay IDアプリで表現したいレイアウトを実現するためにConstraintLayoutを使っている箇所がいくつかあります。

1つのViewに合わせて、縦と横のラインを揃えたい場合

そのまま、ConstraintLayoutの用途である相対的なレイアウトを作りたかった場合です。

このようなレイアウトがあり、チェックアイコンの中央ラインに日付やラベルを揃えたいのとチェックアイコンの上下にある緑や灰色の棒UIも、チェックアイコンの縦真ん中に揃えたいものでした。

これらをコードに起こしてたものがこちらです。

    ConstraintLayout(
        modifier = Modifier.fillMaxWidth(),
    ) {
        val (
            topLine,
            checkicon,
            bottomLine,
            dateRef,
            // ... 他レイアウトの制約名
        ) = createRefs()

        LowerHalfBar(// 棒の下半分
            modifier = Modifier.constrainAs(topLine) {
                start.linkTo(checkicon.start)
                end.linkTo(checkicon.end)
                top.linkTo(parent.top)
            },
            viewData = viewData,
        )

        CheckBoxPaid(// チェックアイコン
            modifier = Modifier.constrainAs(checkicon) {
                start.linkTo(parent.start, margin = 16.dp)
                top.linkTo(parent.top, margin = 16.dp)
                bottom.linkTo(parent.bottom, margin = 16.dp)
            },
            viewData = viewData,
        )

        UpperHalfBar(// 棒の上半分
            modifier = Modifier.constrainAs(bottomLine) {
                start.linkTo(checkicon.start)
                end.linkTo(checkicon.end)
                bottom.linkTo(parent.bottom)
            },
            viewData = viewData,
        )

        Date(// 日付
            modifier = Modifier.constrainAs(dateRef) {
                start.linkTo(checkicon.end, margin = 16.dp)
                top.linkTo(checkicon.top)
                bottom.linkTo(checkicon.bottom)
            },
            viewData = viewData,
        )
        // ...他レイアウト
    }

ConstraintLayout自体の書き方の説明は公式の説明が詳しくわかりやすいので省略しますが、各レイアウトのlinkToにチェックアイコンの上下やstart/endを設定することで、チェックアイコンに対しての中央に各レイアウトを配置することができています。

BoxやRow/Columnを使って表現しようとしたのですが、

  • チェックアイコン+日付等をRowで囲んで上下の棒を左寄せでアイコンの中央っぽくすると、スクリーン設定等でズレる
  • チェックアイコン+上下の棒をColumnで囲んで、日付をそれらの中央に指定すると、微妙にアイコンの中央からはズレてしまうし、上下の棒がつながって見えない

という問題があったりして、ConstraintLayoutを使うことで解決しました。

レイアウトを重ねたい場合

2つ目はUIを重ねて表現したいときです。

こちらのレイアウトなのですが

の2つのレイアウトの後ろに、白背景のSpacerを重ねて表現しています。

        ConstraintLayout {
            val (text1, image2, background3) = createRefs()
            Spacer( // 白背景を重ねる。先に定義する
                modifier = Modifier
                    .background(Color.White)
                    .constrainAs(background3) {
                        top.linkTo(text1.top)// テキストと同じ高さにする
                        bottom.linkTo(text1.bottom)// テキストと同じ高さにする
                        start.linkTo(text1.start, 24.dp) // テキストの少し右側から
                        end.linkTo(image2.end, 12.dp) // 画像の少し左側まで表示する
                        width = Dimension.fillToConstraints
                        height = Dimension.fillToConstraints
                    },
            )
            Text(
                modifier = Modifier
                    //...略
                    .constrainAs(text1) {
                        top.linkTo(image2.top) //画像との中央揃えの設定
                        bottom.linkTo(image2.bottom)//画像との中央揃えの設定
                        start.linkTo(parent.start)
                        end.linkTo(image.start)// 画像の左に配置
                    },
                text = "最近の購入品",
            )

            Image(
                modifier = Modifier
                        // ...略
                    .constrainAs(image2) {
                        end.linkTo(parent.end)
                    },
                painter = //...,
                contentDescription = "画像",
            )

        }
    }

このようにすることでテキストと画像の間の微妙な隙間を白背景で埋めることができます。

Spacerは何も定義しないと0dpでlinkToで幅や高さを設定しても描画されません。

そのため、width/heightでDimension.fillToConstraintsを設定することで、constrainAsで設定したそのUIが表示されていい領域いっぱいまで高さや幅を広げてくれます。

おわりに

今回はPay IDアプリでJetpack Compose上でConstraintLayoutを使っている事例を2つ紹介しました。

ここまで見ていただいた方の助けになったら幸いです。

明日は @meiheiさんと shota.imazekiさんの記事が発表されます。お楽しみに!