在Flutter中创建滚动动画
使用Flutter从头开始构建平滑的滚动动画

介绍
在本文中,我们将介绍如何使用Flutter SDK从头开始创建自定义滚动动画。Flutter是一个功能强大的工具,用于创建运行良好的本机移动应用程序,并且在创建丰富的用户体验(如动画)方面具有极大的灵活性。
如果您没有Flutter环境,请转到安装页面。此处提供了本文中示例项目的源代码。
入门
对于本演示,我创建了一个默认的Flutter项目,使用flutter create
并仅使用Flutter中直接可用的类,没有向项目添加依赖项。在许多情况下,可以直接完成任务(例如自定义动画),而无需额外的库。
这个示例应用程序的基本思想是创建一个包含几个项的简单列表视图,并且当视图向上或向下滚动时,在后台顺时针/逆时针旋转齿轮。我们将通过使用用于列表视图的滚动控制器并将其传递到动画背景来实现此目的,该动画背景构建与滚动位置同步旋转的自定义窗口小部件。
应用入口点
我们将从主应用程序文件lib/main.dart开始:
import 'package:flutter/material.dart';
import 'demo-card.dart';
import 'items.dart';
import 'animated-bg.dart';
void main() => runApp(AnimationDemo());
class AnimationDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(primarySwatch: Colors.deepPurple),
home: MyHomePage(title: 'Flutter Animation Demo'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
ScrollController _controller = new ScrollController();
List<DemoCard> get _cards =>
items.map((Item _item) => DemoCard(_item)).toList();
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(title: Text(widget.title)),
body: Stack(
alignment: AlignmentDirectional.topStart,
children: <Widget>[
AnimatedBackground(controller: _controller),
Center(
child: ListView(
controller: _controller,
children: _cards
)
)
]
)
);
}
}
在main.dart文件中,有以下组件的导入:
- demo-card.dart (从Item构建Card小部件)
- items.dart(为演示应用程序定义Item类和模拟内容)
- animated-bg.dart(构建动画背景小部件)
主app类(AnimationDemo)设置一个包装默认主页小部件(MyHomePage)的基本应用程序。在MyHomePage类中有一个_controller属性,它被初始化为ScrollController类的一个新实例,以便稍后传递给AnimatedBackground(它在后台驱动齿轮旋转动画)和ListView(它呈现滚动列表演示项目)。还有一个_cards属性,它以items.dart中导入的Item对象列表开头,并返回DemoCard列表 要在ListView中呈现的小部件。
物品类
我们检查的第一个导入文件是lib/items.dart:
import 'package:flutter/material.dart';
class Item {
String name;
String description;
MaterialColor color;
IconData icon;
Item(this.name, this.description, this.color, this.icon);
}
List<Item> items = [
Item('A', "Something cool", Colors.amber, Icons.ac_unit),
Item('B', "Hey, why not?", Colors.cyan, Icons.add_photo_alternate),
Item('C', "This might be OK", Colors.indigo, Icons.airplay),
Item('D', "Totally awesome", Colors.green, Icons.crop),
Item('E', "Rockin out", Colors.pink, Icons.album),
Item('F', "Take a look", Colors.blue, Icons.adb)
];
该Item类提供了将沿着要的实例被传递的数据结构DemoCard在要被渲染的ListView。除了名称,描述(在演示中当前未使用),颜色和为每个项目呈现的Icon之外,没有太多其他内容。Item对象列表将作为要在ListView中呈现的简单内容的列表。
DemoCard类
下一个要检查的文件是lib/demo-card.dart:
import 'package:flutter/material.dart';
import 'items.dart';
class DemoCard extends StatelessWidget {
DemoCard(this.item);
final Item item;
static final Shadow _shadow = Shadow(offset: Offset(2.0, 2.0), color: Colors.black26);
final TextStyle _style = TextStyle(color: Colors.white70, shadows: [_shadow]);
@override
Widget build(BuildContext context) {
return Card(
elevation: 3,
shape: RoundedRectangleBorder(
side: BorderSide(width: 1, color: Colors.black26),
borderRadius: BorderRadius.circular(32)
),
color: item.color.withOpacity(.7),
child: Container(
constraints: BoxConstraints.expand(height: 256),
child: RawMaterialButton(
onPressed: () { },
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Text(item.name, style: _style.copyWith(fontSize: 64)),
Icon(item.icon, color: Colors.white70, size: 72),
]
)
],
),
)
)
);
}
}
该DemoCard类接收一个Item在其构造并返回一个卡片式Widget呈现Text和Icon时显示的元素名称和图标对每个项目定义。使用Shadow,TextStyle和RoundedRectangleBorder以及卡片的高程(设置为3
)应用一些基本样式。Column 和Row小部件用于跨卡片中的子元素。
AnimatedBackground类
保存最后的最好,让我们看看lib/animated-bg.dart:
import 'package:flutter/material.dart';
import 'dart:math' as math;
class AnimatedBackground extends StatefulWidget {
AnimatedBackground({Key key, this.controller}) : super(key: key);
final ScrollController controller;
@override
_AnimatedBackgroundState createState() => _AnimatedBackgroundState();
}
class _AnimatedBackgroundState extends State<AnimatedBackground> {
get offset => widget.controller.hasClients ? widget.controller.offset : 0;
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: widget.controller,
builder: (BuildContext context, Widget child) {
return OverflowBox(
maxWidth: double.infinity,
alignment: Alignment(4, 3),
child: Transform.rotate(
angle: ((math.pi * offset) / -1024),
child: Icon(Icons.settings, size: 512, color: Colors.white)
)
);
}
);
}
}
在文件的顶部,dart:math
导入库以获得对π常数的访问,以进行旋转齿轮的旋转变换计算。该AnimatedBackground类构造函数将ScrollController将驱动旋转。如果控制器具有客户端(即控制器已正确初始化并连接到像ListView这样的实际滚动元素),_offset属性将返回控制器偏移量,否则它将返回零。该构建方法返回一个AnimatedBuilder,是以控制器(其被用作一个Listenable在滚动事件上重新绘制自己)并构建一个OverflowBox,它被对齐以将齿轮放在屏幕外。
数值4
并将3
齿轮放置在被测设备的左下角,即iPhone Xʀ模拟器。在实践中,应根据设备屏幕的高度和宽度计算Alignment值,以提供精确值,将齿轮放置在任何设备上的所需位置(左下角,中心偏离侧面等),但为此例如,我们会保持简单。
最后一个是动画本身发生的好部分,在Transform类的rotate静态方法中。此类按预期将角度旋转角度。对于这个演示,我希望齿轮缓慢移动并感觉更多与滚动物理网格相比,而不是它具有1:1值的疯狂飞轮效果,我希望齿轮逆时针滚动,就好像它是物理驾驶列表,所以我把偏移量乘起来。然后除以-1024。
结论
本文和示例项目仅演示了Flutter为创建自定义动画用户体验提供的一些功能。只使用几个基本类和简单的数学,我们有一个完整的动画,为无聊的东西添加一个更有趣的元素(如设置屏幕)。
这些概念可以扩展为创建启动画面,加载动画,页面转换,通知效果或任何其他可以想象的内容。几乎任何以double
参数为参数的东西都可以被动画化,从而可以直接实现效果,例如本例中使用的旋转,以及不透明度,位置和许多其他属性。
感谢阅读并祝你下一个Flutter项目好运!
源码:https://github.com/kenreilly/flutter-scroll-animation-demo