🪟
【Flutter】glassmorphismを作成した
はじめに
今更ながら、Flutterでglassmorphismを自作で作ってみたくなったので挑戦してみました。
環境
flutter --version
Flutter 3.10.3 • channel stable • https://github.com/flutter/flutter.git
Framework • revision f92f44110e (3 months ago) • 2023-06-01 18:17:33 -0500
Engine • revision 2a3401c9bb
Tools • Dart 3.0.3 • DevTools 2.23.1
実装
土台となるContainerを作成
まずは左上から円形に、半透明な白色がグラデーションするContainerを用意します。
Container(
alignment: Alignment.center,
width: 200,
height: 200,
decoration: ShapeDecoration(
gradient: RadialGradient(
radius: 1.6,
center: Alignment.topLeft,
colors: [
Colors.white.withOpacity(0.3),
Colors.white.withOpacity(0.1),
],
),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
),
blurをかける
ClipRect
とBackdropFilter
を用いてblurをかけていきます。
import 'dart:ui' as ui;
ClipRect(
child: BackdropFilter(
filter: ui.ImageFilter.blur(
sigmaX: 5,
sigmaY: 5,
),
child: Container(
...
//土台となるContainer
...
),
),
),
少しそれますが、blur
のプロパティであるsigmaX
とsigmaY
について、このプロパティは背景のぼかしの強度に関わります。数字に強度が比例します。
filter: ui.ImageFilter.blur(
sigmaX: 20,
sigmaY: 20,
),
strokeをつける
単色のstrokeをつけても良かったのですが、ここはこだわってグラデーションがかかったstrokeをつけることにします。
strokeにグラデーションをかけるのには、簡単にわかる分には少ないです。strokeをかけたいWidgetに、そのWidgetより大きいグラデーションがかかったContainer
を親Widgetとして追加する方法が正攻法みたいです。
しかし、今回はstrokeをかけたいWidgetが半透明なため、グラデーションがかかったContainer
の全体が見えてしまいます。
したがって、CustomPainter
を用いて、縁だけを描画するようにします。
class _GradientPainter extends CustomPainter {
_GradientPainter({
required this.strokeWidth,
required this.radius,
required this.gradient,
});
final Paint _paint = Paint();
final double radius;
final double strokeWidth;
final Gradient gradient;
void paint(Canvas canvas, Size size) {
Rect outerRect = Offset.zero & size;
var outerRRect =
RRect.fromRectAndRadius(outerRect, Radius.circular(radius));
Rect innerRect = Rect.fromLTWH(strokeWidth, strokeWidth,
size.width - strokeWidth * 2, size.height - strokeWidth * 2);
var innerRRect = RRect.fromRectAndRadius(
innerRect, Radius.circular(radius - strokeWidth));
_paint.shader = gradient.createShader(outerRect);
Path path1 = Path()..addRRect(outerRRect);
Path path2 = Path()..addRRect(innerRRect);
var path = Path.combine(PathOperation.difference, path1, path2);
canvas.drawPath(path, _paint);
}
bool shouldRepaint(CustomPainter oldDelegate) => oldDelegate != this;
}
child: CustomPaint(
painter: _GradientPainter(
strokeWidth: 1,
radius: 12,
gradient: RadialGradient(
radius: 1.6,
center: Alignment.topLeft,
colors: [
Colors.white.withOpacity(0.5),
Colors.white.withOpacity(0.1),
],
),
),
// child: Container(
...
//土台となるContainer
...
),
わかりにくいとは思いますが、strokeにグラデーションがかかっています。(こだわった私にははっきりみえています。())
影をつける
最後に影をつけていきます。ここでも、半透明の影響で単純に影をつけるだけでは、影全体を透過してしまいます。
そこで、BoxShadow
のプロパティであるblurStyle
でBlurStyle.outer
を指定してあげます。
Container(
decoration: ShapeDecoration(
shadows: const [
BoxShadow(
color: Colors.black12,
offset: Offset(0, 0),
blurRadius: 15,
blurStyle: BlurStyle.outer,
)
],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(12),
),
),
),
width: width,
height: height,
child: ClipRect(
),
),
これで、glassmorphismの完成です。
コード全体
glass_container.dart
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
class GlassContainer extends StatelessWidget {
const GlassContainer({
super.key,
required this.color,
required this.width,
required this.height,
required this.child,
this.radius = 12,
});
final Color color;
final double width;
final double height;
final Widget child;
final double radius;
Widget build(BuildContext context) {
return
Container(
decoration: ShapeDecoration(
shadows: const [
BoxShadow(
color: Colors.black12,
offset: Offset(0, 0),
blurRadius: 15,
blurStyle: BlurStyle.outer,
)
],
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(radius),
),
),
),
width: width,
height: height,
child: ClipRect(
child: BackdropFilter(
filter: ui.ImageFilter.blur(
sigmaX: 5,
sigmaY: 5,
),
child: CustomPaint(
painter: _GradientPainter(
strokeWidth: 1,
radius: radius,
gradient: RadialGradient(
radius: 1.6,
center: Alignment.topLeft,
colors: [
color.withOpacity(0.5),
color.withOpacity(0.1),
],
),
),
child: Container(
alignment: Alignment.center,
width: width,
height: height,
decoration: ShapeDecoration(
gradient: RadialGradient(
radius: 1.6,
center: Alignment.topLeft,
colors: [
color.withOpacity(0.3),
color.withOpacity(0.1),
],
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(radius),
),
),
),
child: child,
),
),
),
),
);
}
}
class _GradientPainter extends CustomPainter {
_GradientPainter({
required this.strokeWidth,
required this.radius,
required this.gradient,
});
final Paint _paint = Paint();
final double radius;
final double strokeWidth;
final Gradient gradient;
void paint(Canvas canvas, Size size) {
Rect outerRect = Offset.zero & size;
var outerRRect =
RRect.fromRectAndRadius(outerRect, Radius.circular(radius));
Rect innerRect = Rect.fromLTWH(strokeWidth, strokeWidth,
size.width - strokeWidth * 2, size.height - strokeWidth * 2);
var innerRRect = RRect.fromRectAndRadius(
innerRect, Radius.circular(radius - strokeWidth));
_paint.shader = gradient.createShader(outerRect);
Path path1 = Path()..addRRect(outerRRect);
Path path2 = Path()..addRRect(innerRRect);
var path = Path.combine(PathOperation.difference, path1, path2);
canvas.drawPath(path, _paint);
}
bool shouldRepaint(CustomPainter oldDelegate) => oldDelegate != this;
}
view_screen.dart
import 'package:flutter/material.dart';
import 'ui/widget/container/glass_container.dart';
class ViewScreen extends StatelessWidget {
const ViewScreen({Key? key}) : super(key: key);
static Route<void> route() {
return MaterialPageRoute<dynamic>(
builder: (_) => const ViewScreen(),
);
}
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Colors.yellow, Colors.blue],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Stack(
alignment: Alignment.center,
children: [
const Align(
alignment: Alignment(-0.6, -0.25),
child: CircleAvatar(
radius: 30,
),
),
Align(
alignment: const Alignment(0.6, 0.25),
child: Container(
width: 60,
height: 60,
color: Colors.yellow,
),
),
const GlassContainer(
width: 200,
height: 200,
color: Colors.white,
child: Text(
"Flutterで作る\nglassmorphism",
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
);
}
}
参考にしたもの
最後に
この記事で、よりよいUIを実装でするきっかけになればと思います。
ここまでご覧いただきありがとうございました。
Discussion