🪟

【Flutter】glassmorphismを作成した

2023/08/18に公開

はじめに

今更ながら、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をかける

ClipRectBackdropFilterを用いてblurをかけていきます。

import 'dart:ui' as ui;
            ClipRect(
              child: BackdropFilter(
                filter: ui.ImageFilter.blur(
                  sigmaX: 5,
                  sigmaY: 5,
                ),
                child: Container(
		...
		//土台となるContainer
		...
                ),
              ),
            ),

少しそれますが、blurのプロパティであるsigmaXsigmaYについて、このプロパティは背景のぼかしの強度に関わります。数字に強度が比例します。

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のプロパティであるblurStyleBlurStyle.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,
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

参考にしたもの

https://note.com/tkc_tsukuru/n/neec95c3c5d83


最後に

この記事で、よりよいUIを実装でするきっかけになればと思います。

ここまでご覧いただきありがとうございました。

Discussion