LoginSignup
5
6

More than 3 years have passed since last update.

useAnimationControllerを使って連続アニメーション

Last updated at Posted at 2021-03-10

動機

「Hooksを使えば、アニメーションがスッキリ書ける」という話を聞いて、興味が湧きました。今までRiverpodを使う際も、hooks_riverpodではなく、何となくflutter_riverpodを使用していました。

噂ではFlutterの開発チームがHooksにはあまり肯定的ではないという声も聞いたのと、Hooksが何となく敷居が高そうなので敬遠していました。ただもう少し調べていくと、Riverpod自体もhooks_riverpodが主流で、Hooksは深く使いこまなくても限定的な利用を出来るものだという意見を多く見かけました。

今後Hooksがどうなるかは分かりませんが、少なくともJavaScriptのReactでは単なる流行りではない新しい概念であるとのことなので、まずは自分も使ってみようと思いました。

今回のコードとアニメーション(null safety対応)

■アニメーションの内容

1.画面左から、ログインIDフォームがスライドイン(duration 0.8秒)
2.画面が起動して0.4秒後に画面左から、0.8秒かけてパスワードフォームがスライドイン(duration 1.2秒)
3.画面が起動して1秒後に、ログインボタンがフェードインしながら、上方向に30pxスライドイン(duration 0.6秒)

Videotogif.gif

  • 今回のコード(Flutter Hooks使用)

今回のサンプルのアニメーションについて、色々考えているうちに時間が経ち、Flutter2.0がリリースされました。まだnull safetyなFlutterのコードのことがイマイチ分かりませんが、今後のコードのメンテナンスのことも考え、今後はライブラリさえ対応していれば、どんどんnull safety対応で記事のサンプルも作成していきたいです。

コードの補足や説明

まずはライブラリのインストールなど。

pubspec.yaml
(略)
environment:
  sdk: ">=2.12.0 <3.0.0" # ここで2.12.0にすることでnull safetyが強制されるそう

dependencies:
  flutter:
    sdk: flutter
  flutter_hooks: ^0.16.0
(略)

今回はアニメーションするログイン画面を作りました。サンプルコードが少し長くなったので、サンプルの方にコメントを残す形にします。

main.dart
import 'dart:ui';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Animation Login Page',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: _AnimationLoginPage(),
    );
  }
}

// ignore: must_be_immutable
class _AnimationLoginPage extends HookWidget {
  final globalKey = GlobalKey();
  double formWidth = 0;

  @override
  Widget build(BuildContext context) {
    AnimationController _loginIdAnimationController = useAnimationController(
      duration: Duration(milliseconds: 800),
    );
    AnimationController _passwordAnimationController = useAnimationController(
      duration: Duration(milliseconds: 1200),
    );
    AnimationController _buttonAnimationController = useAnimationController(
      duration: Duration(milliseconds: 600),
    );

    // ログインIDフォームとログインボタンのアニメーション定義
    Animatable<double> _animatable = Tween<double>(
      begin: 0.0,
      end: 1.0,
    ).chain(
      CurveTween(
        curve: Curves.bounceOut,
      ),
    );

    // パスワードフォームのアニメーション定義
    Animatable<double> _passwordAnimatable = TweenSequence([
      // アニメーションが1.2秒で、最初の0.4秒は待機
      TweenSequenceItem(
        tween: Tween<double>(
          begin: 0.0,
          end: 0.0,
        ),
        weight: 400 / 1200, // 1.2秒のアニメーションのうちの0.4秒
      ),
      // アニメーションが1.2秒で、0.8秒かけてアニメーション
      TweenSequenceItem(
        tween: Tween<double>(
          begin: 0.0,
          end: 1.0,
        ).chain(
          CurveTween(
            curve: Curves.bounceOut,
          ),
        ),
        weight: 800 / 1200, // 1.2秒のアニメーションのうちの0.8秒
      ),
    ]);

    Animation<double> _loginIdAnimation =
        _animatable.animate(_loginIdAnimationController);
    Animation<double> _passwordAnimation =
        _passwordAnimatable.animate(_passwordAnimationController);
    Animation<double> _buttonAnimation =
        _animatable.animate(_buttonAnimationController);

    // Widgetが描画されてから、フォームの長さを取得して、アニメーションを発火させる
    WidgetsBinding.instance?.addPostFrameCallback((_) {
      // フォームのwidthを取得
      RenderBox form =
          globalKey.currentContext!.findRenderObject() as RenderBox;
      formWidth = form.size.width;

      // アニメーションの開始
      _loginIdAnimationController.forward();
      _passwordAnimationController.forward();
      // 1秒後にアニメーション開始
      Future.delayed(Duration(milliseconds: 1000)).then(
        (_) => _buttonAnimationController.forward(),
      );
    });

    return Scaffold(
      appBar: AppBar(
        title: Text(
          'Animation Login Page',
          style: TextStyle(
            fontWeight: FontWeight.bold,
            fontSize: 16,
          ),
        ),
      ),
      body: SingleChildScrollView(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            SizedBox(
              height: 50,
            ),
            Text(
              'Login',
              textAlign: TextAlign.center,
              style: TextStyle(
                fontWeight: FontWeight.bold,
                fontSize: 16,
              ),
            ),
            Padding(
              padding: EdgeInsets.symmetric(horizontal: 35),
              child: Column(
                children: [
                  SizedBox(
                    height: 50,
                  ),
                  AnimatedBuilder(
                    animation: _loginIdAnimation,
                    builder: (BuildContext context, _) {
                      return Transform(
                        transform: _generateFormMatrix(_loginIdAnimation),
                        child: CupertinoTextField(
                          key: globalKey,
                          placeholder: 'id',
                        ),
                      );
                    },
                  ),
                  SizedBox(
                    height: 20,
                  ),
                  AnimatedBuilder(
                    animation: _passwordAnimation,
                    builder: (BuildContext context, _) {
                      return Transform(
                        transform: _generateFormMatrix(_passwordAnimation),
                        child: CupertinoTextField(
                          placeholder: 'password',
                        ),
                      );
                    },
                  ),
                  SizedBox(
                    height: 50,
                  ),
                  AnimatedBuilder(
                    animation: _buttonAnimation,
                    builder: (BuildContext context, _) {
                      return FadeTransition(
                        opacity: _buttonAnimation,
                        child: Transform(
                          transform: _generateButtonMatrix(_buttonAnimation),
                          child: SizedBox(
                            width: double.infinity,
                            child: ElevatedButton(
                              child: Text(
                                'ログイン',
                              ),
                              onPressed: () => null,
                            ),
                          ),
                        ),
                      );
                    },
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }

  /// フォームがスライドするアニメーションの移動量(画面左のPadding(35px) + フォームの長さ)
  Matrix4 _generateFormMatrix(Animation animation) {
    final value = lerpDouble(35.0 + formWidth, 0, animation.value);
    return Matrix4.translationValues(-value!, 0.0, 0.0);
  }

  /// ボタンが上にスライドするアニメーションの移動量(30px)
  Matrix4 _generateButtonMatrix(Animation animation) {
    final value = lerpDouble(30.0, 0, animation.value);
    return Matrix4.translationValues(0.0, value!, 0.0);
  }
}

今回連続アニメーションをすることでコードのボリュームが増えてしまいましたが、シンプル過ぎるとFlutter Hooksの使い心地が分からないと考え、今回のような連続アニメーションのサンプルにしました。

フォームをアニメーションさせる移動量は以下のように考えて設定しました。
スクリーンショット 2021-03-10 22.56.54.png

感想

StatefulWidgetのボイラープレートのコードを捨てることが出来たので、コードは少しスッキリしたものになりました。

  • Hooksを使わなかったバージョンのコード

Hooksを使わなかった場合は225行に対して、Hooksを使用した場合は200行と短くなりました。

しかし慣れの問題もあるとは思いますが、StatefulWidgetのinitStateメソッド内に色々書く習慣があった者としては、少し書きづらさも感じました。
(もう少しHooksの機能などを使いこなせば、また感想が変わるかも知れません)

ちなみに最初の目的であったHookを理解するということに関しては、今回のようにuseAnimationControllerを使っただけでは、Hooksのことは全然分かりませんでした。。また別の何かでHooksの理解を深めたいです。

また連続アニメーションのコード自体にも、もっと良い方法があるのではと、まだまだ思考を重ねる日々です。null safety対応のコードのこともまだよく分からず、今回のサンプルコードはいつも以上にご意見を頂きたいです。

参考記事

5
6
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
5
6