LoginSignup
0
1

More than 1 year has passed since last update.

Composableのカスタムレイアウト

Posted at

このページではComposeの1.0.0-beta08バージョンを基としたアニメーション周りの感想をまとめたものです。参考にした動画はこちらですhttps://youtu.be/DDd6IOlH3io

beta08での情報なので、随時変更される可能性はございます。

@Composable fun Layout()でカスタムレイアウトを作成!

ComposeではLayoutブロックを使要することでカスタムレイアウトを作成することができます。以下動画内の一部を抜粋したサンプルで説明します。

constraint.JPG
(※画像でのminは140dpですが、この記事を書いている時点では134dpに変更されています)

サンプルコード。
https://github.com/android/compose-samples/blob/01d35510a3d5d72099ffbb2c6e25ef4782291d3d/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Categories.kt#L97

Composableレイアウトを作る関数の宣言は以下の通りです。Modifierとcontentを引数にとり、メインブロックにmeasureBlockがあります。

fun Layout(
    modifier: Modifier = Modifier,    
    content:@Composable () -> Unit,
){
    measureBlock: MeasureBlock
}

メジャーブロックは3つのステップで作ることができます。
1. それぞれのアイテムのmeasureを呼び出す。
2. layout()を呼ぶ。
3. それぞれのアイテムを置く(placeを呼び出す)。

コードの外観

Layout(
  modifier = ...,
  children = {
      Text(
          text = category.name,
          ...
       )
       SnackImage(
          ...
       )
  }
) { measurables: List<Measurable>, 
    constraints: Constraints->
  // 1. それぞれのアイテムのmeasureを呼び出す。
  // 2. layout()を呼ぶ。
  // 3. それぞれのアイテムを置く(placeを呼び出す)。
}

MeasureBlockの引数について

引数で渡ってくる constraints: Constraints とはなにか?

AndroidエンジニアにはおなじみのConstraintLayoutとは何も関係なく、maxやminのWidthやHeightが取得できるだけで、このViewの大きさの上限と下限がわかるだけのオブジェクトです。

引数で渡ってくる measurables: List<Measurable> とはなにか?

このリストの一つ一つの要素がUIのコンポーネントのmeasureを呼べるmeasurableになっている。
例えばここでmeasurables[0]はTextに対応して、measurables[1]はSnackImageに対応する。
このMeasurableに対して Measurable#measure()を呼び出すと Placeable を返す。

一番シンプルな例

一番左上にすべての物を置く例がこちらになります。

以下の流れを行っています。

  1. それぞれのアイテムのmeasureを呼び出す。
  2. layout()を呼ぶ。
  3. それぞれのアイテムを置く(placeを呼び出す)。
Layout(
  modifier = ...,
  content = {
      Text(
          text = category.name,
          ...
       )
       SnackImage(
          ...
       )
  }
) { measurables: List<Measurable>, 
    constraints: Constraints->
    // 1. それぞれのアイテムのmeasureを呼び出す。
    val placeables = measurables.map {
        it.measure(constraints)
    }
    // 2. layout()を呼ぶ。
    layout(
            width = constraints.maxWidth,
            height = constraints.maxHeight
    ) {
        placeables.forEach {
            // 3. それぞれのアイテムを置く(placeを呼び出す)。
            it.place(
                    x = 0,
                    y = 0
            )
        }
    }
}

placeable.place()関数は、うまくKotlin DSLの機能を使っておりlayout()のブロックの中でしか呼べなくなっています。

Jetpack Composeのサンプルではどうやっているか?

以下のように実際に行っているようです。
image.png

https://github.com/android/compose-samples/blob/34a75fb3672622a3fb0e6a78adc88bbc2886c28f/Jetsnack/app/src/main/java/com/example/jetsnack/ui/home/search/Categories.kt#L102 より

コードの中にコメントとして解説を入れてみました。

    Layout(
        modifier = modifier
            // レイアウトのアスペクト比を設定
            .aspectRatio(1.45f)
            .shadow(elevation = 3.dp, shape = CategoryShape)
            // 丸みを帯びた長方形にカット
            .clip(CategoryShape)
            .background(Brush.horizontalGradient(gradient))
            .clickable { /* todo */ },
        content = {
            Text(
                text = category.name,
                style = MaterialTheme.typography.subtitle1,
                color = JetsnackTheme.colors.textSecondary,
                modifier = Modifier
                    .padding(4.dp)
                    .padding(start = 8.dp)
            )
            SnackImage(
                imageUrl = category.imageUrl,
                contentDescription = null,
                modifier = Modifier.fillMaxSize()
            )
        }
    ) { measurables, constraints ->
        // テキストの横幅の比率設定(55%)
        val textWidth = (constraints.maxWidth * CategoryTextProportion).toInt()
        val textPlaceable = measurables[0].measure(Constraints.fixedWidth(textWidth))

        // 画像が大きい場合は原寸大を試用し、小さい場合はConstraintの最大サイズに合わせて拡大
        val imageSize = max(MinImageSize.roundToPx(), constraints.maxHeight)
        // 縦横の大きさを固定して、Constraintに渡しmeasure()する
        val imagePlaceable = measurables[1].measure(Constraints.fixed(imageSize, imageSize))
        layout(
            width = constraints.maxWidth,
            height = constraints.minHeight
        ) {
            //テキストの左寄せ
            textPlaceable.placeRelative(
                x = 0,
                y = (constraints.maxHeight - textPlaceable.height) / 2 // 縦方向の中央配置
            )
            //はみ出した画像はクリップされる
            imagePlaceable.placeRelative(
                //テキストの最後尾に画像を配置
                x = textWidth,
                y = (constraints.maxHeight - imagePlaceable.height) / 2 // 縦方向の中央配置
            )
        }
    }

スクロールと連動のアニメーション処理

scroll.gif

Layout()でスクロールに連動した伸縮のエフェクトも実現できます。
以下のコードは主に、画像の伸縮の処理を行っています。

@Composable
private fun CollapsingImageLayout(
    collapseFraction: Float,
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        check(measurables.size == 1)

        val imageMaxSize = min(ExpandedImageSize.roundToPx(), constraints.maxWidth)
        val imageMinSize = max(CollapsedImageSize.roundToPx(), constraints.minWidth)
        val imageWidth = lerp(imageMaxSize, imageMinSize, collapseFraction)
        // measureする
        val imagePlaceable = measurables[0].measure(Constraints.fixed(imageWidth, imageWidth))

        // lerpは2点A,B間の間のベクトルを近似的関数で求めてくれるFunctionです
        val imageY = lerp(MinTitleOffset, MinImageOffset, collapseFraction).roundToPx()
        val imageX = lerp(
            (constraints.maxWidth - imageWidth) / 2, // centered when expanded
            constraints.maxWidth - imageWidth, // right aligned when collapsed
            collapseFraction
        )
        // layoutのセット
        layout(
            width = constraints.maxWidth,
            height = imageY + imageWidth
        ) {
            imagePlaceable.placeRelative(imageX, imageY)
        }
    }
}

スクロールとの連動方法

ScrollStateを作成し、スクロールの変化量を常に記録します。スクロールの情報を、それぞれBody()やImage()などのComposable関数に渡すことで、全体がスクロールに連動するようになります。

@Composable
fun SnackDetail(
...
) {
...
    Stack(Modifier.fillMaxSize()) {
        val scroll = rememberScrollState(0f) // ← ここでScrollStateを記録
     ...
        Body(related, scroll)
     ...
        Image(snack.imageUrl, scroll.value) 
     ...
    }

他にも様々な実装がサンプルで用意されています

他にもJetsnackサンプル内で様々な動きが実装されていますので、興味のある方は以下のリンクから是非参考にしてみてください!
https://github.com/android/compose-samples/tree/master/Jetsnack#features

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1