Flutterを使用したアプリのアニメーション
公開: 2022-03-10あらゆるプラットフォーム向けのアプリは、直感的で見栄えがよく、ユーザーの操作に快適なフィードバックを提供することで賞賛されます。 アニメーションはまさにそれを行う方法の1つです。
クロスプラットフォームフレームワークであるFlutterは、過去2年間で成熟し、Webとデスクトップのサポートが含まれるようになりました。 それを使って開発されたアプリはスムーズで見栄えが良いという評判を集めています。 豊富なアニメーションサポート、UIの宣言型の記述方法、「ホットリロード」、およびその他の機能により、完全なクロスプラットフォームフレームワークになりました。
Flutterを使い始めて、アニメーションを追加するための型破りな方法を学びたい場合は、適切な場所にいます。アニメーションとモーションウィジェットの領域、つまりアニメーションを追加する暗黙の方法について説明します。
Flutterはウィジェットの概念に基づいています。 アプリの各ビジュアルコンポーネントはウィジェットです—Androidのビューと考えてください。 Flutterは、Animationクラス、管理用の「AnimationController」オブジェクト、およびデータ範囲を補間するための「Tween」を使用したアニメーションサポートを提供します。 これらの3つのコンポーネントが連携して、スムーズなアニメーションを提供します。 これにはアニメーションの手動作成と管理が必要なため、アニメーションの明示的な方法として知られています。
それでは、アニメーションウィジェットとモーションウィジェットを紹介します。 Flutterは、本質的にアニメーションをサポートする多数のウィジェットを提供します。 すべてのアニメーションはこのカテゴリのウィジェットによって処理されるため、アニメーションオブジェクトやコントローラーを作成する必要はありません。 必要なアニメーションに適切なウィジェットを選択し、ウィジェットのプロパティ値を渡してアニメーション化するだけです。 この手法は、アニメーション化の暗黙的な方法です。
上のグラフは、Flutterのアニメーション階層、明示的アニメーションと暗黙的アニメーションの両方がどのようにサポートされているかを大まかに示しています。
この記事で取り上げるアニメーションウィジェットのいくつかは次のとおりです。
-
AnimatedOpacity
-
AnimatedCrossFade
-
AnimatedAlign
-
AnimatedPadding
-
AnimatedSize
-
AnimatedPositioned
。
Flutterは、事前定義されたアニメーションウィジェットだけでなく、 AnimatedWidget
と呼ばれる汎用ウィジェットも提供します。これを使用して、カスタムの暗黙的にアニメーション化されたウィジェットを作成できます。 名前から明らかなように、これらのウィジェットはアニメーションウィジェットとモーションウィジェットのカテゴリに属しているため、アニメーションをよりスムーズで見栄えよくするためのいくつかの共通のプロパティがあります。
これらの一般的なプロパティについては、後ですべての例で使用されるため、ここで説明します。
-
duration
パラメータをアニメーション化する期間。 -
reverseDuration
逆アニメーションの長さ。 -
curve
パラメータをアニメートするときに適用する曲線。 補間された値は、線形分布から取得できます。または、指定されている場合は、曲線から取得できます。
「Quoted」と呼ばれる簡単なアプリを作成することから、旅を始めましょう。 アプリが起動するたびにランダムな引用が表示されます。 注意すべき2つのこと:最初に、これらの引用はすべてアプリケーションにハードコーディングされます。 次に、ユーザーデータは保存されません。
注:これらの例のすべてのファイルはGitHubにあります。
入門
Flutterをインストールする必要があります。次に進む前に、基本的なフローについてある程度理解しておく必要があります。 開始するのに適した場所は、「GoogleのFlutterを使用して真のクロスプラットフォームモバイル開発を行う」です。
AndroidStudioで新しいFlutterプロジェクトを作成します。
これにより、新しいプロジェクトウィザードが開き、プロジェクトの基本を構成できます。
プロジェクトタイプの選択画面には、さまざまなタイプのFlutterプロジェクトがあり、それぞれが特定のシナリオに対応しています。このチュートリアルでは、 Flutterアプリケーションを選択して[次へ]を押します。
次に、プロジェクト固有の情報(プロジェクト名とパス、会社のドメインなど)を入力する必要があります。 下の画像をご覧ください。
プロジェクト名、Flutter SDKパス、プロジェクトの場所、およびオプションのプロジェクトの説明を追加します。 [次へ]を押します。
各アプリケーション(AndroidまたはiOS)には、一意のパッケージ名が必要です。 通常、Webサイトドメインの逆を使用します。 たとえば、com.googleまたはcom.yahoo。 [完了]を押して、機能するFlutterアプリケーションを生成します。
プロジェクトが生成されると、上記の画面が表示されます。 main.dartファイルを開きます(スクリーンショットで強調表示されています)。 これがメインのアプリケーションファイルです。 サンプルプロジェクトはそれ自体で完成しており、エミュレーターまたは物理デバイス上で変更なしで直接実行できます。
main.dartファイルの内容を次のコードスニペットに置き換えます。
import 'package:animated_widgets/FirstPage.dart'; import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Animated Widgets', debugShowCheckedModeBanner: false, theme: ThemeData( primarySwatch: Colors.blue, accentColor: Colors.redAccent, ), home: FirstPage(), ); } }
このコードは、新しいアプリの作成に関連する簡単な情報を追加するだけで、 main.dartファイルをクリーンアップします。 MyAppクラスはオブジェクトを返します。MaterialAppウィジェットは、 MaterialApp
デザインに準拠したアプリを作成するための基本構造を提供します。 コードをより構造化するには、 libフォルダー内にFirstPage.dartとQuotes.dartの2つの新しいdartファイルを作成します。
FirstPage.dartには、Quotedアプリに必要なすべての視覚要素(ウィジェット)を担当するすべてのコードが含まれます。 すべてのアニメーションはこのファイルで処理されます。
注:この記事の後半では、アニメーション化された各ウィジェットのすべてのコードスニペットが、Scaffoldウィジェットの子としてこのファイルに追加されます。 詳細については、GitHubのこの例が役立つ可能性があります。
FirstPage.dartに次のコードを追加することから始めます。 これは、他のものが後で追加される部分的なコードです。
import 'dart:math'; import 'package:animated_widgets/Quotes.dart'; import 'package:flutter/material.dart'; class FirstPage extends StatefulWidget { @override State createState() { return FirstPageState(); } } class FirstPageState extends State with TickerProviderStateMixin { bool showNextButton = false; bool showNameLabel = false; bool alignTop = false; bool increaseLeftPadding = false; bool showGreetings = false; bool showQuoteCard = false; String name = ''; double screenWidth; double screenHeight; String quote; @override void initState() { super.initState(); Random random = new Random(); int quoteIndex = random.nextInt(Quotes.quotesArray.length); quote = Quotes.quotesArray[quoteIndex]; } @override Widget build(BuildContext context) { screenWidth = MediaQuery.of(context).size.width; screenHeight = MediaQuery.of(context).size.height; return Scaffold( appBar: _getAppBar(), body: Stack( children: [ // All other children will be added here. // In this article, all the children widgets are contained // in their own separate methods. // Just method calls should be added here for the respective child. ], ), ); } }
import 'dart:math'; import 'package:animated_widgets/Quotes.dart'; import 'package:flutter/material.dart'; class FirstPage extends StatefulWidget { @override State createState() { return FirstPageState(); } } class FirstPageState extends State with TickerProviderStateMixin { bool showNextButton = false; bool showNameLabel = false; bool alignTop = false; bool increaseLeftPadding = false; bool showGreetings = false; bool showQuoteCard = false; String name = ''; double screenWidth; double screenHeight; String quote; @override void initState() { super.initState(); Random random = new Random(); int quoteIndex = random.nextInt(Quotes.quotesArray.length); quote = Quotes.quotesArray[quoteIndex]; } @override Widget build(BuildContext context) { screenWidth = MediaQuery.of(context).size.width; screenHeight = MediaQuery.of(context).size.height; return Scaffold( appBar: _getAppBar(), body: Stack( children: [ // All other children will be added here. // In this article, all the children widgets are contained // in their own separate methods. // Just method calls should be added here for the respective child. ], ), ); } }
import 'dart:math'; import 'package:animated_widgets/Quotes.dart'; import 'package:flutter/material.dart'; class FirstPage extends StatefulWidget { @override State createState() { return FirstPageState(); } } class FirstPageState extends State with TickerProviderStateMixin { bool showNextButton = false; bool showNameLabel = false; bool alignTop = false; bool increaseLeftPadding = false; bool showGreetings = false; bool showQuoteCard = false; String name = ''; double screenWidth; double screenHeight; String quote; @override void initState() { super.initState(); Random random = new Random(); int quoteIndex = random.nextInt(Quotes.quotesArray.length); quote = Quotes.quotesArray[quoteIndex]; } @override Widget build(BuildContext context) { screenWidth = MediaQuery.of(context).size.width; screenHeight = MediaQuery.of(context).size.height; return Scaffold( appBar: _getAppBar(), body: Stack( children: [ // All other children will be added here. // In this article, all the children widgets are contained // in their own separate methods. // Just method calls should be added here for the respective child. ], ), ); } }
import 'dart:math'; import 'package:animated_widgets/Quotes.dart'; import 'package:flutter/material.dart'; class FirstPage extends StatefulWidget { @override State createState() { return FirstPageState(); } } class FirstPageState extends State with TickerProviderStateMixin { bool showNextButton = false; bool showNameLabel = false; bool alignTop = false; bool increaseLeftPadding = false; bool showGreetings = false; bool showQuoteCard = false; String name = ''; double screenWidth; double screenHeight; String quote; @override void initState() { super.initState(); Random random = new Random(); int quoteIndex = random.nextInt(Quotes.quotesArray.length); quote = Quotes.quotesArray[quoteIndex]; } @override Widget build(BuildContext context) { screenWidth = MediaQuery.of(context).size.width; screenHeight = MediaQuery.of(context).size.height; return Scaffold( appBar: _getAppBar(), body: Stack( children: [ // All other children will be added here. // In this article, all the children widgets are contained // in their own separate methods. // Just method calls should be added here for the respective child. ], ), ); } }
Quotes.dartファイルには、ハードコードされたすべての見積もりのリストが含まれています。 ここで注意すべき点の1つは、リストが静的オブジェクトであるということです。 これは、Quotesクラスの新しいオブジェクトを作成せずに他の場所で使用できることを意味します。 上記のリストは単にユーティリティとして機能するため、これは設計によって選択されます。
このファイルに次のコードを追加します。
class Quotes { static const quotesArray = [ "Good, better, best. Never let it rest. 'Til your good is better and your better is best", "It does not matter how slowly you go as long as you do not stop.", "Only I can change my life. No one can do it for me." ]; }
プロジェクトのスケルトンの準備ができたので、もう少し引用を具体化しましょう。
AnimatedOpacity
アプリに個人的なタッチを加えるには、ユーザーの名前を知っておくと便利です。それを聞いて、次のボタンを表示しましょう。 ユーザーが名前を入力するまで、このボタンは非表示になり、名前を付けると優雅に表示されます。 ボタンにはある種の可視性アニメーションが必要ですが、そのためのウィジェットはありますか? はいあります。
AnimatedOpacity
と入力します。 このウィジェットは、暗黙のアニメーションサポートを追加することにより、不透明度ウィジェットに基づいて構築されています。 どのように使用しますか? シナリオを思い出してください。アニメーションで表示される次のボタンを表示する必要があります。 ボタンウィジェットをAnimatedOpacity
ウィジェット内にラップし、いくつかの適切な値を入力し、アニメーションをトリガーする条件を追加します。残りはFlutterが処理できます。
_getAnimatedOpacityButton() { return AnimatedOpacity( duration: Duration(seconds: 1), reverseDuration: Duration(seconds: 1), curve: Curves.easeInOut, opacity: showNextButton ? 1 : 0, child: _getButton(), ); }
AnimatedOpacity
ウィジェットには、2つの必須プロパティがあります。
-
opacity
値1は、完全に表示されることを意味します。 0(ゼロ)は非表示を意味します。 アニメーション化中、Flutterはこれら2つの極値の間の値を補間します。 可視性を変更してアニメーションをトリガーするための条件がどのように配置されているかを確認できます。 -
child
可視性がアニメーション化される子ウィジェット。
これで、暗黙のウィジェットを使用して可視性アニメーションを追加することがいかに簡単であるかを理解する必要があります。 そして、そのようなウィジェットはすべて同じガイドラインに従い、使いやすいです。 次の話に移りましょう。
AnimatedCrossFade
ユーザーの名前はありますが、ウィジェットはまだ入力を待っています。 前のステップでは、ユーザーが名前を入力すると、次のボタンが表示されます。 ここで、ユーザーがボタンを押したときに、入力の受け入れを停止して、入力した名前を表示したいと思います。 もちろん、これを行う方法はたくさんありますが、入力ウィジェットを非表示にして、編集できないテキストウィジェットを表示することもできます。 AnimatedCrossFade
ウィジェットを使用して試してみましょう。
このウィジェットには2つの子が必要です。これは、ウィジェットが何らかの条件に基づいてそれらの間でクロスフェードするためです。 このウィジェットを使用する際に留意すべき重要なことの1つは、両方の子が同じ幅である必要があるということです。 高さが異なる場合、背の高いウィジェットは下からクリップされます。 このシナリオでは、入力とラベルの2つのウィジェットが子として使用されます。
_getAnimatedCrossfade() { return AnimatedCrossFade( duration: Duration(seconds: 1), alignment: Alignment.center, reverseDuration: Duration(seconds: 1), firstChild: _getNameInputWidget(), firstCurve: Curves.easeInOut, secondChild: _getNameLabelWidget(), secondCurve: Curves.easeInOut, crossFadeState: showNameLabel ? CrossFadeState.showSecond : CrossFadeState.showFirst, ); }
このウィジェットには、異なる必須パラメーターのセットが必要です。
-
crossFadeState
この状態は、どの子を表示するかを決定します。 -
firstChild
このウィジェットの最初の子を指定します。 -
secondChild
2番目の子を指定します。
AnimatedAlign
この時点で、名前ラベルは画面の中央に配置されます。 引用符を表示するには画面の中央が必要なので、上部の方がはるかに見栄えが良くなります。 簡単に言うと、名前ラベルウィジェットの配置を中央から上に変更する必要があります。 そして、以前のクロスフェードアニメーションと一緒にこの配置の変更をアニメーション化するのは素晴らしいことではないでしょうか? やってみましょう。
いつものように、これを達成するためにいくつかの技術を使用することができます。 名前ラベルウィジェットはすでに中央揃えになっているため、ウィジェットの上下の値を操作するよりも、その配置をアニメーション化する方がはるかに簡単です。 AnimatedAlign
ウィジェットはこの仕事に最適です。
このアニメーションを開始するには、トリガーが必要です。 このウィジェットの唯一の目的は、配置の変更をアニメーション化することです。そのため、子の追加、配置の設定、配置の変更のトリガーなど、いくつかのプロパティしかありません。それだけです。
_getAnimatedAlignWidget() { return AnimatedAlign( duration: Duration(seconds: 1), curve: Curves.easeInOut, alignment: alignTop ? Alignment.topLeft : Alignment.center, child: _getAnimatedCrossfade(), ); }
必須のプロパティは2つだけです。
- 子:
配置が変更される子。 - アラインメント:
必要なアライメント値。
このウィジェットは本当にシンプルですが、結果はエレガントです。 さらに、2つの異なるアニメーションウィジェットを使用して、より複雑なアニメーションを簡単に作成できることもわかりました。 これがアニメーションウィジェットの美しさです。
AnimatedPadding
これで、ユーザーの名前が上部に表示され、さまざまな種類のアニメーションウィジェットを使用して、手間をかけずにスムーズにアニメーション化できます。 名前の前に「こんにちは」という挨拶を付けましょう。 上部に値「Hi」のテキストウィジェットを追加すると、下の画像のように、あいさつ文のテキストウィジェットと重なるようになります。
名前のテキストウィジェットの左側にパディングがある場合はどうなりますか? 左のパディングを増やすことは間違いなく機能しますが、待ってください。アニメーションでパディングを増やすことはできますか? はい、それがAnimatedPadding
が行うことです。 これらすべてをより見栄えよくするために、挨拶テキストウィジェットをフェードインさせ、同時に名前テキストウィジェットのパディングを増やしましょう。
_getAnimatedPaddingWidget() { return AnimatedPadding( duration: Duration(seconds: 1), curve: Curves.fastOutSlowIn, padding: increaseLeftPadding ? EdgeInsets.only(left: 28.0) : EdgeInsets.only(left: 0), child: _getAnimatedCrossfade(), ); }
上記のアニメーションは、前のアニメーション化された配置が完了した後にのみ発生するはずなので、このアニメーションのトリガーを遅らせる必要があります。 トピックから簡単に離れて、これは遅延を追加するための一般的なメカニズムについて話す良い機会です。 Flutterはそのような手法をいくつか提供しますが、Future.delayedコンストラクターは、より単純で、よりクリーンで、より読みやすいアプローチの1つです。 たとえば、1秒後にコードを実行するには:
Future.delayed(Duration(seconds: 1), (){ sum = a + b; // This sum will be calculated after 1 second. print(sum); });
遅延時間はすでにわかっているため(以前のアニメーション時間から計算)、この間隔の後にアニメーションをトリガーできます。
// Showing “Hi” after 1 second - greetings visibility trigger. _showGreetings() { Future.delayed(Duration(seconds: 1), () { setState(() { showGreetings = true; }); }); } // Increasing the padding for name label widget after 1 second - increase padding trigger. _increaseLeftPadding() { Future.delayed(Duration(seconds: 1), () { setState(() { increaseLeftPadding = true; }); }); }
このウィジェットには、2つの必須プロパティのみがあります。
-
child
このウィジェット内の子。パディングが適用されます。 -
padding
追加するスペースの量。
AnimatedSize
今日、ある種のアニメーションを備えたアプリには、ユーザーの注意を引くためにビジュアルコンポーネントにズームインまたはズームアウトすることが含まれます(一般にスケーリングアニメーションと呼ばれます)。 ここで同じテクニックを使ってみませんか? 画面の中央からズームインする動機付けの引用をユーザーに表示できます。 子のサイズを変更することで制御されるズームインおよびズームアウト効果を有効にするAnimatedSize
ウィジェットを紹介します。
このウィジェットは、必要なパラメーターに関しては他のウィジェットとは少し異なります。 Flutterが「ティッカー」と呼ぶものが必要です。 Flutterには、新しいフレームイベントがトリガーされるたびにオブジェクトに通知するメソッドがあります。 それは、「今すぐやれ!」という合図を送るものと考えることができます。 … 今やれ! … 今やれ! …」
AnimatedSize
ウィジェットには、ティッカープロバイダーを受け入れるプロパティ( vsync )が必要です。 ティッカープロバイダーを取得する最も簡単な方法は、クラスにミックスインを追加することです。 2つの基本的なティッカープロバイダーの実装があります。単一のティッカーを提供するSingleTickerProviderStateMixin
。 およびTickerProviderStateMixin
は、いくつかを提供します。
ティッカーのデフォルトの実装は、アニメーションのフレームをマークするために使用されます。 この場合、後者が使用されます。 ミックスインの詳細。
// Helper method to create quotes card widget. _getQuoteCardWidget() { return Card( color: Colors.green, elevation: 8.0, child: _getAnimatedSizeWidget(), ); } // Helper method to create animated size widget and set its properties. _getAnimatedSizeWidget() { return AnimatedSize( duration: Duration(seconds: 1), curve: Curves.easeInOut, vsync: this, child: _getQuoteContainer(), ); } // Helper method to create the quotes container widget with different sizes. _getQuoteContainer() { return Container( height: showQuoteCard ? 100 : 0, width: showQuoteCard ? screenWidth - 32 : 0, child: Center( child: Padding( padding: EdgeInsets.symmetric(horizontal: 16), child: Text(quote, style: TextStyle(color: Colors.white, fontWeight: FontWeight.w400, fontSize: 14),), ), ), ); } // Trigger used to show the quote card widget. _showQuote() { Future.delayed(Duration(seconds: 2), () { setState(() { showQuoteCard = true; }); }); }
このウィジェットの必須プロパティ:
-
vsync
アニメーションとフレームの変更を調整するために必要なティッカープロバイダー。< -
child
サイズが変わる子がアニメ化されます。
ズームインとズームアウトのアニメーションを簡単に調整できるようになりました。
AnimatedPositioned
すごい! 引用符は中央からズームインして、ユーザーの注意を引き付けます。 ズームイン中に下から上にスライドした場合はどうなりますか? 試してみよう。 このモーションには、見積もりウィジェットの位置を操作し、位置プロパティの変更をアニメーション化することが含まれます。 AnimatedPositioned
は完璧な候補です。
このウィジェットは、指定された位置が変更されるたびに、指定された期間にわたって子の位置を自動的に移行します。 注意すべき1つのポイント:親ウィジェットが「スタック」である場合にのみ機能します。 このウィジェットは非常にシンプルで簡単に使用できます。 どれどれ。
// Helper method to create the animated positioned widget. // With position changes based on “showQuoteCard” flag. _getAnimatedPositionWidget() { return AnimatedPositioned( duration: Duration(seconds: 1), curve: Curves.easeInOut, child: _getQuoteCardWidget(), top: showQuoteCard ? screenHeight/2 - 100 : screenHeight, left: !showQuoteCard ? screenWidth/2 : 12, ); }
このウィジェットには、必須のプロパティが1つだけあります。
-
child
位置が変更されるウィジェット。
子のサイズがその位置とともに変化すると予想されない場合、このウィジェットのよりパフォーマンスの高い代替手段はSlideTransition
です。
これが私たちの完全なアニメーションです:
結論
アニメーションは、ユーザーエクスペリエンスの不可欠な部分です。 静的なアプリやアニメーションがぎこちないアプリは、ユーザーの定着率を低下させるだけでなく、結果を出すための開発者の評判も低下させます。
今日、最も人気のあるアプリには、ユーザーを喜ばせるためのある種の微妙なアニメーションがあります。 ユーザーのリクエストに対するアニメーションのフィードバックは、ユーザーをさらに探索するように働きかけることもできます。 Flutterは、スムーズでレスポンシブなアニメーションの豊富なサポートなど、クロスプラットフォーム開発のための多くの機能を提供します。
Flutterは、他の開発者のアニメーションを使用できる優れたプラグインサポートを備えています。 コミュニティからの愛情を込めてバージョン1.9に成熟した今、Flutterは将来さらに良くなるはずです。 今がFlutterを学ぶ絶好の機会だと思います!
その他のリソース
- 「アニメーションとモーションウィジェット」、Flutter Docs
- 「アニメーション入門」、Flutter Docs
- FlutterCodelabs
編集者注:この記事のレビューに協力してくれたAhmadAwaisに心から感謝します。