【Flutter】AnimatedWidgetで簡単アニメーション!見せ方が変わる

対象者

  • Flutterを使用したアプリ開発の経験があり、アニメーションの実装に関心がある方
  • 「AnimatedWidget」の基本的な概念と具体的な利用法を理解し、自身のプロジェクトに適用したいと考えている方
  • 自身の技術スキルを向上させ、開発者としてのキャリアを一段と進めたいと思っている方

はじめに

あなたはFlutterを使ったアプリ開発に取り組んでいるエンジニアとして、もっとユーザーの体験を向上させたいと思っているかもしれません。その一環として、自然で滑らかなアニメーションを実装したいと考えていることでしょう。だからこそ、この記事はあなたにとって価値ある情報を提供できることでしょう。

この記事では、アニメーションの実装に必要な「AnimatedWidget」に焦点を当て、その役割と利用法について解説します。それらの知識があなたのスキルを一段と高め、今後のアプリ開発を円滑に進める手助けとなることを願っています。

AnimatedWidgetとは何か

Flutterのアニメーションにおける基本的な概念

Flutterのアニメーションシステムは多彩な表現力を持っていますが、その中心的な存在がAnimatedWidgetと言えます。Flutterのアニメーションは一般的には、AnimationControllerとTweenという2つの主要な要素から成り立っています。これらを組み合わせて、独自のアニメーションを作成することができます。

具体的には、AnimationControllerはアニメーションの時間を制御し、Tweenはアニメーションの範囲を定義します。そして、AnimatedWidgetはこの2つを統合して、アニメーションの変化を実際のWidgetに反映させる役割を果たします。

late AnimationController _controller = AnimationController(
    duration: const Duration(seconds: 4),
    vsync: this,
);
late Animation<double> _animation =
  Tween<double>(begin: 1, end: 8).animate(_controller);

上記のコードは、4秒間で1から8まで変化するアニメーションを制御する基本的なコードです。

AnimatedWidgetの役割と使用例

AnimatedWidgetは、Flutterでアニメーションを扱うための基本的なWidgetで、特定の値が変化するときにその変化を自動的に描画に反映します。AnimatedWidgetはアニメーションオブジェクトをリッスンし、アニメーションの値が変化すると自動的に再描画されます。この特性を利用することで、効率的にアニメーションを制御することができます。

また、AnimatedWidgetは自身を継承してさまざまな種類のアニメーションWidgetを作成することが可能で、これにより非常に豊かなアニメーション表現が可能となります。

class ButtonTransition extends AnimatedWidget {
  const ButtonTransition({required Animation<double> animation})
      : super(listenable: animation);

  Animation<double> get width => listenable as Animation<double>;

  @override
  Widget build(BuildContext context) {
    return OutlinedButton(
      onPressed: () => print('Hello'),
      child: Text('Clicke me!'),
      style: OutlinedButton.styleFrom(side: BorderSide(width: width.value)),
    );
  }
}

このコードのポイントを以下の3つです。

  1. AnimatedWidgetの継承

    • ButtonTransitionクラスはAnimatedWidgetを継承しています。これにより、アニメーションの状態を監視しながら、ウィジェットを再構築することが可能になります。
  2. Animationの受け取り

    • コンストラクタでAnimation<double> animationを受け取り、super(listenable: animation)として親クラスに渡しています。これにより、このアニメーションの状態の変更に応じてウィジェットが更新されます。
    • widthプロパティを介して、受け取ったアニメーションオブジェクトを利用します。width.valueで現在のアニメーションの値を取得できます。
  3. buildメソッドの実装

    • buildメソッド内で、アニメーションの値をもとに、OutlinedButtonのスタイルを動的に変更しています。具体的には、ボタンのボーダーの幅をwidth.valueで取得した現在のアニメーションの値として設定しています。

このクラスを使用すると、ボタンのボーダーの幅をアニメーションさせることができます。例えば、ボーダーの幅を1から8まで変化させるアニメーションを作成し、このクラスでそのアニメーションを表示することが可能です。

AnimatedWidgetはその使いやすさと柔軟性から、Flutterでアニメーションを扱う上で非常に重要な役割を果たしています。

詰まった点

カスタムAnimatedWidgetの引数をAnimationController

AnimationControllerのvalueでも値はとれてビルドエラーは出ない。だが値が0 から1までしか変化しないので、外観的になにも反映されなかった。

まとめ

Flutterのアニメーションでは、AnimatedWidgetとその派生ウィジェットを利用することが一般的です。AnimatedWidgetは、状態の変化に応じてウィジェットのビジュアルをスムーズに変化させるための抽象クラスで、そのサブクラスとしてはAnimatedContainerやAnimatedOpacityなどがあります。今回は、派生クラスを作成して実装してみました。

Flutterでアニメーションを作成する際の考慮点としては、ユーザー体験を高めるための手段であることを忘れず、複雑すぎず、シンプルで明確なアニメーションを心がけることが重要です。さらに、アニメーションの挙動は全体のデザインやユーザーの期待と一致するように調整する必要があります。

AnimatedWidgetを活用することで、より魅力的で直感的なUIを作成することができ、ユーザー体験を向上させることができます。アニメーションはユーザーにフィードバックを提供し、またアプリ全体のブランディングにも寄与します。

参考

ソース(main.dartにコピペして動作確認用)

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller = AnimationController(
    duration: const Duration(seconds: 2),
    vsync: this,
  )..repeat(reverse: true);
  late Animation<double> _animation =
      Tween<double>(begin: 1, end: 8).animate(_controller);

  @override
  void initState() {
    super.initState();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          children: [
            ButtonTransition(
              animation: _animation,
            ),
            ButtonTransition2(
              controller: _controller,
            ),
          ],
        ),
      ),
    );
  }
}

class ButtonTransition extends AnimatedWidget {
  const ButtonTransition({required Animation<double> animation})
      : super(listenable: animation);

  Animation<double> get width => listenable as Animation<double>;

  @override
  Widget build(BuildContext context) {
    return OutlinedButton(
      onPressed: () => print('Hello'),
      child: Text('Clicke me!'),
      style: OutlinedButton.styleFrom(side: BorderSide(width: width.value)),
    );
  }
}

class ButtonTransition2 extends AnimatedWidget {
  ButtonTransition2({required AnimationController controller})
      : super(listenable: controller);

  Animation<double> get value => listenable as Animation<double>;

  late final _width = Tween<double>(begin: 1, end: 8).animate(value);

  late final _color =
      ColorTween(begin: Colors.blue, end: Colors.yellow).animate(value);

  @override
  Widget build(BuildContext context) {
    return OutlinedButton(
      onPressed: () => print('Hello'),
      child: Text('Clicke me!'),
      style: OutlinedButton.styleFrom(
        side: BorderSide(width: _width.value, color: _color.value!),
      ),
    );
  }
}