SwiftUIとJetpackComposeで、同セグメントの機能を両フレームワークでつくろうとした時に
- きれいに対応関係がまとまっている
- コピペで動く
- 最もシンプルな実装
そんな記事があったらいいなと思い、備忘録も兼ねて自分が実装してみた範囲でまとめていきたいと思います。初心者ですので、より良い実装をご存知の方がいらっしゃいましたら、ご教示ください。
今回はドロップシャドウ編です。
対応関係
【SwiftUI】.shadow修飾子(modifier)
【JetpackCompose】①不完全な公式.shadow修飾子 ②自作の修飾子 ③自分自身をぼかして重ねる
SwiftUI
公式の.shadow修飾子
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)
}
}
}
実行結果
塗りつぶし・枠線・図形のどの場合も、いい感じの影ができています。
JetpackCompose
1. 公式の.shadow修飾子
こちらにも、公式の出している.shadow修飾子があります。
.shadow修飾子(Compose 1.2.0-alpha06以降) -> 色変更できる(参考)
fun Modifier.shadow(
elevation: Dp,
shape: Shape = RectangleShape,
clip: Boolean = elevation > 0.dp,
ambientColor: Color = DefaultShadowColor,
spotColor: Color = DefaultShadowColor
): Modifier
.shadow修飾子(上記以前) -> 色変更できない
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を代入できたらいいのですが。
ではどうするか
- 自作の修飾子をつくる -> 枠線のみの影にも対応。Modifierとしての取り回しの良さ。
- ぼかしたそれ自身を後ろに重ねる -> 何にでも使える。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のように、全てに対応できました。個人的には、シンプルな形は自作の修飾子、複雑な図形には自分自身をぼかして重ねる手法が合っているように思います。
まとめ
ドロップシャドウ
対応関係を再掲します。
【SwiftUI】.shadow修飾子(modifier)
【JetpackCompose】①不完全な.shadow修飾子 ②自作の修飾子 ③自分自身をぼかして重ねる
この記事ではSwiftUIびいきな感じになってしまいましたが、JetpackComposeではマテリアルデザインのガイドライン上、自由に影をつけるのをあまり推奨していないのかもしれません。それぞれのフレームワークを比べることで、両社のイデオロギーが垣間見えるのが興味深いです。
ImageVectorからPath/Shapeにできるライブラリがあれば、それでCanvas内で図形も描画するか、公式shadow修飾子のshape引数に入れることで、もしかするとSwiftUIと同じ使い心地の自作の修飾子がつくれるかもしれません。機会があれば挑戦してみたいと思います。