2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【SwiftUI/JetpackCompose】ドロップシャドウをつくる ~比較で覚える宣言的モバイルUI~

Last updated at Posted at 2022-09-28

SwiftUIとJetpackComposeで、同セグメントの機能を両フレームワークでつくろうとした時に

  • きれいに対応関係がまとまっている
  • コピペで動く
  • 最もシンプルな実装

そんな記事があったらいいなと思い、備忘録も兼ねて自分が実装してみた範囲でまとめていきたいと思います。初心者ですので、より良い実装をご存知の方がいらっしゃいましたら、ご教示ください。

今回はドロップシャドウ編です。

対応関係

【SwiftUI】.shadow修飾子(modifier)
【JetpackCompose】①不完全な公式.shadow修飾子 ②自作の修飾子 ③自分自身をぼかして重ねる

SwiftUI

公式の.shadow修飾子

https://developer.apple.com
func shadow(
    color: Color = Color(.sRGBLinear, white: 0, opacity: 0.33),
    radius: CGFloat,
    x: CGFloat = 0,
    y: CGFloat = 0
) -> some View

具体例

修飾子として、ビューにつけます。

コードはこちら
struct ContentView: View {
    var body: some View {
        VStack {
            // 塗りつぶし
            RoundedRectangle(cornerRadius: 40)
                .fill(Color.green)
                .frame(width: 200, height: 120)
                .shadow(color: Color.gray, radius: 8, x: 3, y: 3)
            
            Spacer().frame(height: 50)
            
            // 枠線のみ
            RoundedRectangle(cornerRadius: 30)
                .stroke(Color.green, lineWidth: 20)
                .frame(width: 180, height: 100)
                .shadow(color: Color.gray, radius: 8, x: 3, y: 3)
            
            Spacer().frame(height: 50)
            
            // 図形
            Image(systemName: "car")
                .resizable()
                .scaledToFit()
                .foregroundColor(Color.green)
                .frame(width: 150)
                .shadow(color: Color.gray, radius: 8, x: 3, y: 3)
        }
    }
}

実行結果

塗りつぶし・枠線・図形のどの場合も、いい感じの影ができています。

swiftui_shadow.png

JetpackCompose

1. 公式の.shadow修飾子

こちらにも、公式の出している.shadow修飾子があります。

.shadow修飾子(Compose 1.2.0-alpha06以降) -> 色変更できる(参考)

https://developer.android.com
fun Modifier.shadow(
    elevation: Dp,
    shape: Shape = RectangleShape,
    clip: Boolean = elevation > 0.dp,
    ambientColor: Color = DefaultShadowColor,
    spotColor: Color = DefaultShadowColor
): Modifier

.shadow修飾子(上記以前) -> 色変更できない

https://developer.android.com
fun Modifier.shadow(
    elevation: Dp,
    shape: Shape = RectangleShape,
    clip: Boolean = elevation > 0.dp,
): Modifier

具体例

コードはこちら
@Composable
fun ExampleScreen(name: String) {
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        // 塗りつぶし
        Box(
            modifier = Modifier
                .shadow(8.dp, shape = RoundedCornerShape(40.dp))
                .clip(RoundedCornerShape(40.dp))
                .size(width = 200.dp, height = 120.dp,)
                .background(Color.Green)
        )

        Spacer(modifier = Modifier.height(50.dp))

        // 枠線のみ
        Box(
            modifier = Modifier
                .shadow(elevation = 8.dp, shape = RoundedCornerShape(40.dp))
                .border(20.dp, Color.Green, RoundedCornerShape(40.dp))
                .size(width = 200.dp, height = 120.dp,)
                .background(Color.Transparent)
        )

        Spacer(modifier = Modifier.height(50.dp))

        // 図形
        Icon(
            imageVector = Icons.Outlined.DirectionsCar,
            contentDescription = null,
            tint = Color.Green,
            modifier = Modifier
                .shadow(elevation = 8.dp,)
                .size(150.dp)
        )

        Spacer(modifier = Modifier.height(50.dp))

        // 図形 with Surface
        Surface(
            elevation = 9.dp,
        ) {
            Icon(
                imageVector = Icons.Outlined.DirectionsCar,
                contentDescription = null,
                tint = Color.Green,
                modifier = Modifier
                    .size(150.dp)
            )
        }

    }
}

実行結果

塗りつぶし図形はうまくいっていますが、枠線の内側の影がうまくいっていません。図形の影もこちらなどを参考に2通りやってみましたが、矩形の影になってしまいます。.shadowのshape引数にimageVectorを代入できたらいいのですが。
jetpack_compose_shadow.png

ではどうするか

  1. 自作の修飾子をつくる -> 枠線のみの影にも対応。Modifierとしての取り回しの良さ。
  2. ぼかしたそれ自身を後ろに重ねる -> 何にでも使える。Modifier的には使えない。

2. 自作の修飾子

こちらを参考にさせていただきました。
基本的にCanvasで作成したぼかした矩形を背後に追加します。枠線のみの場合にも対応できるように、一部変更を加えています。Canvasで作成することで、修飾子として実装できるのが特徴です。便利な反面、図形には対応できません。

fun Modifier.advancedShadow(
    color: Color = Color.Black,
    alpha: Float = 0.1f,
    cornersRadius: Dp = 0.dp,
    strokeWidth: Dp? = null, // <- 枠線のみの影をつけたい時は値を入れる
    shadowBlurRadius: Dp = 8.dp,
    offsetX: Dp = 3.dp,
    offsetY: Dp = 3.dp,
) = drawBehind {

    val shadowColor = color.copy(alpha = alpha).toArgb()
    val transparentColor = color.copy(alpha = 0f).toArgb()

    drawIntoCanvas {
        val paint = Paint()
        strokeWidth?.let { // <- 枠線のみの影をつけるため追加
            paint.style = PaintingStyle.Stroke
            paint.strokeWidth = strokeWidth.toPx()
        }
        val frameworkPaint = paint.asFrameworkPaint()
        frameworkPaint.color = transparentColor
        frameworkPaint.setShadowLayer(
            shadowBlurRadius.toPx(),
            offsetX.toPx(),
            offsetY.toPx(),
            shadowColor
        )
        val strokeOffset: Float = (strokeWidth?.toPx())?.div(2f) ?: 0f
        it.drawRoundRect(
            strokeOffset,
            strokeOffset,
            this.size.width - strokeOffset,
            this.size.height - strokeOffset,
            cornersRadius.toPx(),
            cornersRadius.toPx(),
            paint
        )
    }
}

3. 自分自身をぼかして重ねる

シンプルですが、なんだかんだ万能です。.blurが偉大です。
実装の見た目は、あまりスマートではないのが玉にキズです。以下は、「DirectionsCar」アイコンの例です。

.blur()は、Android12以上でないと非対応でした。

Box {
    // 影の部分
    Icon(
        imageVector = Icons.Outlined.DirectionsCar,
        contentDescription = null,
        tint = Color.Gray,
        modifier = Modifier
            .offset(x = 3.dp, y = 3.dp)
            .blur(8.dp)
            .alpha(0.3f)
            .size(150.dp)
    )
    // 本体
    Icon(
        imageVector = Icons.Outlined.DirectionsCar,
        contentDescription = null,
        tint = Color.Green,
        modifier = Modifier
            .size(150.dp)
    )
}

具体例

コードはこちら
fun ExampleScreen(name: String) {
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        // 塗りつぶし 自作の修飾子
        Box(
            modifier = Modifier
                .advancedShadow(
                    strokeWidth = 20.dp,
                    cornersRadius = 40.dp,
                )
                .size(width = 200.dp, height = 120.dp,)
                .clip(RoundedCornerShape(40.dp))
                .background(Color.Green)
        )

        Spacer(modifier = Modifier.height(50.dp))

        // 枠線のみ 自作の修飾子
        Box(
            modifier = Modifier
                .advancedShadow(
                    strokeWidth = 20.dp,
                    cornersRadius = 40.dp,
                )
                .border(20.dp, Color.Green, RoundedCornerShape(40.dp))
                .size(width = 200.dp, height = 120.dp,)
                .background(Color.Transparent)
        )

        Spacer(modifier = Modifier.height(10.dp))

        // 枠線のみ 自分自身をぼかして重ねる
        Box(contentAlignment = Alignment.Center) {
            // 影の部分
            Box( // 二重構造にしないと、影がクリップされて汚くなる
                modifier = Modifier
                    .offset(x = 3.dp, y = 3.dp)
                    .blur(8.dp)
                    .size(width = 300.dp, height = 200.dp,), // 本体より少し大きくする
                contentAlignment = Alignment.Center
            ) {
                Box(
                    modifier = Modifier
                        .alpha(0.1f)
                        .border(20.dp, Color.Black, RoundedCornerShape(40.dp))
                        .size(width = 200.dp, height = 120.dp,)
                        .background(Color.Transparent)
                )
            }
            // 本体
            Box(
                modifier = Modifier
                    .border(20.dp, Color.Green, RoundedCornerShape(40.dp))
                    .size(width = 200.dp, height = 120.dp,)
                    .background(Color.Transparent)
            )
        }

        // 図形 自分自身をぼかして重ねる
        Box {
            // 影の部分
            Icon(
                imageVector = Icons.Outlined.DirectionsCar,
                contentDescription = null,
                tint = Color.Black,
                modifier = Modifier
                    .offset(x = 3.dp, y = 3.dp)
                    .blur(8.dp)
                    .alpha(0.1f)
                    .size(180.dp)
            )
            // 本体
            Icon(
                imageVector = Icons.Outlined.DirectionsCar,
                contentDescription = null,
                tint = Color.Green,
                modifier = Modifier
                    .size(180.dp)
            )
        }
    }
}

実行結果

SwiftUIのように、全てに対応できました。個人的には、シンプルな形は自作の修飾子、複雑な図形には自分自身をぼかして重ねる手法が合っているように思います。
jetpack_compose_shadow_revised.png

まとめ

ドロップシャドウ
対応関係を再掲します。
【SwiftUI】.shadow修飾子(modifier)
【JetpackCompose】①不完全な.shadow修飾子 ②自作の修飾子 ③自分自身をぼかして重ねる

この記事ではSwiftUIびいきな感じになってしまいましたが、JetpackComposeではマテリアルデザインのガイドライン上、自由に影をつけるのをあまり推奨していないのかもしれません。それぞれのフレームワークを比べることで、両社のイデオロギーが垣間見えるのが興味深いです。

ImageVectorからPath/Shapeにできるライブラリがあれば、それでCanvas内で図形も描画するか、公式shadow修飾子のshape引数に入れることで、もしかするとSwiftUIと同じ使い心地の自作の修飾子がつくれるかもしれません。機会があれば挑戦してみたいと思います。

2
4
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
2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?