Flutterを使用したアプリのアニメーション

公開: 2022-03-10
クイックサマリー↬Flutterは、クロスプラットフォームアプリに優れたアニメーションサポートを提供します。 この記事では、アプリにアニメーションを追加するための新しい型破りで簡単な方法について説明します。 これらの新しい「アニメーションとモーション」ウィジェットとは正確には何であり、それらを使用して単純なアニメーションと複雑なアニメーションを追加するにはどうすればよいでしょうか。

あらゆるプラットフォーム向けのアプリは、直感的で見栄えがよく、ユーザーの操作に快適なフィードバックを提供することで賞賛されます。 アニメーションはまさにそれを行う方法の1つです。

クロスプラットフォームフレームワークであるFlutterは、過去2年間で成熟し、Webとデスクトップのサポートが含まれるようになりました。 それを使って開発されたアプリはスムーズで見栄えが良いという評判を集めています。 豊富なアニメーションサポート、UIの宣言型の記述方法、「ホットリロード」、およびその他の機能により、完全なクロスプラットフォームフレームワークになりました。

Flutterを使い始めて、アニメーションを追加するための型破りな方法を学びたい場合は、適切な場所にいます。アニメーションとモーションウィジェットの領域、つまりアニメーションを追加する暗黙の方法について説明します。

Flutterはウィジェットの概念に基づいています。 アプリの各ビジュアルコンポーネントはウィジェットです—Androidのビューと考えてください。 Flutterは、Animationクラス、管理用の「AnimationController」オブジェクト、およびデータ範囲を補間するための「Tween」を使用したアニメーションサポートを提供します。 これらの3つのコンポーネントが連携して、スムーズなアニメーションを提供します。 これにはアニメーションの手動作成と管理が必要なため、アニメーションの明示的な方法として知られています。

それでは、アニメーションウィジェットとモーションウィジェットを紹介します。 Flutterは、本質的にアニメーションをサポートする多数のウィジェットを提供します。 すべてのアニメーションはこのカテゴリのウィジェットによって処理されるため、アニメーションオブジェクトやコントローラーを作成する必要はありません。 必要なアニメーションに適切なウィジェットを選択し、ウィジェットのプロパティ値を渡してアニメーション化するだけです。 この手法は、アニメーション化の暗黙的な方法です。

Flutterのアニメーション階層。 (大プレビュー)

上のグラフは、Flutterのアニメーション階層、明示的アニメーションと暗黙的アニメーションの両方がどのようにサポートされているかを大まかに示しています。

この記事で取り上げるアニメーションウィジェットのいくつかは次のとおりです。

  • AnimatedOpacity
  • AnimatedCrossFade
  • AnimatedAlign
  • AnimatedPadding
  • AnimatedSize
  • AnimatedPositioned

Flutterは、事前定義されたアニメーションウィジェットだけでなく、 AnimatedWidgetと呼ばれる汎用ウィジェットも提供します。これを使用して、カスタムの暗黙的にアニメーション化されたウィジェットを作成できます。 名前から明らかなように、これらのウィジェットはアニメーションウィジェットとモーションウィジェットのカテゴリに属しているため、アニメーションをよりスムーズで見栄えよくするためのいくつかの共通のプロパティがあります。

これらの一般的なプロパティについては、後ですべての例で使用されるため、ここで説明します。

  • duration
    パラメータをアニメーション化する期間。
  • reverseDuration
    逆アニメーションの長さ。
  • curve
    パラメータをアニメートするときに適用する曲線。 補間された値は、線形分布から取得できます。または、指定されている場合は、曲線から取得できます。

「Quoted」と呼ばれる簡単なアプリを作成することから、旅を始めましょう。 アプリが起動するたびにランダムな引用が表示されます。 注意すべき2つのこと:最初に、これらの引用はすべてアプリケーションにハードコーディングされます。 次に、ユーザーデータは保存されません。

これらの例のすべてのファイルはGitHubにあります。

ジャンプした後もっと! 以下を読み続けてください↓

入門

Flutterをインストールする必要があります。次に進む前に、基本的なフローについてある程度理解しておく必要があります。 開始するのに適した場所は、「GoogleのFlutterを使用して真のクロスプラットフォームモバイル開発を行う」です。

AndroidStudioで新しい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.dartQuotes.dartの2つの新しいdartファイルを作成します。

FirstPage.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ファイル。 (大プレビュー)

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に心から感謝します。