Flutterでインナーシャドウを描画するコード、検索すると出てくるものが軒並みうまく描画できなかったので自作しました。数年前に調べたときはうまくレンダリングできていた気がするのですが…。
- 本記事執筆時点のFlutterのバージョン: 3.24.4
結論のコード
CustomPainterを使う方式で実装しました。ざっくり言うと単純にインナーシャドウを描画したい部分に穴を開けたパスを作って、そのパスを描画する際にMaskFilterでシャドウを付けています。
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
class InnerShadowPainter extends CustomPainter {
const InnerShadowPainter({
required this.shadows,
this.borderRadius,
});
final List<Shadow> shadows;
final BorderRadius? borderRadius;
RRect applyBorderRadius(Rect rect, BorderRadius borderRadius) {
return RRect.fromRectAndCorners(
rect,
topLeft: borderRadius.topLeft,
topRight: borderRadius.topRight,
bottomLeft: borderRadius.bottomLeft,
bottomRight: borderRadius.bottomRight,
);
}
Path createShadowPath(Rect rect, Shadow shadow) {
final outerRect = rect.inflate(max(shadow.offset.dx.abs(), shadow.offset.dy.abs()) + shadow.blurRadius + 1);
final path = Path()
..fillType = PathFillType.evenOdd
..addRect(outerRect);
final innerRect = rect.translate(shadow.offset.dx, shadow.offset.dy);
if (borderRadius != null) {
return path..addRRect(applyBorderRadius(innerRect, borderRadius!));
} else {
return path..addRect(innerRect);
}
}
@override
void paint(Canvas canvas, Size size) {
final rect = Offset.zero & size;
canvas.save();
if (borderRadius != null) {
canvas.clipRRect(applyBorderRadius(rect, borderRadius!));
} else {
canvas.clipRect(rect);
}
for (final shadow in shadows) {
final shadowPath = createShadowPath(rect, shadow);
final shadowPaint = Paint()
..color = shadow.color
..maskFilter = MaskFilter.blur(BlurStyle.normal, shadow.blurSigma);
canvas
..translate(shadow.offset.dx, shadow.offset.dy)
..drawPath(shadowPath, shadowPaint)
..translate(-shadow.offset.dx, -shadow.offset.dy);
}
canvas.restore();
}
@override
bool shouldRepaint(InnerShadowPainter oldDelegate) {
return !listEquals(shadows, oldDelegate.shadows) || borderRadius != oldDelegate.borderRadius;
}
}
class InnerShadow extends StatelessWidget {
const InnerShadow({
super.key,
required this.shadows,
this.borderRadius,
required this.child,
});
final List<Shadow> shadows;
final BorderRadius? borderRadius;
final Widget child;
@override
Widget build(BuildContext context) {
return CustomPaint(
foregroundPainter: InnerShadowPainter(
shadows: shadows,
borderRadius: borderRadius,
),
child: child,
);
}
}
使い方
InnerShadow(
shadows: [
Shadow(
color: Color.fromRGBO(0,0,0,0.5),
blurRadius: 2.0,
offset: Offset(1, 1),
),
],
child: Container(
width: 200,
height: 200,
alignment: Alignment.center,
color: Colors.white,
),
)
結果
おわりに
最初はsaveLayerとBlendModeをこねくり回していたのですが、よく考えたら素直な方法で描画できました。