使用 Flutter 为应用程序制作动画
已发表: 2022-03-10适用于任何平台的应用程序在直观、美观并为用户交互提供令人愉快的反馈时都会受到称赞。 动画是做到这一点的方法之一。
Flutter 是一个跨平台的框架,在过去的两年里已经成熟,包括 Web 和桌面支持。 它赢得了使用它开发的应用程序流畅且美观的声誉。 凭借其丰富的动画支持、声明式的 UI 编写方式、“Hot Reload”等特性,它现在是一个完整的跨平台框架。
如果您刚开始使用 Flutter 并想学习一种非常规的添加动画的方法,那么您来对地方了:我们将探索动画和运动小部件的领域,这是一种添加动画的隐式方法。
Flutter 基于小部件的概念。 应用程序的每个视觉组件都是一个小部件——将它们视为 Android 中的视图。 Flutter 使用 Animation 类、用于管理的“AnimationController”对象和用于插入数据范围的“Tween”来提供动画支持。 这三个组件协同工作以提供流畅的动画。 由于这需要手动创建和管理动画,因此它被称为一种显式的动画制作方式。
现在让我向您介绍动画和运动小部件。 Flutter 提供了许多固有地支持动画的小部件。 无需创建动画对象或任何控制器,因为所有动画都由此类小部件处理。 只需为所需的动画选择适当的小部件,然后将小部件的属性值传递给动画即可。 这种技术是一种隐含的动画方式。
上图大致列出了 Flutter 中的动画层次结构,以及如何支持显式和隐式动画。
本文介绍的一些动画小部件是:
-
AnimatedOpacity
-
AnimatedCrossFade
-
AnimatedAlign
-
AnimatedPadding
-
AnimatedSize
-
AnimatedPositioned
。
Flutter 不仅提供了预定义的动画小部件,还提供了一个名为AnimatedWidget
的通用小部件,可用于创建自定义的隐式动画小部件。 从名称可以看出,这些小部件属于动画和运动小部件类别,因此它们具有一些共同的属性,可以让我们使动画更流畅和更好看。
现在让我解释一下这些常见的属性,因为它们将在后面的所有示例中使用。
-
duration
动画参数的持续时间。 -
reverseDuration
反向动画的持续时间。 -
curve
为参数设置动画时应用的曲线。 插值可以取自线性分布,或者,如果指定的话,可以取自曲线。
让我们通过创建一个我们称之为“Quoted”的简单应用程序开始旅程。 每次应用启动时,它都会显示一个随机报价。 有两点需要注意:首先,所有这些引用都将在应用程序中进行硬编码; 其次,不会保存任何用户数据。
注意:这些示例的所有文件都可以在 GitHub 上找到。
入门
应该安装 Flutter,在继续之前您需要熟悉基本流程。 一个很好的起点是“使用 Google 的 Flutter 进行真正的跨平台移动开发”。
在 Android Studio 中创建一个新的 Flutter 项目。
这将打开一个新项目向导,您可以在其中配置项目基础知识。
在项目类型选择屏幕中,有各种类型的 Flutter 项目,每个项目都适合特定的场景。对于本教程,选择Flutter Application并按Next 。
您现在需要输入一些特定于项目的信息:项目名称和路径、公司域等。 看看下面的图片。
添加项目名称、Flutter SDK 路径、项目位置和可选的项目描述。 按下一步。
每个应用程序(无论是 Android 还是 iOS)都需要一个唯一的包名称。 通常,您使用网站域的反向; 例如,com.google 或 com.yahoo。 按Finish生成一个正常工作的 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
小部件,它提供了创建符合 Material Design 的应用程序的基本结构。 为了使代码更有条理,在lib文件夹中创建两个新的 dart 文件: FirstPage.dart和Quotes.dart 。
FirstPage.dart将包含负责我们引用的应用程序所需的所有视觉元素(小部件)的所有代码。 所有的动画都在这个文件中处理。
注意:在本文后面,每个动画小部件的所有代码片段都作为 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 类的新对象。 这是通过设计选择的,因为上面的列表只是作为一个实用程序。
将以下代码添加到该文件中:
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." ]; }
项目骨架现在已经准备好了,让我们再充实一下 Quoted。
AnimatedOpacity
为了给应用程序带来个人风格,知道用户的名字会很好,所以让我们询问它并显示一个下一步按钮。 在用户输入他们的名字之前,这个按钮是隐藏的,当给出名字时它会优雅地显示出来。 我们需要某种按钮的可见性动画,但是有没有一个小部件呢? 就在这里。
输入AnimatedOpacity
。 此小部件通过添加隐式动画支持构建在 Opacity 小部件之上。 我们如何使用它? 记住我们的场景:我们需要显示一个具有动画可见性的下一步按钮。 我们将按钮小部件包装在AnimatedOpacity
小部件中,输入一些适当的值并添加触发动画的条件——Flutter 可以处理其余部分。
_getAnimatedOpacityButton() { return AnimatedOpacity( duration: Duration(seconds: 1), reverseDuration: Duration(seconds: 1), curve: Curves.easeInOut, opacity: showNextButton ? 1 : 0, child: _getButton(), ); }
AnimatedOpacity
小部件有两个强制性属性:
-
opacity
值为 1 表示完全可见; 0(零)表示隐藏。 在制作动画时,Flutter 在这两个极端之间插入值。 您可以看到如何放置条件以更改可见性,从而触发动画。 -
child
将具有动画可见性的子小部件。
您现在应该了解使用隐式小部件添加可见性动画是多么简单。 所有这些小部件都遵循相同的准则并且易于使用。 让我们继续下一个。
AnimatedCrossFade
我们有用户名,但小部件仍在等待输入。 在上一步中,当用户输入他们的姓名时,我们会显示下一个按钮。 现在,当用户按下按钮时,我想停止接受输入并显示输入的名称。 当然,有很多方法可以做到这一点,但也许我们可以隐藏输入小部件并显示不可编辑的文本小部件。 让我们使用AnimatedCrossFade
小部件尝试一下。
这个小部件需要两个孩子,因为小部件根据某些条件在它们之间交叉淡入淡出。 使用此小部件时要记住的一件重要事情是两个孩子的宽度应该相同。 如果高度不同,则从底部剪掉较高的小部件。 在这种情况下,两个小部件将用作子部件:输入和标签。
_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
指定第二个孩子。
AnimatedAlign
此时,名称标签位于屏幕中央。 它在顶部看起来会更好,因为我们需要屏幕中心来显示引号。 简单地说,名称标签小部件的对齐方式应该从中心更改为顶部。 将这种对齐更改与之前的交叉淡入淡出动画一起制作动画不是很好吗? 我们开始做吧。
与往常一样,可以使用多种技术来实现这一点。 由于名称标签小部件已经居中对齐,因此对其对齐进行动画处理将比操作小部件的顶部和左侧值简单得多。 AnimatedAlign
小部件非常适合这项工作。
要启动此动画,需要触发器。 这个小部件的唯一目的是动画对齐变化,所以它只有几个属性:添加一个孩子,设置它的对齐,触发对齐变化,就是这样。
_getAnimatedAlignWidget() { return AnimatedAlign( duration: Duration(seconds: 1), curve: Curves.easeInOut, alignment: alignTop ? Alignment.topLeft : Alignment.center, child: _getAnimatedCrossfade(), ); }
它只有两个强制性属性:
- 孩子:
将修改其对齐方式的孩子。 - 结盟:
所需的对齐值。
这个小部件非常简单,但结果很优雅。 此外,我们看到了使用两个不同的动画小部件来创建更复杂的动画是多么容易。 这就是动画小部件的美妙之处。
AnimatedPadding
现在我们在顶部有用户名,使用不同类型的动画小部件,毫不费力地流畅地制作动画。 让我们在名字前添加一个问候语“Hi”。 在顶部添加一个值为“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 秒后执行一段代码:
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; }); }); }
这个小部件只有两个强制属性:
-
child
此小部件内的子级,将应用填充。 -
padding
要添加的空间量。
AnimatedSize
今天,任何具有某种动画的应用程序都将包括放大或缩小视觉组件以吸引用户注意力(通常称为缩放动画)。 为什么不在这里使用相同的技术? 我们可以向用户展示从屏幕中心放大的励志名言。 让我向您介绍AnimatedSize
小部件,它启用放大和缩小效果,通过更改其子项的大小来控制。
当涉及到所需的参数时,这个小部件与其他小部件有点不同。 我们需要 Flutter 所说的“Ticker”。 Flutter 有一种方法可以让对象知道何时触发了新的帧事件。 它可以被认为是发出一个信号,说:“现在就做! … 现在做! … 现在做! ……”
AnimatedSize
小部件需要一个属性——vsync——它接受一个ticker provider。 获取代码提供者的最简单方法是在类中添加一个Mixin 。 有两个基本的代码提供者实现: SingleTickerProviderStateMixin
,它提供单个代码; 和TickerProviderStateMixin
,它提供了几个。
Ticker 的默认实现用于标记动画的帧。 在这种情况下,采用后者。 更多关于混合的信息。
// 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
是完美的候选。
只要指定的位置发生变化,这个小部件就会在给定的持续时间内自动转换孩子的位置。 需要注意的一点:它仅在其父小部件是“堆栈”时才有效。 这个小部件非常简单易用。 让我们来看看。
// 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, ); }
这个小部件只有一个强制属性:
-
child
位置将被更改的小部件。
如果不希望孩子的大小随着它的位置而改变,则此小部件的更具性能的替代方案是SlideTransition
。
这是我们的完整动画:
结论
动画是用户体验不可或缺的一部分。 静态应用程序或带有 janky 动画的应用程序不仅会降低用户留存率,还会降低开发人员在交付成果方面的声誉。
今天,大多数流行的应用程序都有某种微妙的动画来取悦用户。 对用户请求的动画反馈也可以吸引他们进行更多探索。 Flutter 为跨平台开发提供了很多功能,包括对流畅和响应式动画的丰富支持。
Flutter 有很好的插件支持,允许我们使用来自其他开发者的动画。 现在已经成熟到 1.9 版本,在社区的厚爱下,Flutter 未来一定会变得更好。 我想说现在是学习 Flutter 的好时机!
更多资源
- “动画和运动小部件”,Flutter Docs
- “动画简介”,Flutter Docs
- Flutter 代码实验室
编者注:非常感谢 Ahmad Awais 在审阅本文时提供的帮助。