🐝

【Flutter】枠線を光らせる Container & CustomPaint (InnerShadow)

2024/03/01に公開

今回やること

今回は小ネタです 💡
枠線を光らせます。
光っているよう見せます。

想像はつくと思いますが Shadow のお話です。
Flutter では InnerShadow が実装されていないので、
自作で表現する必要があります。

今回は 2 種類の方法で InnerShadow を表現しました。

  1. Container に小さい Container を入れて表現
  2. CustomPaint を使って表現

実行例

どのように表現しているか

簡単に言うと重ねているだけです。

ベースの Container に dropShadow、border、bgColor を付けて
その上に小さい Container をのせて、この Container にも dropShadow を付けます。

CustomPaint の場合は、OuterShadow(緑)→InnerShadow(グレー)→border(薄緑) の順に重ねています。

実行例

Container Ver.

class ShiningContainer extends StatelessWidget {
  final Color borderColor;
  final Color outerColor;
  final Color innerColor;
  final double radius;
  final double thickness;
  final Widget child;
  final double? width;
  final double? height;

  const ShiningContainer({
    Key? key,
    required this.borderColor,
    required this.outerColor,
    required this.innerColor,
    required this.radius,
    required this.thickness,
    required this.child,
    this.width,
    this.height,
  }) : super(key: key);

  
  Widget build(BuildContext context) {
    return Container(
      width: width,
      height: height,
      padding: const EdgeInsets.all(10), // paddingで内側のContainerを小さくしている
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(radius),
        border: Border.all(color: borderColor, width: thickness),
        color: outerColor,
        boxShadow: [
          BoxShadow(
            color: outerColor,
            spreadRadius: 1,
            blurRadius: 15,
          ),
        ],
      ),
      child: Container(
          decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(radius - thickness),
            color: innerColor,
            boxShadow: [
              BoxShadow(
                color: innerColor,
                spreadRadius: 7,
                blurRadius: 7,
              ),
            ],
          ),
          child: child),
    );
  }
}

CustomPaint Ver.

class ShiningBorderPainter extends CustomPainter {
  final Color borderColor;
  final Color outerColor;
  final Color innerColor;
  final double radius;
  final double thickness;
  ShiningBorderPainter(
      {required this.borderColor,
      required this.outerColor,
      required this.innerColor,
      required this.radius,
      required this.thickness});

  
  void paint(Canvas canvas, Size size) {
    final shadowWidth = thickness / 1.5;

    final Paint borderPaint = Paint()
      ..color = borderColor
      ..style = PaintingStyle.stroke
      ..strokeWidth = thickness;
    final outerPaint = Paint()
      ..color = outerColor
      ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 8.0);
    final innerPaint = Paint()
      ..color = innerColor
      ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 8.0);

    final RRect outerRect = RRect.fromLTRBR(0, 0, size.width, size.height, Radius.circular(radius));
    final RRect borderRect = RRect.fromLTRBR(
        shadowWidth, shadowWidth, size.width - shadowWidth, size.height - shadowWidth, Radius.circular(radius));
    final RRect innerRect = RRect.fromLTRBR(shadowWidth * 2, shadowWidth * 2, size.width - shadowWidth * 2,
        size.height - shadowWidth * 2, Radius.circular(radius));

    canvas.drawRRect(outerRect, outerPaint);
    canvas.drawRRect(innerRect, innerPaint);
    canvas.drawRRect(borderRect, borderPaint);
  }

  
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false;
  }
}

終わりに

簡単ではありますが、1 つの表現方法として紹介させていただきました。
もっと簡単に InnerShadow が実現できれば良いのですが。
他に良い方法があればコメントいただけると嬉しいです。

コード全文

import 'package:flutter/material.dart';

class ShiningBorderPage extends StatelessWidget {
  const ShiningBorderPage({super.key});

  
  Widget build(BuildContext context) {
    final Color _shadowColor = Colors.green.shade400;
    final Color _borderColor = Colors.green.shade100;

    return Scaffold(
        backgroundColor: Colors.grey.shade900,
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              ShiningContainer(
                  width: 400,
                  height: 150,
                  outerColor: _shadowColor,
                  borderColor: _borderColor,
                  innerColor: Colors.grey.shade900,
                  radius: 24,
                  thickness: 4,
                  child: const Center(
                    child: Text('Container',
                        style: TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold)),
                  )),
              SizedBox(
                width: 400,
                height: 150,
                child: CustomPaint(
                    painter: ShiningBorderPainter(
                        outerColor: _shadowColor,
                        borderColor: _borderColor,
                        innerColor: Colors.grey.shade900,
                        radius: 24,
                        thickness: 4),
                    child: const Center(
                      child: Text('CustomPaint',
                          style: TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold)),
                    )),
              )
            ],
          ),
        ));
  }
}

class ShiningContainer extends StatelessWidget {
  final Color borderColor;
  final Color outerColor;
  final Color innerColor;
  final double radius;
  final double thickness;
  final Widget child;
  final double? width;
  final double? height;

  const ShiningContainer({
    Key? key,
    required this.borderColor,
    required this.outerColor,
    required this.innerColor,
    required this.radius,
    required this.thickness,
    required this.child,
    this.width,
    this.height,
  }) : super(key: key);

  
  Widget build(BuildContext context) {
    return Container(
      width: width,
      height: height,
      padding: const EdgeInsets.all(10),
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(radius),
        border: Border.all(color: borderColor, width: thickness),
        color: outerColor,
        boxShadow: [
          BoxShadow(
            color: outerColor,
            spreadRadius: 1,
            blurRadius: 15,
          ),
        ],
      ),
      child: Container(
          decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(radius - thickness),
            color: innerColor,
            boxShadow: [
              BoxShadow(
                color: innerColor,
                spreadRadius: 7,
                blurRadius: 7,
              ),
            ],
          ),
          child: child),
    );
  }
}

class ShiningBorderPainter extends CustomPainter {
  final Color borderColor;
  final Color outerColor;
  final Color innerColor;
  final double radius;
  final double thickness;
  ShiningBorderPainter(
      {required this.borderColor,
      required this.outerColor,
      required this.innerColor,
      required this.radius,
      required this.thickness});

  
  void paint(Canvas canvas, Size size) {
    final shadowWidth = thickness / 1.5;

    final Paint borderPaint = Paint()
      ..color = borderColor
      ..style = PaintingStyle.stroke
      ..strokeWidth = thickness;
    final outerPaint = Paint()
      ..color = outerColor
      ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 8.0);
    final innerPaint = Paint()
      ..color = innerColor
      ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 8.0);

    final RRect outerRect = RRect.fromLTRBR(0, 0, size.width, size.height, Radius.circular(radius));
    final RRect borderRect = RRect.fromLTRBR(
        shadowWidth, shadowWidth, size.width - shadowWidth, size.height - shadowWidth, Radius.circular(radius));
    final RRect innerRect = RRect.fromLTRBR(shadowWidth * 2, shadowWidth * 2, size.width - shadowWidth * 2,
        size.height - shadowWidth * 2, Radius.circular(radius));

    canvas.drawRRect(outerRect, outerPaint);
    canvas.drawRRect(innerRect, innerPaint);
    canvas.drawRRect(borderRect, borderPaint);
  }

  
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false;
  }
}

Discussion