🥃

FlutterでちょこっとGlassmorphism

2021/01/23に公開1

最近話題(?)のGlassmorphicなUIをFlutterでちょこっと試してみました。
Material DesignのWidgetをベースにそれっぽくしただけなので、細かいところはご自身で修正しながら色々試してみてください。

できたもの

glassmorphic_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

import '../widgets/glass_app_bar.dart';
import '../widgets/glass_button.dart';
import '../widgets/glass_container.dart';
import '../widgets/glass_drawer.dart';
import '../widgets/glass_floating_action_button.dart';
import '../widgets/glass_list_tile.dart';
import '../widgets/glass_text.dart';

class GlassmorphismScreen extends StatelessWidget {
  
  Widget build(BuildContext context) {
    // 画面上部のバーを透明にする
    SystemChrome.setSystemUIOverlayStyle(
      SystemUiOverlayStyle(statusBarColor: Colors.transparent),
    );

    return Stack(
      children: [
        Container(
          decoration: BoxDecoration(
            image: const DecorationImage(
              image: AssetImage('path/to/background/image'),
              fit: BoxFit.cover,
            ),
          ),
        ),
        Scaffold(
          backgroundColor: Colors.transparent,
          drawerScrimColor: Colors.black.withOpacity(0.2),
          extendBodyBehindAppBar: true,
          appBar: GlassAppBar(
            title: const GlassText('GlassAppBar'),
            centerTitle: true,
          ),
          drawer: const GlassDrawer(),
          body: SafeArea(
            child: SingleChildScrollView(
              padding: const EdgeInsets.all(16),
              child: Column(
                children: [
                  GlassContainer(
                    width: 300,
                    height: 300,
                    child: Center(
                      child: GlassText(
                        'GlassContainer',
                        fontSize: 32,
                      ),
                    ),
                  ),
                  const SizedBox(height: 32),
                  GlassListTile(
                    leading: CircleAvatar(
                      backgroundColor: Colors.white.withOpacity(0.3),
                    ),
                    title: const GlassText('GlassListTile'),
                    subtitle: const GlassText(
                      'Subtitle',
                      opacity: 0.2,
                    ),
                    trailing: Icon(
                      Icons.arrow_forward_ios,
                      color: Colors.white.withOpacity(0.3),
                    ),
                    onTap: () {},
                    onLongPress: () {},
                  ),
                  const SizedBox(height: 32),
                  GlassButton(
                    child: const GlassText('GlassButton'),
                    onPressed: () {},
                  ),
                  const SizedBox(height: 32),
                  GlassButton(
                    child: const GlassText('Show GlassBottomSheet'),
                    onPressed: () {
                      const double radius = 16;
                      final BorderRadius borderRadius = BorderRadius.vertical(
                        top: Radius.circular(16),
                      );
                      showModalBottomSheet(
                        context: context,
                        shape:
                            RoundedRectangleBorder(borderRadius: borderRadius),
                        backgroundColor: Colors.transparent,
                        barrierColor: Colors.black.withOpacity(0.2),
                        builder: (BuildContext ctx) {
                          return GlassContainer(
                            radius: radius,
                            borderRadius: borderRadius,
                            child: Center(
                              child: GlassText(
                                'GlassBottomSheet',
                                fontSize: 32,
                              ),
                            ),
                          );
                        },
                      );
                    },
                  ),
                ],
              ),
            ),
          ),
          floatingActionButton: GlassFloatingActionButton(
            child: Icon(
              Icons.add,
              color: Colors.white.withOpacity(0.4),
            ),
            onPressed: () {},
          ),
        ),
      ],
    );
  }
}

GlassText

glass_text.dart
import 'package:flutter/material.dart';

class GlassText extends StatelessWidget {
  const GlassText(
    this.data, {
    this.color = Colors.white,
    this.opacity = 0.4,
    this.fontSize,
    this.fontWeight = FontWeight.bold,
  });

  final String data;
  final Color color;
  final double opacity;
  final double fontSize;
  final FontWeight fontWeight;

  
  Widget build(BuildContext context) {
    return Text(
      data,
      style: TextStyle(
        color: color.withOpacity(opacity),
        fontSize: fontSize,
        fontWeight: fontWeight,
      ),
    );
  }
}

GlassContainer

glass_container.dart
import 'dart:ui';

import 'package:flutter/material.dart';

class GlassContainer extends StatelessWidget {
  GlassContainer({
    this.width,
    this.height,
    this.radius = 16,
    BorderRadius borderRadius,
    this.blur = 20,
    this.child,
  }) {
    this.borderRadius = borderRadius ?? BorderRadius.circular(radius);
  }

  final double width;
  final double height;
  final double radius;
  BorderRadius borderRadius;
  final double blur;
  final Widget child;

  
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        boxShadow: [
          BoxShadow(
            blurRadius: 24,
            spreadRadius: 16,
            color: Colors.black.withOpacity(0.2),
          )
        ],
      ),
      // ぼかす範囲をContainerの背後のみにする
      child: ClipRRect(
        borderRadius: borderRadius,
        child: BackdropFilter(
          // 背景をぼかす
          filter: ImageFilter.blur(
            sigmaX: blur,
            sigmaY: blur,
          ),
          child: Container(
            height: height,
            width: width,
            decoration: BoxDecoration(
              color: Colors.white.withOpacity(0.2),
              border: Border.all(
                width: 1.5,
                color: Colors.white.withOpacity(0.2),
              ),
              borderRadius: borderRadius,
            ),
            child: child,
          ),
        ),
      ),
    );
  }
}

GlassListTile

glass_list_tile.dart
import 'package:flutter/material.dart';

import './glass_container.dart';

class GlassListTile extends StatelessWidget {
  const GlassListTile({
    Key key,
    this.borderRadius = 16,
    this.leading,
    this.title,
    this.subtitle,
    this.trailing,
    this.onTap,
  }) : super(key: key);

  final double borderRadius;
  final Widget leading;
  final Widget title;
  final Widget subtitle;
  final Widget trailing;
  final void Function() onTap;

  
  Widget build(BuildContext context) {
    return GlassContainer(
      radius: borderRadius,
      child: Center(
        child: Material(
          color: Colors.transparent,
          child: ListTile(
            leading: leading,
            title: title,
            subtitle: subtitle,
            trailing: trailing,
            onTap: onTap,
          ),
        ),
      ),
    );
  }
}

GlassButton

glass_button.dart
import 'package:flutter/material.dart';

import './glass_container.dart';

class GlassButton extends StatelessWidget {
  const GlassButton({
    this.radius = 16,
    this.child,
     this.onPressed,
    this.onLongPressed,
  });

  final double radius;
  final Widget child;
  final void Function() onPressed;
  final void Function() onLongPressed;

  
  Widget build(BuildContext context) {
    return RawMaterialButton(
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(radius),
      ),
      onPressed: onPressed,
      onLongPress: onLongPressed,
      child: GlassContainer(
        radius: radius,
        child: Padding(
          padding: const EdgeInsets.all(10),
          child: child,
        ),
      ),
    );
  }
}

GlassAppBar

glass_app_bar.dart
import 'package:flutter/material.dart';

import './glass_container.dart';

class GlassAppBar extends StatelessWidget with PreferredSizeWidget {
  GlassAppBar({
    this.leading,
    this.title,
    this.centerTitle,
  }) : preferredSize = Size.fromHeight(kToolbarHeight);

  final Widget leading;
  final Widget title;
  final bool centerTitle;

  
  final Size preferredSize;

  
  Widget build(BuildContext context) {
    return AppBar(
      backgroundColor: Colors.transparent,
      flexibleSpace: const GlassContainer(radius: 0),
      leading: leading,
      title: title,
      centerTitle: centerTitle,
      iconTheme: const IconThemeData(
        color: Colors.white,
        opacity: 0.4,
      ),
    );
  }
}

GlassDrawer

glass_drawer.dart
import 'package:flutter/material.dart';

import './glass_container.dart';
import './glass_text.dart';

class GlassDrawer extends StatelessWidget {
  const GlassDrawer();

  
  Widget build(BuildContext context) {
    return Theme(
      data: Theme.of(context).copyWith(
        canvasColor: Colors.transparent,
      ),
      child: Drawer(
        child: GlassContainer(
          borderRadius: BorderRadius.only(
            topRight: Radius.circular(16),
            bottomRight: Radius.circular(16),
          ),
          child: ListView(
            padding: const EdgeInsets.all(0),
            children: [
              DrawerHeader(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    CircleAvatar(
                      backgroundColor: Colors.white.withOpacity(0.3),
                      foregroundColor: Colors.white.withOpacity(0.3),
                      radius: 30,
                      child: const Icon(
                        Icons.person,
                        size: 32,
                      ),
                    ),
                    const SizedBox(height: 8),
                    const GlassText(
                      'Glass Drawer',
                      fontSize: 24,
                    ),
                    const GlassText(
                      'example@example.com',
                      opacity: 0.25,
                    ),
                  ],
                ),
                decoration: BoxDecoration(
                  color: Colors.white.withOpacity(0.1),
                ),
              ),
              ListView.separated(
                padding: const EdgeInsets.all(0),
                shrinkWrap: true,
                itemCount: 5,
                itemBuilder: (BuildContext context, int index) {
                  return ListTile(
                    title: GlassText('Drawer Item ${index + 1}'),
                    onTap: () {},
                  );
                },
                separatorBuilder: (BuildContext context, int index) {
                  return Divider(
                    thickness: 1.5,
                    indent: 10,
                    endIndent: 10,
                    color: Colors.white.withOpacity(0.1),
                  );
                },
              ),
            ],
          ),
        ),
      ),
    );
  }
}

ListViewにpaddingを設定しないと、Drawerと重なっている部分のみステータスバーの色が透明になりませんでした。

GlassFloatingActionButton

glass_floating_action_button.dart
import 'dart:ui';

import 'package:flutter/material.dart';

class GlassFloatingActionButton extends StatelessWidget {
  const GlassFloatingActionButton({
     this.onPressed,
    this.child,
  });

  final Function() onPressed;
  final Widget child;

  
  Widget build(BuildContext context) {
    return ClipRRect(
      borderRadius: BorderRadius.circular(30),
      child: BackdropFilter(
        filter: ImageFilter.blur(
          sigmaX: 30,
          sigmaY: 30,
        ),
        child: FloatingActionButton(
          elevation: 20,
          onPressed: onPressed,
          backgroundColor: Colors.transparent,
          shape: CircleBorder(
            side: BorderSide(
              color: Colors.white.withOpacity(0.2),
              width: 1.5,
            ),
          ),
          child: child,
        ),
      ),
    );
  }
}

GlassBottomSheet

showModalBottomSheet(
  context: context,
  backgroundColor: Colors.transparent,
  barrierColor: Colors.black.withOpacity(0.2),
  builder: (BuildContext ctx) {
    return GlassContainer(
      child: Center(
        child: GlassText(
          'GlassBottomSheet',
          fontSize: 32,
        ),
      ),
    );
  },
);

参考

Discussion

たも@Flutterくんたも@Flutterくん

めちゃくちゃ参考になりました!

こちらのGlassAppbarを使ってTabを実装したいのですが、
bottomを追加すると、文字が透明になります。

高さはAppbarのクラスから、
preferredSize = Size.fromHeight(toolbarHeight ??
kToolbarHeight + (bottom?.preferredSize.height ?? 0.0));
を取得してきて、Tabを表示するスペースは確保しました。

もしよろしければ、カスタムAppbarでのタブの実装方法も教えていただけると助かります!