🎬

flutter_hooksでアニメーションを作ってみた!

2024/01/10に公開

HookWidgetを使ってみたい!

Flutterは簡単にアニメーションをつけることができる。簡単と言っても知識は必要。前回アニメーションの勉強をする記事を書いて、それを参考に記事を書いたのと、StatefulWidgetHookWidgetでは使える機能が異なるので、そこの解説をしたいと思いました。

今回はこんなものを作ってみた!
https://www.youtube.com/shorts/IaMtJEodKwk

これがサンプルコード

今回は作り方の解説より、アニメーションやってみたよ〜って記事ですので、全部内容は書きません。サンプルコードを参考にしてみてください。
https://github.com/sakurakotubaki/FlutterHooksAnimation

前回書いたアニメーションの記事:
https://zenn.dev/joo_hashi/articles/22fdb4ed96b91b

Flutter Genを使って画像とフォントの設定はしてます
https://zenn.dev/joo_hashi/articles/ac04c688c394f8

補足情報

StatefulWidgetでないとTickerProviderStateMixinクラスが使えないので、HookWidgetで同じような機能を使うときは、useSingleTickerProviderを使います。他に違いがあるとしたら、flutter_hooksだと状態の破棄は勝手にやってくれるので、disposeメソッドを書かないところでしょうか...

これが、flutter_hooksで使えるアニメーションの機能たち。リファレンス見てもさっぱりわからん???
https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useSingleTickerProvider.html

https://pub.dev/documentation/flutter_hooks/latest/flutter_hooks/useAnimationController.html

作ったもの

ふわ〜っと文字が出てきて、ボタンに変わって次のページは画面遷移するアニメーションのDEMOアプリです。

🔚NextPageクラス

これは次のページで表示する画像とテキストをラップしてるコンテナがくるくる回るアニメーションです。今日までこれ仕組みが理解できなかった😅

これが良くある方法:

StatefulWidgetの場合
import 'package:flutter/material.dart';
import 'dart:math' as math;

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

  
  State<AnimatedBuilderExample> createState() => _AnimatedBuilderExampleState();
}

///AnimationController は `vsync:this` で作成できます。
/// TickerProviderStateMixin.
class _AnimatedBuilderExampleState extends State<AnimatedBuilderExample>
    with TickerProviderStateMixin {
  // lateをAnimationControllerにつけると、初期化を遅らせることができる。..repeat()で繰り返しアニメーションを行う。
  late final AnimationController _controller = AnimationController(
    duration: const Duration(seconds: 10),
    vsync: this,
  )..repeat();

  // AnimationControllerを破棄する必要があるので、dispose()をオーバーライドする。
  
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    // AnimatedBuilderは、アニメーションの値を受け取り、ウィジェットを構築する。
    return Scaffold(
      appBar: AppBar(
        title: const Text('AnimatedBuilder Example'),
      ),
      body: Center(
        child: AnimatedBuilder(
          animation: _controller, // AnimationControllerを渡す。
          child: Container(
            width: 200.0,
            height: 200.0,
            color: Colors.green,
            child: const Center(
              child: Text('くるくる回っちゃうもんね〜'),
            ),
          ),
          builder: (BuildContext context, Widget? child) {
            // Transform.rotateで回転させる。
            return Transform.rotate(
              angle: _controller.value * 2.0 * math.pi,
              child: child,
            );
          },
        ),
      ),
    );
  }
}

HookWidgetの場合:

Hookの場合
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_animaition/gen/assets.gen.dart';

class NextPage extends HookWidget {
  const NextPage({super.key});

  
  Widget build(BuildContext context) {
    // 画像がくるくる回るアニメーションを制御するためのuseAnimationController
    final controller = useAnimationController(
      duration: const Duration(seconds: 3),
      vsync: useSingleTickerProvider(),
    )..repeat();
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: AnimatedBuilder(
          animation: controller, // AnimationControllerを渡す。
          child: Container(
            width: 200.0,
            height: 200.0,
            color: Colors.green,
            child: Center(
              child: Column(
                children: [
                  Assets.images.orechan.image(
                    width: 100,
                    height: 100,
                  ),
                  const Text('くるくる回っちゃうもんね〜'),
                ],
              ),
            ),
          ),
          builder: (BuildContext context, Widget? child) {
            // Transform.rotateで回転させる。
            return Transform.rotate(
              angle: controller.value * 2.0 * math.pi,
              child: child,
            );
          },
        ),
      ),
    );
  }
}

🔜FirstPageクラス

最初のページは、画面が呼ばれるとおしゃれな文字ってわけではないですがアニメーションが表示されて、次のページへ画面遷移するボタンに変わります。StatefulWidgetのときは、initStatesetStateを使ってますが、HookWidgetのときは、useStateで状態の管理をして、ページが呼ばれたら、useEffectでアニメーションを実行するメソッドを実行しています。

StatefulWidgetの場合
import 'package:flutter/material.dart';

class AnimatedCrossFadeExample extends StatefulWidget {
  const AnimatedCrossFadeExample({Key? key}) : super(key: key);

  
  _AnimatedCrossFadeExampleState createState() =>
      _AnimatedCrossFadeExampleState();
}

class _AnimatedCrossFadeExampleState extends State<AnimatedCrossFadeExample> {
  // 初期値がtrueなので、最初はfirstChildが表示される
  bool _first = true;

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

  Future<void> _loadAnimation() async {
    // 画面が呼ばれたときにアニメーションを開始
    Future.delayed(Duration.zero, () {
      setState(() {
        _first = !_first;
      });
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('AnimatedCrossFade Example'),
      ),
      body: Center(
        child: AnimatedCrossFade(
          duration: const Duration(seconds: 3), // 3秒かけてアニメーションする
          // firstChildは最初に表示されるWidget
          firstChild: const FlutterLogo(
              style: FlutterLogoStyle.horizontal, size: 100.0),
          // secondChildはfirstChildが消えた後に表示されるWidget
          secondChild:
              const FlutterLogo(style: FlutterLogoStyle.stacked, size: 100.0),
          crossFadeState:
              _first ? CrossFadeState.showFirst : CrossFadeState.showSecond,
        ),
      ),
    );
  }
}
Hookの場合
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_animaition/gen/fonts.gen.dart';
import 'package:hooks_animaition/next_page.dart';

class FirstPage extends HookWidget {
  const FirstPage({super.key});

  
  Widget build(BuildContext context) {
    // bool型の値を保持する変数を宣言
    final firstBool = useState<bool>(true);
    // 3秒後にfirstBoolの値を反転させる関数
    Future<void> loadAnimation() async {
      await Future.delayed(Duration.zero, () {
        firstBool.value = !firstBool.value;
      });
      firstBool.value = false;
    }
    // 画面が表示された時にloadAnimationを実行する
    useEffect(() {
      loadAnimation();
      return null;
    }, []);

    return Scaffold(
      appBar: AppBar(
        title: const Text(
          'First Page!',
          style: TextStyle(
            fontFamily: FontFamily.rubikDoodleShadow,
            fontSize: 25,
            color: Colors.deepPurple,
          ),
        ),
      ),
      body: Center(
        child: AnimatedCrossFade(
          duration: const Duration(seconds: 3), // 3秒かけてアニメーションする
          // firstChildは最初に表示されるWidget
          firstChild: const Text(
            'Welcome!',
            style: TextStyle(
              fontFamily: FontFamily.rubikDoodleShadow,
              fontSize: 25,
              color: Colors.deepPurple,
            ),
          ),
          // secondChildはfirstChildが消えた後に表示されるWidget
          secondChild: ElevatedButton(
            onPressed: () {
              Navigator.of(context).push(
                MaterialPageRoute(
                  builder: (context) {
                    return const NextPage();
                  },
                ),
              );
            },
            // ignore: sort_child_properties_last
            child: Ink(
              decoration: BoxDecoration(
                gradient: const LinearGradient(
                  colors: [Colors.red, Colors.purple, Colors.blue],
                ),
                borderRadius: BorderRadius.circular(4),
              ),
              child: Container(
                padding:
                    const EdgeInsets.symmetric(vertical: 10, horizontal: 15),
                child:
                    const Text('次のページへ', style: TextStyle(color: Colors.white)),
              ),
            ),
            style: ElevatedButton.styleFrom(
              padding: EdgeInsets.zero,
            ),
          ),
          crossFadeState: firstBool.value
              ? CrossFadeState.showFirst
              : CrossFadeState.showSecond,
        ),
      ),
    );
  }
}

まとめ

アニメーション機能は時間や位置を制御したり、状態の破棄をするライフサイクルがあるので、難しくて奥が深いな〜と思いました😅
極めたら楽しいんだろうな〜と思ってこれからは、アニメーションを使ったアプリも作りたいですね。

Discussion