【Flutter】自作パッケージ animated_to が DartPad で使えるようになったので遊んでみる
Flutter / Dart には DartPad という Web ツールがあります。
これは 1 ファイルに収まるような小規模の Dart/Flutter コードを Web 上で簡単に試せるツールなのですが、あくまで 1 つの Dart ファイルにコードを書いて動作確認をするものであり、pubspec.yaml
を使ったパッケージ管理はできません。その代わり、Dart/Flutter チームがあらかじめ設定した 100 個ほどのパッケージ が使える作りになっています。
依存パッケージの定義はこちらのファイルに列挙されていて、よく見るところだと riverpod や provider、shared_preferences などが使えるようになっているようです。
そして今回、私が半年ほど前から公開していた animated_to というパッケージがめでたくこのリストに追加されたので、使い方の紹介がてら実際に DartPad で遊んでみたいと思います!
DartPad を開く
まずは DartPad を開いてみましょう。
普段から使っている方にとってはお馴染みの画面だと思いますが、画面左のエディタには以下のようなシンプルな Dart コードが書かれています。
void main() {
for (var i = 0; i < 10; i++) {
print('hello ${i + 1}');
}
}
この状態で "Run" ボタンをクリックすると、実際にコードが実行されて右側に実行結果が表示される仕組みです。
この DartPad は Flutter のコードも実行できます。画面上部の "Create" から "Flutter snippet" を選択してもう一度実行してみましょう。今度は Flutter の Hello World 画面が表示されたのではないかと思います。
ちょっとした UI であれば Flutter の環境構築やプロジェクト作成をしなくてもさくっと試せるため、DartPad で Flutter の動作確認(特に Widget の見た目や挙動のチェック)ができることは覚えておいて損はないと思います。
animated_to を使ってみる
では、animated_to で遊んでみましょう。AnimatedTo を使うと、たとえばこんな画面が簡単に作れてしまいます。以下の URL から DartPad を開いて実行してみてください。[1]
animated_to は 任意の Widget の移動をアニメーションできる パッケージです。
たとえばリビルドによってある Widget が画面上から画面下に場所を変えた場合、通常はリビルドと同時にパッと UI が変わるところを、AnimatedTo
という Widget でラップしてあげることでアニメーションしながら場所が更新される という挙動になります。それだけと言えばそれだけです。
試しに四角形 Widget を作って動かしてみましょう。
StatelessWidget
を StatefulWidget
に変更し、四角形な 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
の出番です。四角形を表す Container
を AnimatedTo.curve
で囲ってみてください。[2]
AnimatedTo.curve(
child: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(8),
),
),
),
すると「import 文が足りない」旨のエラーが出ますので、Command + .
ショートカットで Quick Fix を呼び出し、import 文を補完してください。
DartPad の補完メニューに自分のパッケージの名前が、、!すでに満足ですが続けます。
さて、import 文を補完すると今度は引数が足りない旨のエラーが表示されます。
AnimatedTo は Widget の位置が変わった際に RenderObject
が失われてしまわないよう、globalKey
引数に GlobalKey
を必要とします。
Flutter の Widget ツリーの仕組みとして、通常はある Widget の位置がリビルドによって変わったら一度 Element
や RenderObject
を破棄して「新しい 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.spring
は description
引数に 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
というものがありますが、こちらは Stack
の children
でしか使えない縛りがあります。AnimatedTo
は移動元と移動先を選びません。
もし興味がありましたら、ぜひこのパッケージを使っていろんなアニメーションを作ってみてください!そしてぜひコードを DartPad で共有してください!
-
Gist に
main.dart
という名前をつけて DartPad で実行可能なコードを保存しておくと、その Gist の ID を使ってhttps://dartpad.dev/?id=[gist_id]
という URL でコードを DartPad 上で共有できます。これも覚えておくと便利です。 ↩︎ -
VSCode など IDE での操作と同様、
Command + .
のショートカットでWrap with widget
などの Quick Fix が利用できます。 ↩︎ -
GIF動画だとカクカクしてしまって感動が薄れてしまうので、ぜひお手元で実行してみてください。コーディングが面倒な場合は こちら から直接実行して確認できます。 ↩︎
Discussion