18
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-05-11

目次

作成するアニメーション

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;
}

応用

18
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?