Flutterにおける水の波紋アニメーション

Water Wave Animation in Flutterの表紙画像

GitHub
Youtube

さあ始めましょう。
まずはボタンのあるレイアウトを作ります。

import 'package:circular_animation/animation.dart';
import 'package:flutter/material.dart';

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  
  State createState() => _HomePageState();
}

class _HomePageState extends State {
  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: const CircleAvatar(
          radius: 50,
          backgroundColor: Colors.deepPurple,
          child: Icon(Icons.waves_rounded),
        ),
      ),
    );
  }
}

次に本命のロジックの部分です。
これを別ファイル、または同じファイルに追加してください。

// ignore_for_file: must_be_immutable

import 'dart:async';

import 'package:flutter/material.dart';

class WaveAnimation extends StatefulWidget {
  Widget child;
  double outerMostCircleStartRadius;
  double outerMostCircleEndRadius;
  double startOpacity;
  Duration animationTime;
  int numberOfCircles;
  double borderWidth;
  Color borderColor;
  double gap;
  Duration delay;

  WaveAnimation({
    super.key,
    required this.child,
    required this.borderColor,
    required this.outerMostCircleEndRadius,
    required this.outerMostCircleStartRadius,
    this.startOpacity = .1,
    this.numberOfCircles = 2,
    this.delay = const Duration(seconds: 2),
    this.animationTime = const Duration(seconds: 1),
    this.borderWidth = 8.0,
    this.gap = 15.0,
  }) {
    assert(numberOfCircles > 0);
    assert(gap > 0);
    assert(outerMostCircleStartRadius > 0);
    assert(outerMostCircleEndRadius > 0);
    assert(startOpacity > 0);
    assert(delay >= animationTime);
  }
  
  _WaveAnimationState createState() => _WaveAnimationState();
}

class _WaveAnimationState extends State<WaveAnimation>
    with TickerProviderStateMixin {
  AnimationController? _radiusOpacityController;
  Animation? _radiusAnimation;
  Animation? _opacityAnimation;

  
  void initState() {
    super.initState();
    _setupAnimation();
  }

  void _setupAnimation() {
    _radiusOpacityController = AnimationController(
      vsync: this,
      duration: widget.animationTime,
    )..addListener(() {
        setState(() {});
      });

    Timer(widget.delay, () {
      _radiusOpacityController?.reset();
      _radiusOpacityController?.forward();
    });

    _radiusAnimation = Tween(
            begin: widget.outerMostCircleStartRadius,
            end: widget.outerMostCircleEndRadius)
        .animate(
      CurvedAnimation(
        parent: _radiusOpacityController!,
        curve: Curves.linear,
      ),
    );

    _opacityAnimation = Tween(begin: widget.startOpacity, end: 0.0).animate(
        CurvedAnimation(
            parent: _radiusOpacityController!, curve: Curves.linear));
  }

  
  void dispose() {
    super.dispose();
    _radiusOpacityController?.dispose();
  }

  
  Widget build(BuildContext context) {
    return Stack(
      alignment: Alignment.center,
      children: [
        Opacity(
          opacity: _opacityAnimation!.value,
          child: CustomPaint(
            painter: _CustomPainter(
              numberOfCircles: widget.numberOfCircles,
              borderWidth: widget.borderWidth,
              borderColor: widget.borderColor,
              gap: widget.gap,
              outermostCircleRadius: _radiusAnimation!.value,
            ),
          ),
        ),
        widget.child,
      ],
    );
  }
}

class _CustomPainter extends CustomPainter {
  int? _numberOfCircles;
  double? _borderWidth;
  Color? _borderColor;
  double? _gap;
  double? _outermostCircleRadius;

  _CustomPainter({
    required int numberOfCircles,
    required double borderWidth,
    required Color borderColor,
    required double gap,
    required double outermostCircleRadius,
  }) {
    _numberOfCircles = numberOfCircles;
    _borderWidth = borderWidth;
    _borderColor = borderColor;
    _gap = gap;
    _outermostCircleRadius = outermostCircleRadius;
  }

  
  void paint(Canvas canvas, Size size) {
    final borderPaint = Paint()
      ..color = _borderColor!
      ..strokeWidth = _borderWidth!
      ..style = PaintingStyle.stroke;

    var center = Offset(size.width / 2, size.height / 2);

    for (int i = 0; i < _numberOfCircles! && _outermostCircleRadius! > 0; i++) {
      final radius = _outermostCircleRadius! - _gap! * i;
      if (radius > 0) {
        canvas.drawCircle(center, radius, borderPaint);
      }
    }
  }

  
  bool shouldRepaint(_CustomPainter oldDelegate) {
    return oldDelegate._outermostCircleRadius != _outermostCircleRadius;
  }
}

このアニメーションを使い方:

import 'package:circular_animation/animation.dart';
import 'package:flutter/material.dart';

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: WaveAnimation(
          borderColor: Colors.black,
          outerMostCircleEndRadius: 100,
          outerMostCircleStartRadius: 50,
          numberOfCircles: 4,
          child: const CircleAvatar(
            radius: 50,
            backgroundColor: Colors.deepPurple,
            child: Icon(Icons.waves_rounded),
          ),
        ),
      ),
    );
  }
}

何をしましたか?

  • 半径と不透明度のアニメーションのためのコントローラーを作成しました
  • アニメーションの時間に応じて、不透明度を減少させ、半径を増加させます
  • 与えられた時間後にアニメーションを再開するためにタイマーを使用します
  • レイアウトにスタックを使用します。私たちの円は、渡された子ウィジェットの下にあります
  • _CustomPainterは円を描くために使用されます
  • 最も外側の円から始めて、内側の円に進み、半径が0より大きい間描き続けます

出力:
画像の説明

このアニメーションでさらに多くの組み合わせを作り、LinkedInで私にタグを付けてください。
LinkedIn

こちらの記事はdev.toの良い記事を日本人向けに翻訳しています。
https://dev.to/raman04byte/water-wave-animation-in-flutter-io5