📝

【Flutter】自作パッケージ animated_to が DartPad で使えるようになったので遊んでみる

に公開

Flutter / Dart には DartPad という Web ツールがあります。

https://dartpad.dev/

これは 1 ファイルに収まるような小規模の Dart/Flutter コードを Web 上で簡単に試せるツールなのですが、あくまで 1 つの Dart ファイルにコードを書いて動作確認をするものであり、pubspec.yaml を使ったパッケージ管理はできません。その代わり、Dart/Flutter チームがあらかじめ設定した 100 個ほどのパッケージ が使える作りになっています。

依存パッケージの定義はこちらのファイルに列挙されていて、よく見るところだと riverpod や provider、shared_preferences などが使えるようになっているようです。

https://github.com/dart-lang/dart-pad/blob/290442b262bc28b190c11bc7b23b13b2636f149a/pkgs/dart_services/tool/dependencies/pub_dependencies_stable.json

そして今回、私が半年ほど前から公開していた animated_to というパッケージがめでたくこのリストに追加されたので、使い方の紹介がてら実際に DartPad で遊んでみたいと思います!

DartPad を開く

まずは DartPad を開いてみましょう。

https://dartpad.dev/

普段から使っている方にとってはお馴染みの画面だと思いますが、画面左のエディタには以下のようなシンプルな Dart コードが書かれています。

void main() {
  for (var i = 0; i < 10; i++) {
    print('hello ${i + 1}');
  }
}

この状態で "Run" ボタンをクリックすると、実際にコードが実行されて右側に実行結果が表示される仕組みです。

DartPadの実行結果

この DartPad は Flutter のコードも実行できます。画面上部の "Create" から "Flutter snippet" を選択してもう一度実行してみましょう。今度は Flutter の Hello World 画面が表示されたのではないかと思います。

Flutterの実行結果

ちょっとした UI であれば Flutter の環境構築やプロジェクト作成をしなくてもさくっと試せるため、DartPad で Flutter の動作確認(特に Widget の見た目や挙動のチェック)ができることは覚えておいて損はないと思います。

animated_to を使ってみる

では、animated_to で遊んでみましょう。AnimatedTo を使うと、たとえばこんな画面が簡単に作れてしまいます。以下の URL から DartPad を開いて実行してみてください。[1]

https://dartpad.dev/?id=48f0edc4630551e9a812aee27864f2e9

animated_to は 任意の Widget の移動をアニメーションできる パッケージです。

たとえばリビルドによってある Widget が画面上から画面下に場所を変えた場合、通常はリビルドと同時にパッと UI が変わるところを、AnimatedTo という Widget でラップしてあげることでアニメーションしながら場所が更新される という挙動になります。それだけと言えばそれだけです。

試しに四角形 Widget を作って動かしてみましょう。

StatelessWidgetStatefulWidget に変更し、四角形な Widget を配置し、それがボタンタップによって上寄せと下寄せが切り替わるようにしてみます。

import 'package:flutter/material.dart';

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

/// [StatefulWidget] に変更
class MyApp extends StatefulWidget {
  const MyApp({super.key});

  
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  // 現在上寄せかどうかの状態を表すフラグ
  bool _isTop = true; 

  
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: Container(
          width: double.infinity,
          padding: const EdgeInsets.all(16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.center,
            // ここで上寄せか下寄せかを切り替え
            mainAxisAlignment: _isTop ? MainAxisAlignment.start : MainAxisAlignment.end,
            children: [
              // 青い四角形
              Container(
                width: 50,
                height: 50,
                decoration: BoxDecoration(
                  color: Colors.blue,
                  borderRadius: BorderRadius.circular(8),
                ),
              ),
            ],
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () {
            // ボタンがタップされたらフラグを切り替えてリビルド
            setState(() => _isTop = !_isTop);
          },
          child: const Icon(Icons.swap_vert),
        ),
      ),
    );
  }
}

とりあえずボタンをタップすると四角形の場所が変わる画面を作ってみました。一度実行して動作確認をしてみると、まだ四角形がパッと上下を移動するのみです。味気ないですね。

ではいよいよ AnimatedTo の出番です。四角形を表す ContainerAnimatedTo.curve で囲ってみてください。[2]

AnimatedTo.curve(
  child: Container(
    width: 50,
    height: 50,
    decoration: BoxDecoration(
      color: Colors.blue,
      borderRadius: BorderRadius.circular(8),
    ),
  ),
),

すると「import 文が足りない」旨のエラーが出ますので、Command + . ショートカットで Quick Fix を呼び出し、import 文を補完してください。

import文の補完

DartPad の補完メニューに自分のパッケージの名前が、、!すでに満足ですが続けます。

さて、import 文を補完すると今度は引数が足りない旨のエラーが表示されます。

AnimatedTo は Widget の位置が変わった際に RenderObject が失われてしまわないよう、globalKey 引数に GlobalKey を必要とします。

Flutter の Widget ツリーの仕組みとして、通常はある Widget の位置がリビルドによって変わったら一度 ElementRenderObject を破棄して「新しい UI コンポーネント」としてレイアウト計算と描画が行われるのですが、GlobalKey が設定されている場合はリビルドの前後でそれらが使いまわされる ようになっています。AnimatedTo はそれを活用するわけです。

今回は雑に GlobalObjectKey(1) を渡してあげましょう。GlobalObjectKey は、コンストラクタの引数に渡したオブジェクト(今回は 1)が同じ限り同一であると認識される GlobalKey です。

AnimatedTo.curve(
  globalKey: GlobalObjectKey(1),
  child: Container(
    width: 50,
    height: 50,
    decoration: BoxDecoration(
      color: Colors.blue,
      borderRadius: BorderRadius.circular(8),
    ),
  ),
),

globalKey をセットできたら再度実行して動作を見てみましょう。今度はボタンタップ時にアニメーションで位置が変化するはずです。[3]

少しだけ仕組みを説明すると、AnimatedTo はアニメーション中にリビルドを一切発生させません。独自実装の RenderObject により、paintChild() による描画処理だけが毎フレーム連続して行われるため、プラットフォームに関わらずとても滑らかです。

というわけでどうでしょうか?以上が animated_to パッケージのコンセプトです。

これだけといえばこれだけですが、逆にこれしかやらないからこそ 他の Widget と組み合わせてリッチな UI を自由に組み上げるいちパーツとして使いやすい と言えるはずです。たとえば AnimatedTo をさらに AnimatedOpacity などの標準 Widget でラップしてあげることで、「移動しながらスーッと消えていく」ようなアニメーションが簡単に実現できます。いろいろ試してみてください!

物理アニメーション

さて、先ほどは AnimatedTo.curve をつかって "Curve" アニメーションで四角形を動かしてみましたが、AnimatedTo にはもうひとつ AnimatedTo.spring というコンストラクタが用意されています。

AnimatedTo.spring(
  globalKey: GlobalObjectKey(1),
  child: Container(
    width: 50,
    height: 50,
    decoration: BoxDecoration(
      color: Colors.blue,
      borderRadius: BorderRadius.circular(8),
    ),
  ),
),

AnimatedTo.spring はアニメーションの挙動を "Spring" アニメーションに変更します。

Spring アニメーションは物理アニメーションのひとつで、「ばね」のような、より現実に近い動き方をするのが特徴です。また物理アニメーション全般の特徴として、アニメーション中に次のアニメーションが発生した際、その瞬間の勢いを保ったまま自然に次のアニメーションに移行できます

Spring アニメーションは SpringDescription というオブジェクトに設定値を持たせてさまざまな動きを実現できますが、AnimatedTo.spring はデフォルトで「iOS 標準」と同じアニメーションの設定値になるように作ってあります。ぜひ試してみてください。そして FloatingActionButton を連打して「自然な中断と次への移行」を体験してみてください。

先述の通り、AnimatedTo.springdescription 引数に Flutter 標準の SpringDescription オブジェクトを渡すことによってアニメーションの挙動を設定できますが、いきなりフリーハンドで設定しようとするとアニメーションの知識が必要になって大変ですので、animated_to が内部的に利用している springster パッケージにあらかじめ定義してあるものを使ってみると良さそうです。

/// springster は transitive な依存関係のため Quick Fix に出てこない
import 'package:springster/springster.dart';

...

AnimatedTo.spring(
  globalKey: GlobalObjectKey(1),
  description: Spring.bouncy,
  child: Container(
    width: 50,
    height: 50,

アニメーションがバウンスするような挙動に変わったのではないかなと思います。

まとめ

他にもいろいろ使い方はありますが、書き始めると終わらなくなってしまうのでいったんここまでとさせてください。もし興味があれば animated_to の Readme を読んでいただくか、X の @chooyan_i18n まで質問いただければと思います。GitHub の issue でもぜひ。

AnimatedTo のような「Widget をラップするだけで機能するアニメーション」のことを Flutter のドキュメント では「暗黙的なアニメーション(implicit animation)」と読んでいるようです。一方で AnimationController を使ってさらに自由に挙動を制御する方法を「明示的なアニメーション (explicit animation)」と呼んで区別しています。

animated_to はとにかく簡単に他の Widget とも自然に組み合わせて使えるよう、「アニメーションで移動する」ことのみを「暗黙的」に行うことに特化したパッケージです。似たような Widget として標準の AnimatedPositioned というものがありますが、こちらは Stackchildren でしか使えない縛りがあります。AnimatedTo は移動元と移動先を選びません。

もし興味がありましたら、ぜひこのパッケージを使っていろんなアニメーションを作ってみてください!そしてぜひコードを DartPad で共有してください!

脚注
  1. Gist に main.dart という名前をつけて DartPad で実行可能なコードを保存しておくと、その Gist の ID を使って https://dartpad.dev/?id=[gist_id] という URL でコードを DartPad 上で共有できます。これも覚えておくと便利です。 ↩︎

  2. VSCode など IDE での操作と同様、Command + . のショートカットで Wrap with widget などの Quick Fix が利用できます。 ↩︎

  3. GIF動画だとカクカクしてしまって感動が薄れてしまうので、ぜひお手元で実行してみてください。コーディングが面倒な場合は こちら から直接実行して確認できます。 ↩︎

GitHubで編集を提案

Discussion