12
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

【Flutter】シンプルな波のアニメーション (Simple Wave Animation)

目次

作成するアニメーション

wave.gif

ベースプログラム

これが今回のベースとなるプログラムです。
一番下の_WaveViewStateにアニメーションの実装をしていきます。

main.dart
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // システムバー・ナビゲーションバーの色
    SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
      statusBarColor: Colors.transparent,
      statusBarIconBrightness: Brightness.dark,
      statusBarBrightness: Brightness.light,
      systemNavigationBarColor: Colors.white,
      systemNavigationBarDividerColor: Colors.grey,
      systemNavigationBarIconBrightness: Brightness.dark,
    ));
    return MaterialApp(
      debugShowCheckedModeBanner: false, // DEBUGバナー削除
      title: 'Wave Animation',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: WaveView(),
    );
  }
}

class WaveView extends StatefulWidget {
  @override
  _WaveViewState createState() => _WaveViewState();
}

class _WaveViewState extends State<WaveView> {
  // ...
}


コーディング手順

1. アニメーションを作る準備

// 1
class _WaveViewState extends State<WaveView> with SingleTickerProviderStateMixin {
  // 2
  AnimationController waveController; // AnimationControllerの宣言
  static const darkBlue = Color(0xFF264bc5); // 波の色

  // 3
  @override
  void initState() {
    waveController = AnimationController(
      duration: const Duration(seconds: 3), // アニメーションの間隔を3秒に設定
      vsync: this, // おきまり
    )..repeat(); // リピート設定

    super.initState();
  }

  // 4
  @override
  void dispose() {
    waveController.dispose(); // AnimationControllerは明示的にdisposeする。
    super.dispose();
  }

  // 5
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: AnimatedBuilder(
        animation: waveController, // waveControllerを設定
        builder: (context, child) => Stack(
          children: <Widget>[
            Text(waveController.value.toString()), // テスト用
            // 本当の要素はこのあと追加します。
          ],
        ),
      ),
    );
  }

}
  1. AnimationControllerを1つだけ扱うのでSingleTickerProviderStateMixinを適用
  2. waveControllerというAnimationControllerを宣言
  3. アニメーションの間隔・リピートを設定
  4. AnimationControllerの破棄
  5. AnimatedBuilderwaveControllerを設定

ここまでできたら一度コンパイルして動かしてみましょう。
waveController.valueが3秒間隔で0~1まで変化していることがわかるはずです。

2. 波の要素を1つ追加

build関数のみ

class _WaveViewState extends State<WaveView> with SingleTickerProviderStateMixin {
  // ...

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: AnimatedBuilder(
        animation: waveController, // waveControllerを設定
        builder: (context, child) => Stack(
          children: <Widget>[
            // ↓ 追加部分
            // 1
            ClipPath(
              // 3
              child: Container(color: darkBlue),
              // 2
              clipper: WaveClipper(context, waveController.value, 0),
            ),
            // ↑ 追加部分
          ],
        ),
      ),
    );
  }
}
  1. クリッピング(切り抜き)用のウイジェットClipPathを追加
  2. 切り抜き対象(child)にContainerを設定(波の色を指定)
  3. 切り抜き方法としてWaveClipperを設定

WaveClipperが未定義であるため,このままでは動きません。
次のステップでWaveClipperを実装します。

3. WaveClipperを実装

WaveClipper_WaveViewStateの外に実装します。

class WaveClipper extends CustomClipper<Path> {
  // 1
  WaveClipper(this.context, this.waveControllerValue, this.offset) {
    final width = MediaQuery.of(context).size.width; // 画面の横幅
    final height = MediaQuery.of(context).size.height; // 画面の高さ

    // coordinateListに波の座標を追加
    for (var i = 0; i <= width / 3; i++) {
      final step = (i / width) - waveControllerValue;
      coordinateList.add(
        Offset(
          i.toDouble() * 3, // X座標
          math.sin(step * 2 * math.pi - offset) * 45 + height * 0.5, // Y座標
        ),
      );
    }
  }

  final BuildContext context;
  final double waveControllerValue; // waveController.valueの値
  final double offset; // 波のずれ
  final List<Offset> coordinateList = []; // 波の座標のリスト

  // 2
  @override
  Path getClip(Size size) {
    final path = Path()
      // addPolygon: coordinateListに入っている座標を直線で結ぶ。
      //             false -> 最後に始点に戻らない
      ..addPolygon(coordinateList, false)
      ..lineTo(size.width, size.height) // 画面右下へ
      ..lineTo(0, size.height) // 画面左下へ
      ..close(); // 始点に戻る
    return path;
  }

  // 3
  // 再クリップするタイミング -> animationValueが更新されていたとき
  @override
  bool shouldReclip(WaveClipper oldClipper) =>
      waveControllerValue != oldClipper.waveControllerValue;
}
  1. コンストラクタを定義, coordinateListの初期化を行います。
  2. 切り抜き方法を定義, 引数のsizeは画面のサイズです。
  3. 再クリップの判定方法を定義, 返り値がtrueなら再クリップされます。

これで動くようになりました。リロードしてみましょう。波が一つ追加されているはずです。

4. 波をもう一つ追加

build関数のみ

class _WaveViewState extends State<WaveView> with SingleTickerProviderStateMixin {
  // ...

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: AnimatedBuilder(
        animation: waveController, // waveControllerを設定
        builder: (context, child) => Stack(
          children: <Widget>[
            // 1つ目の波
            ClipPath(
              child: Container(color: darkBlue),
              clipper: WaveClipper(context, waveController.value, 0),
            ),
            // 2つ目の波
            ClipPath(
              child: Container(color: darkBlue.withOpacity(0.6)),
              clipper: WaveClipper(context, waveController.value, 0.5),
            ),
          ],
        ),
      ),
    );
  }

最終的なプログラム(_WaveViewStateWaveClipperのみ)

main.dart
class _WaveViewState extends State<WaveView>
    with SingleTickerProviderStateMixin {
  AnimationController waveController; // AnimationControllerの宣言
  static const darkBlue = Color(0xFF264bc5); // 波の色

  @override
  void initState() {
    waveController = AnimationController(
      duration: const Duration(seconds: 3), // アニメーションの間隔を3秒に設定
      vsync: this, // おきまり
    )..repeat(); // リピート設定

    super.initState();
  }

  @override
  void dispose() {
    waveController.dispose(); // AnimationControllerは明示的にdisposeする。
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: AnimatedBuilder(
        animation: waveController, // waveControllerを設定
        builder: (context, child) => Stack(
          children: <Widget>[
            // 1つ目の波
            ClipPath(
              child: Container(color: darkBlue),
              clipper: WaveClipper(context, waveController.value, 0),
            ),
            // 2つ目の波
            ClipPath(
              child: Container(color: darkBlue.withOpacity(0.6)),
              clipper: WaveClipper(context, waveController.value, 0.5),
            ),
          ],
        ),
      ),
    );
  }
}

class WaveClipper extends CustomClipper<Path> {
  WaveClipper(this.context, this.waveControllerValue, this.offset) {
    final width = MediaQuery.of(context).size.width; // 画面の横幅
    final height = MediaQuery.of(context).size.height; // 画面の高さ

    // coordinateListに波の座標を追加
    for (var i = 0; i <= width / 3; i++) {
      final step = (i / width) - waveControllerValue;
      coordinateList.add(
        Offset(
          i.toDouble() * 3, // X座標
          math.sin(step * 2 * math.pi - offset) * 45 + height * 0.5, // Y座標
        ),
      );
    }
  }

  final BuildContext context;
  final double waveControllerValue; // waveController.valueの値
  final double offset; // 波のずれ
  final List<Offset> coordinateList = []; // 波の座標のリスト

  @override
  Path getClip(Size size) {
    final path = Path()
      // addPolygon: coordinateListに入っている座標を直線で結ぶ。
      //             false -> 最後に始点に戻らない
      ..addPolygon(coordinateList, false)
      ..lineTo(size.width, size.height) // 画面右下へ
      ..lineTo(0, size.height) // 画面左下へ
      ..close(); // 始点に戻る
    return path;
  }

  // 再クリップするタイミング -> animationValueが更新されていたとき
  @override
  bool shouldReclip(WaveClipper oldClipper) =>
      waveControllerValue != oldClipper.waveControllerValue;
}

応用

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
12
Help us understand the problem. What are the problem?