Flutter页面-基础Widget
+ 在Flutter中,几乎所有的对象都是一个`Widget`,与原生开发中的**控件**不同的是,Flutter中的`widget`的概念更广泛,它不仅可以表示UI元素,也可以表示一些功能性的组件如:用于手势检测的 `GestureDetector` widget、用于应用主题数据传递的`Theme`等等。由于Flutter主要就是用于构建用户界面的,所以,在大多数时候,可以认为widget就是一个控件,不必纠结于概念。
Widget的功能是“描述一个UI元素的配置数据”,Widget其实并不是表示最终绘制在设备屏幕上的显示元素,而只是显示元素的一个配置数据。实际上,Flutter中真正代表屏幕上显示元素的类是`Element`,也就是说Widget只是描述`Element`的一个配置。一个Widget可以对应多个`Element`,这是因为同一个Widget对象可以被添加到UI树的不同部分,而真正渲染时,UI树的每一个节点都会对应一个`Element`对象。
Widget
`StatelessWidget`和`StatefulWidget`是`flutter`的基础组件,日常开发中自定义`Widget`都是选择继承这两者之一。也是在往后的开放中,我们最多接触的Widget:
- `StatelessWidget`:无状态的,展示信息,面向那些始终不变的UI控件;
- `StatefulWidget`:有状态的,可以通过改变状态使得 UI 发生变化,可以包含用户交互(比如弹出一个 dialog)。
在实际使用中,Stateless与Stateful的选择需要取决于这个 Widget 是有状态还是无状态,简单来说看界面是否需要更新。
Stateless Widget
StatelessWidget用于不需要维护状态的场景,它通常在`build`方法中通过嵌套其它Widget来构建UI,在构建过程中会递归的构建其嵌套的Widget。
> `BuildContext`表示构建widget的上下文,它是操作widget在树中位置的一个句柄,它包含了一些查找、遍历当前Widget树的一些方法。每一个widget都有一个自己的context对象。
import 'package:flutter/material.dart';
void main() => runApp(StatelessApp());
class StatelessApp extends StatelessWidget {
///在build方法中通过嵌套其它Widget来构建UI,在构建过程中会递归的构建其嵌套的Widget
@override
Widget build(BuildContext context) {
//嵌套 MaterialApp:封装了应用程序实现Material Design所需要的一些widget
return MaterialApp(
title: "Widget演示", //标题,显示在recent时候的标题
//主页面
//Scaffold : Material Design布局结构的基本实现。
home: Scaffold(
//ToolBar/ActionBar
appBar: AppBar(title: Text("Widget")),
body: Text("Hello,Flutter!"),
)
);
}
}Material Design:
> 一种设计语言,Material Design 于2014年的 Google I/O 首次亮相,是谷歌推出的全新的设计语言。说白了,就是一种设计风格。
Stateful Widget
StatefulWidget是动态的,添加了一个新的接口`createState()`用于创建和Stateful widget相关的状态`State`,它在Stateful widget的生命周期中可能会被多次调用。
当State被改变时,可以手动调用其`setState()`方法通知Flutter framework状态发生改变,Flutter framework在收到消息后,会重新调用其`build`方法重新构建widget树,从而达到更新UI的目的。
class StatefulState extends State<StatefulApp> {
int _i;
///当Widget第一次插入到Widget树时会被调用,对于每一个State对象,Flutter framework只会调用一次该回调
@override
void initState() {
super.initState();
_i = 1;
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "Widget演示",
theme: ThemeData(),
home: Scaffold(
appBar: AppBar(title: Text("Widget")),
body: RaisedButton(
onPressed: () {
//修改状态,setState会重新调用build更新ui
setState(() {
_i++;
});
},
child: Text("Hello,Flutter! $_i"),
),
));
}
}
State生命周期
State类除了`build`之外还提供了很多方法能够让我们重写,这些方法会在不同的状态下由Flutter调起执行,所以这些方法我们就称之为**生命周期**方法。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool isShowChild;
///当Widget第一次插入到Widget树时会被调用,对于每一个State对象,Flutter framework只会调用一次该回调
@override
void initState() {
super.initState();
isShowChild = true;
debugPrint("parent initState......");
}
///初始化时,在initState()之后立刻调用
///当依赖的InheritedWidget rebuild,会触发此接口被调用
@override
void didChangeDependencies() {
super.didChangeDependencies();
debugPrint("parent didChangeDependencies......");
}
///绘制界面,当setState触发的时候会再次被调用
@override
Widget build(BuildContext context) {
debugPrint("parent build......");
return MaterialApp(
home: Scaffold(
body: Center(
child: RaisedButton(
onPressed: () {
setState(() {
isShowChild = !isShowChild;
});
},
child: isShowChild ? Child() : Text("演示移除Child"),
)),
),
);
}
///状态改变的时候会调用该方法,比如调用了setState
@override
void didUpdateWidget(MyApp oldWidget) {
super.didUpdateWidget(oldWidget);
debugPrint("parent didUpdateWidget......");
}
///当State对象从树中被移除时,会调用此回调
@override
void deactivate() {
super.deactivate();
debugPrint('parent deactivate......');
}
///当State对象从树中被永久移除时调用;通常在此回调中释放资源
@override
void dispose() {
super.dispose();
debugPrint('parent dispose......');
}
}
class Child extends StatefulWidget {
@override
_ChildState createState() => _ChildState();
}
class _ChildState extends State<Child> {
@override
Widget build(BuildContext context) {
debugPrint("child build......");
return Text('lifeCycle');
}
@override
void initState() {
super.initState();
debugPrint("child initState......");
}
///初始化时,在initState()之后立刻调用
///当依赖的InheritedWidget rebuild,会触发此接口被调用
@override
void didChangeDependencies() {
super.didChangeDependencies();
debugPrint("child didChangeDependencies......");
}
///父widget状态改变的时候会调用该方法,比如父节点调用了setState
@override
void didUpdateWidget(Child oldWidget) {
super.didUpdateWidget(oldWidget);
debugPrint("child didUpdateWidget......");
}
///当State对象从树中被移除时,会调用此回调
@override
void deactivate() {
super.deactivate();
debugPrint('child deactivate......');
}
///当State对象从树中被永久移除时调用;通常在此回调中释放资源
@override
void dispose() {
super.dispose();
debugPrint('child dispose......');
}
}
执行的输出结果显示为:
- 运行到显示
```dart
I/flutter (22218): parent initState......
I/flutter (22218): parent didChangeDependencies......
I/flutter (22218): parent build......
I/flutter (22218): child initState......
I/flutter (22218): child didChangeDependencies......
I/flutter (22218): child build......
```
- 点击按钮会移除Child
```dart
I/flutter (22218): parent build......
I/flutter (22218): child deactivate......
I/flutter (22218): child dispose......
```
- 将MyApp的代码由` child: isShowChild ? Child() : Text("演示移除Child")`,改为` child: Child()`,点击按钮时
```dart
I/flutter (22765): parent build......
I/flutter (22765): child didUpdateWidget......
I/flutter (22765): child build......
```
从这些实验中能够得出State的生命周期
基础widget文本显示
Text
`Text`是展示单一格式的文本Widget(Android `TextView`)。
import 'package:flutter/material.dart';
///
/// main方法 调用runApp传递Widget,这个Widget成为widget树的根
void main() => runApp(TextApp());
///
/// 1、单一文本Text
///
//创建一个无状态的Widget
class TextApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
//封装了应用程序实现Material Design所需要的一些widget
return MaterialApp(
title: "Text演示", //标题,显示在recent时候的标题
//主页面
//Scaffold : Material Design布局结构的基本实现。
home: Scaffold(
//ToolBar/ActionBar
appBar: AppBar(title: Text("Text")),
body: Text("Hello,Flutter"),
),
);
}
}在使用`Text`显示文字时候,可能需要对文字设置各种不同的样式,类似Android的 `android:textColor/Size`等
在Flutter中也拥有类似的属性
Widget _TextBody() {
return Text(
"Hello,Flutter",
style: TextStyle(
//颜色
color: Colors.red,
//字号 默认14
fontSize: 18,
//粗细
fontWeight: FontWeight.w800,
//斜体
fontStyle: FontStyle.italic,
//underline:下划线,overline:上划线,lineThrough:删除线
decoration: TextDecoration.lineThrough,
decorationColor: Colors.black,
//solid:实线,double:双线,dotted:点虚线,dashed:横虚线,wavy:波浪线
decorationStyle: TextDecorationStyle.wavy),
);
}
class TextApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "Text演示",
home: Scaffold(
appBar: AppBar(title: Text("Text")),
body: _TextBody(),
),
);
}
}RichText
如果需要显示更为丰富样式的文本(比如一段文本中文字不同颜色),可以使用`RichText`或者`Text.rich`
Widget _RichTextBody() {
var textSpan = TextSpan(
text: "Hello",
style: TextStyle(color: Colors.red),
children: [
TextSpan(text: "Flu", style: TextStyle(color: Colors.blue)),
TextSpan(text: "uter", style: TextStyle(color: Colors.yellow)),
],
);
//Text.rich(textSpan);
return RichText(text: textSpan);
}DefaultTextStyle
在widget树中,文本的样式默认是可以被继承的,因此,如果在widget树的某一个节点处设置一个默认的文本样式,那么该节点的子树中所有文本都会默认使用这个样式。相当于在Android中定义 Theme
Widget _DefaultStyle(){
DefaultTextStyle(
//设置文本默认样式
style: TextStyle(
color:Colors.red,
fontSize: 20.0,
),
textAlign: TextAlign.start,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text("Hello Flutter!"),
Text("Hello Flutter!"),
Text("Hello Flutter!",
style: TextStyle(
inherit: false, //不继承默认样式
color: Colors.grey
),
),
],
),
);
}图片显示
"图文":有文字显示Widget,又怎么少的了图片呢。
FlutterLogo
这个Widget用于显示Flutter的logo......
Widget flutterLogo() {
return FlutterLogo(
//大小
size: 100,
//logo颜色 默认为 Colors.blue
colors: Colors.red,
//markOnly:只显示logo,horizontal:logo右边显示flutter文字,stacked:logo下面显示文字
style: FlutterLogoStyle.stacked,
//logo上文字颜色
textColor: Colors.blue,
);
}Icon
主要用于显示内置图标的`Widget`
Widget icon() {
return Icon(
//使用预定义Material icons
// https://docs.flutter.io/flutter/material/Icons-class.html
Icons.add,
size: 100,
color: Colors.red);
}Image
显示图片的`Widget`。图片常用的格式主要有bmp,jpg,png,gif,webp等,Android中并不是天生支持gif和webp动图,但是这一特性在flutter中被很好的支持了。
| 方式 | 解释 |
| ------------- | ----------------------------------------------------------- |
| Image() | 使用ImageProvider提供图片,如下方法本质上也是使用的这个方法 |
| Image.asset | 加载资源图片 |
| Image.file | 加载本地图片文件 |
| Image.network | 加载网络图片 |
| Image.memory | 加载内存图片 |
Iamge.asset
在工程目录下创建目录,如:assets,将图片放入此目录。打开项目根目录:pubspec.yaml
return MaterialApp(
title: "Image演示",
home: Scaffold(
appBar: AppBar(title: Text("Image")),
body: Image.asset("assets/banner.jpeg"),
),
);Image.file
在sd卡中放入一张图片。然后利用**path_provider**库获取sd卡根目录(Dart库版本可以在:https://pub.dartlang.org/packages查询)。
**注意权限**
class ImageState extends State<ImageApp> {
Image image;
@override
void initState() {
super.initState();
getExternalStorageDirectory().then((path) {
setState(() {
image = Image.file(File("${path.path}${Platform.pathSeparator}banner.jpeg"));
});
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "Image演示",
home: Scaffold(
appBar: AppBar(title: Text("Image")),
body: image,
),
);
}
}Image.network
直接给网络地址即可。
> Flutter 1.0,加载https时候经常出现证书错误。必须断开AS打开app
Image.memory
Future<List<int>> _imageByte() async {
String path = (await getExternalStorageDirectory()).path;
return await File("$path${Platform.pathSeparator}banner.jpeg").readAsBytes();
}
class ImageState extends State<ImageApp> {
Image image;
@override
void initState() {
super.initState();
_imageByte().then((bytes) {
setState(() {
image = Image.memory(bytes);
});
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "Image演示",
home: Scaffold(
appBar: AppBar(title: Text("Image")),
body: image,
),
);
}
}fit属性相当于android中的scaletype,定义如下:
| fit | 说明 | 效果 |
| ---------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| BoxFit.fill | 填充,忽略原有的宽高比,填满为止 |  |
| BoxFit.contain | 包含,不改变原有比例让容器包含整个图片,容器多余部分填充背景 |  |
| BoxFit.cover | 覆盖,不改变原有比例,让图片充满整个容器,图片多余部分裁剪 |  |
| BoxFit.fitWidth | 横向图片填充 |  |
| BoxFit.fitHeight | 纵向图片填充 |  |
| BoxFit.none | 原始大小居中 |  |
| BoxFit.scaleDown | 图片大小小于容器事相当于none,图片大小大于容器时缩小图片大小实现contain |  |
CircleAvatar
主要用来显示用户的头像,任何图片都会被剪切为圆形。
CircleAvatar(
//图片提供者 ImageProvider
backgroundImage: AssetImage("assets/banner.jpeg"),
//半径,控制大小
radius: 50.0,
);FadeInImage
当使用默认`Image` widget显示图片时,您可能会注意到它们在加载完成后会直接显示到屏幕上。这可能会让用户产生视觉突兀。如果最初显示一个占位符,然后在图像加载完显示时淡入,我们可以使用`FadeInImage`来达到这个目的!
image = FadeInImage.memoryNetwork(
placeholder: kTransparentImage,
image: 'https://flutter.io/images/homepage/header-illustration.png',
);按钮
Material widget库中提供了多种按钮Widget如RaisedButton、FlatButton、OutlineButton等,它们都是直接或间接对RawMaterialButton的包装定制,所以他们大多数属性都和`RawMaterialButton`一样。所有Material 库中的按钮都有如下相同点:
1. 按下时都会有“水波动画”。
2. 有一个`onPressed`属性来设置点击回调,当按钮按下时会执行该回调,如果不提供该回调则按钮会处于禁用状态,禁用状态不响应用户点击。
RaisedButton
"漂浮"按钮,它默认带有阴影和灰色背景
RaisedButton(
child: Text("normal"),
onPressed: () => {},
)FlatButton
扁平按钮,默认背景透明并不带阴影
FlatButton(
child: Text("normal"),
onPressed: () => {},
)OutlineButton
默认有一个边框,不带阴影且背景透明。
OutlineButton(
child: Text("normal"),
onPressed: () => {},
)IconButton
可点击的Icon
IconButton(
icon: Icon(Icons.thumb_up),
onPressed: () => {},
)按钮外观可以通过其属性来定义,不同按钮属性大同小异
const FlatButton({
...
@required this.onPressed, //按钮点击回调
this.textColor, //按钮文字颜色
this.disabledTextColor, //按钮禁用时的文字颜色
this.color, //按钮背景颜色
this.disabledColor,//按钮禁用时的背景颜色
this.highlightColor, //按钮按下时的背景颜色
this.splashColor, //点击时,水波动画中水波的颜色
this.colorBrightness,//按钮主题,默认是浅色主题
this.padding, //按钮的填充
this.shape, //外形
@required this.child, //按钮的内容
})
FlatButton(
onPressed: () => {},
child: Text("Raised"),
//蓝色
color: Colors.blue,
//水波
splashColor: Colors.yellow,
//深色主题,这样文字颜色会变成白色
colorBrightness: Brightness.dark,
//圆角按钮
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50)
),
)而`RaisedButton`,默认配置有阴影,因此在配置`RaisedButton` 时,拥有一系列 elevation 属性的配置
const RaisedButton({
...
this.elevation = 2.0, //正常状态下的阴影
this.highlightElevation = 8.0,//按下时的阴影
this.disabledElevation = 0.0,// 禁用时的阴影
...
}输入框
import 'package:flutter/material.dart';
void main() => runApp(Demo1());
class Demo1 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "Demo1",
home: Scaffold(
appBar: AppBar(
title: Text("登录"),
),
//线性布局,垂直方向
body: Column(
children: <Widget>[
TextField(
//自动获得焦点
autofocus: true,
decoration: InputDecoration(
labelText: "用户名",
hintText: "用户名或邮箱",
prefixIcon: Icon(Icons.person)),
),
TextField(
//隐藏正在编辑的文本
obscureText: true,
decoration: InputDecoration(
labelText: "密码",
hintText: "您的登录密码",
prefixIcon: Icon(Icons.lock)),
),
],
),
),
);
}
}这个效果非常的“系统”,我们可能大多数情况下需要将下划线更换为矩形边框,这时候可能就需要组合widget来完成:
//容器 设置一个控件的尺寸、背景、margin
Container(
margin: EdgeInsets.all(32),
child: TextField(
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
labelText: "用户名",
hintText: "用户名或邮箱",
prefixIcon: Icon(Icons.person),
border: InputBorder.none //隐藏下划线
)),
//装饰
decoration: BoxDecoration(
// 边框浅灰色,宽度1像素
border: Border.all(color: Colors.red[200], width: 1.0),
//圆角
borderRadius: BorderRadius.circular(5.0),
),
)焦点控制
FocusNode: 与Widget绑定,代表了这个Widget的焦点
FocusScope: 焦点控制范围
FocusScopeNode:控制焦点
class _TextFocusState extends State<TextFocusWidget> {
FocusNode focusNode1 = new FocusNode();
FocusNode focusNode2 = new FocusNode();
void _listener() {
debugPrint("用户名输入框焦点:${focusNode1.hasFocus}");
}
@override
void initState() {
super.initState();
//监听焦点状态改变事件
focusNode1.addListener(_listener);
}
@override
void dispose() {
super.dispose();
focusNode1.dispose();
focusNode2.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
TextField(
autofocus: true,
//关联焦点
focusNode: focusNode1,
//设置键盘动作为: 下一步
textInputAction: TextInputAction.next,
//点击下一步执行回调
onEditingComplete: () {
//获得 context对应UI树的焦点范围 的焦点控制器
FocusScopeNode focusScopeNode = FocusScope.of(context);
//将焦点交给focusNode2
focusScopeNode.requestFocus(focusNode2);
},
decoration: InputDecoration(
labelText: "用户名",
hintText: "用户名或邮箱",
prefixIcon: Icon(Icons.person)),
),
TextField(
//隐藏正在编辑的文本
obscureText: true,
focusNode: focusNode2,
decoration: InputDecoration(
labelText: "密码",
hintText: "您的登录密码",
prefixIcon: Icon(Icons.lock)),
),
custom(),
],
);
}
}获取输入内容
获取输入内容有两种方式:
1. 定义两个变量,用于保存用户名和密码,然后在onChange触发时,各自保存一下输入内容。
2. 通过controller直接获取。
onChange获得输入内容:
TextField(
onChanged: (s) => debugPrint("ssss:$s"),
)controller获取:
定义一个controller:
//定义一个controller
TextEditingController _unameController=new TextEditingController();
```
然后设置输入框controller:
```dart
TextField(
controller: _unameController, //设置controller
...
)通过controller获取输入框内容
debugPrint(_unameController.text)TextFormField
`TextFormField`比`TextField`多了一些属性,其中 ***validator***用于设置验证回调。在单独使用时与`TextField`没有太大的区别。当结合`From`,利用`From`可以对输入框进行分组,然后进行一些统一操作(验证)
class _TextFocusState extends State<TextFocusWidget> {
//全局key
GlobalKey<FormState> _key = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Form(
//类似 id
key: _key,
child: Column(
children: <Widget>[
TextFormField(
autofocus: true,
decoration: InputDecoration(
labelText: "用户名",
hintText: "用户名或邮箱",
icon: Icon(Icons.person)),
// 校验用户名
validator: (v) {
return v.trim().length > 0 ? null : "用户名不能为空";
}),
TextFormField(
decoration: InputDecoration(
labelText: "密码",
hintText: "您的登录密码",
icon: Icon(Icons.lock)),
// 校验用户名
validator: (v) {
return v.trim().length > 0 ? null : "密码不能为空";
}),
RaisedButton(
onPressed: () {
//Form所有TextFormField成功 返回true
if (_key.currentState.validate()) {
}
},
child: Text("提交"),
)
],
));
}
}
第一个页面
入口函数
一个Flutter工程的入口函数与Dart命令行工程一样是`main`,不同的是在Flutter中执行`runApp(ArticleApp())` 就能够在手机屏幕上展示这个Widget。
import 'package:flutter/material.dart';
void main() => runApp(new ArticleApp());ArticleApp
我们要实现的文章列表页面UI就在`ArticleApp`中定义:
class ArticleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: new Text(
'文章',
style: const TextStyle(color: Colors.white),
),
),
body: new ArticlePage(),
),
);
}
}
`build`方法中返回的就是我们需要显示在屏幕上的widget。`MaterialApp`代表使用Material Design风格,这是一个封装了很多Android MD设计所必须要的组件的小部件。假设我们需要显示一个`Text`,而没有包裹在`MaterialApp`内:
class ArticleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
//Center:摆放在中间
return Center(
child: Text("你好!"),
);
}
}如果直接运行则会出现异常,因为Flutter不知道以什么顺序摆放文字
因此我们不得不给`Text`指名`textDirection`属性:
class ArticleApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Text("你好!",
style: const TextStyle(color: Colors.white),
textDirection: TextDirection.ltr),
);
}
}但是如果包含在`MaterialApp`当中我们就不需要指名类似`textDirection`这样的属性了,因为内部已经内置了一套风格,指明了这些必须属性。而`Scaffold`则实现了基本的 Material Design布局结构,在 Material 设计中定义的单个界面上的各种布局元素,在 Scaffold 中都支持。比如:AppBar、抽屉菜单、BottomNavigationBar等等。
ArticlePage
在我们的布局中指定了`Scaffold`的body(主体)为`ArticlePage`,这是一个我们自定义的组合Widget。
class ArticlePage extends StatefulWidget {
@override
_ArticlePageState createState() => _ArticlePageState();
}
class _ArticlePageState extends State<ArticlePage> {
///滑动控制器
ScrollController _controller = new ScrollController();
///控制小菊花的显示
bool _isLoading = true;
///请求到的文章数据
List articles = [];
///banner图
List banners = [];
///总文章数有多少
var listTotalSize = 0;
///分页加载,当前页码
var curPage = 0;
@override
void initState() {
super.initState();
_controller.addListener(() {
///获得 SrollController 监听控件可以滚动的最大范围
var maxScroll = _controller.position.maxScrollExtent;
///获得当前位置的像素值
var pixels = _controller.position.pixels;
///当前滑动位置到达底部,同时还有更多数据
if (maxScroll == pixels && articles.length < listTotalSize) {
///加载更多
_getArticlelist();
}
});
_pullToRefresh();
}
_getArticlelist([bool update = true]) async {
/// 请求成功是map,失败是null
var data = await Api.getArticleList(curPage);
if (data != null) {
var map = data['data'];
var datas = map['datas'];
///文章总数
listTotalSize = map["total"];
if (curPage == 0) {
articles.clear();
}
curPage++;
articles.addAll(datas);
///更新ui
if (update) {
setState(() {});
}
}
}
_getBanner([bool update = true]) async {
var data = await Api.getBanner();
if (data != null) {
banners.clear();
banners.addAll(data['data']);
if (update) {
setState(() {});
}
}
}
///下拉刷新
Future<void> _pullToRefresh() async {
curPage = 0;
Iterable<Future> futures = [_getArticlelist(), _getBanner()];
await Future.wait(futures);
_isLoading = false;
setState(() {});
return null;
}
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
///小菊花
Offstage(
offstage: !_isLoading, //是否隐藏
child: new Center(child: CircularProgressIndicator()),
),
///内容
Offstage(
offstage: _isLoading,
child: new RefreshIndicator(
child: ListView.builder(
itemCount: articles.length + 1,
itemBuilder: (context, i) => _buildItem(i),
controller: _controller,
),
onRefresh: _pullToRefresh),
)
],
);
}
Widget _buildItem(int i) {
if (i == 0) {
return new Container(
height: 180.0,
child: _bannerView(),
);
}
var itemData = articles[i - 1];
return new ArticleItem(itemData);
}
Widget _bannerView() {
var list = banners.map((item) {
return Image.network(item['imagePath'], fit: BoxFit.cover);
}).toList();
return list.isNotEmpty
? BannerView(
list,
intervalDuration: const Duration(seconds: 3),
)
: null;
}
}这个Widget的代码比较多,它配置了我们见到的banner、与文章列表。代码中重写了State的生命周期方法`initState`与`build`。我们首先来观察`build`方法:
@override
Widget build(BuildContext context) {
//Stack:帧布局
return Stack(
children: <Widget>[
///正在加载
Offstage( //可以控制是否隐藏
offstage: !_isLoading, //是否隐藏
child: new Center(child: CircularProgressIndicator()),//圆形进度指示器(小菊花)
),
///内容
Offstage(
offstage: _isLoading,
child: new RefreshIndicator( //下拉刷新
child: ListView.builder(
itemCount: articles.length + 1, //列表视图的个数
itemBuilder: (context, i) => _buildItem(i),//类似adapter,item显示什么?返回widget
controller: _controller,//滑动控制器
),
onRefresh: _pullToRefresh),//刷新回调方法
)
],
);
}这段代码中各个部分都给到了注释,`_buildItem`与`_pullToRefresh`方法分别用于条目视图的生成与新数据的获取。
_pullToRefresh
`_pullToRefresh`是传递给下拉刷新组件:`RefreshIndicator`的刷新回调方法参数,它需要返回一个`Future<void>`,同时我们初次进入页面也需要自动的去获取一次数据,所以我们还会在`initState`方法中主动的调用一次该方法。
Future<void> _pullToRefresh() async {
curPage = 0;
Iterable<Future> futures = [_getArticlelist(), _getBanner()];
await Future.wait(futures);
_isLoading = false;
setState(() {});
return null;
}在这个方法中,我们需要重新请求文章列表与banner图,因此借助`Future.wait`组合两个任务,在两个任务都完成后,再利用`setState`更新UI完成重绘。
_buildItem
获取到数据之后,接下来我们需要对这些数据进行展示
Widget _buildItem(int i) {
if (i == 0) {
return new Container(
height: 180.0,
child: _bannerView(),
);
}
var itemData = articles[i - 1];
return new ArticleItem(itemData);
}
Widget _bannerView() {
///banners是请求到的banner信息组,其中imagePath代表了图片地址
///map意为映射,对banners中的数据进行遍历并返回Iterable<?>迭代器,
///?则是在map的参数:一个匿名方法中返回的类型
var list = banners.map((item) {
return Image.network(item['imagePath'], fit: BoxFit.cover);
}).toList();
///BannerView的条目不能为空
return list.isNotEmpty
? BannerView(
list,
///切换时间
intervalDuration: const Duration(seconds: 3),
)
: null;
}`_buildItem`用于生成ListView当中的条目。注意在配置ListView时,我们给的`itemCount`为:`articles.length + 1`。articles就是请求到的文章信息数量,而**+1**则是为了显示banner。因此当`i=0`,显示第一个条目时候,我们返回了一个`BannerView`。这个`BannerView`其实是一个库(关于如何导入第三方库在最后)。而`ArticleItem`则又是我们自己定义的用于显示文章信息item的组合Widget。
ArticleItem
class ArticleItem extends StatelessWidget {
final itemData;
const ArticleItem(this.itemData);
@override
Widget build(BuildContext context) {
///时间与作者
Row author = new Row( //水平线性布局
children: <Widget>[
//expanded 最后摆我,相当于linearlayout的weight权重
new Expanded(
child: Text.rich(TextSpan(children: [
TextSpan(text: "作者: "),
TextSpan(
text: itemData['author'],
style: new TextStyle(color: Theme.of(context).primaryColor))
]))),
new Text(itemData['niceDate'])//时间
],
);
///标题
Text title = new Text(
itemData['title'],
style: new TextStyle(fontSize: 16.0, color: Colors.black),
textAlign: TextAlign.left,
);
///章节名
Text chapterName = new Text(itemData['chapterName'],
style: new TextStyle(color: Theme.of(context).primaryColor));
Column column = new Column( //垂直线性布局
crossAxisAlignment: CrossAxisAlignment.start, //子控件左对齐
children: <Widget>[
new Padding(
padding: EdgeInsets.all(10.0),
child: author,
),
new Padding(
padding: EdgeInsets.fromLTRB(10.0, 5.0, 10.0, 5.0),
child: title,
),
new Padding(
padding: EdgeInsets.fromLTRB(10.0, 5.0, 10.0, 10.0),
child: chapterName,
),
],
);
return new Card(
///阴影效果
elevation: 4.0,
child: column,
);
}
}Expanded
可以按比例“扩伸”Row、Column所占用的空间。
const Expanded({
int flex = 1,
@required Widget child,
})flex为弹性系数,和Android中的`LinearLayout`的`weight`比重效果一致。
class _LayoutWidgetState extends State<LayoutWidget> {
@override
Widget build(BuildContext context) {
return Row(
//将Row 分成 2+3+1分,
children: <Widget>[
Expanded(flex:2,child: Container(child: Text('1'), color: Colors.red)),
Expanded(flex:3,child: Container(child: Text('1'), color: Colors.blue)),
Expanded(flex:1,child: Container(child: Text('1'), color: Colors.yellow)),
],
);
}
}网络请求
一个app中,网络请求是最基本的功能,我们需要使用网络请求数据用于显示或者进行不同的逻辑处理。在我们的案例中,同样需要请求文章数据与banner数据。在Dart SDK中的io库其实提供了`HttpClient` 进行网络请求。大家都知道,Java中也提供了HttpConnection,但是我们更喜欢使用更加方便的OkHttp,所以一般开发中,我们可能使用一些更加方便的网络库,比如**http、dio**等等。进入https://pub.dartlang.org/ 输入库名就能够搜索到相关的库。这次我们使用dio来完成网络的请求:
class HttpManager {
Dio _dio;
static HttpManager _instance;
factory HttpManager.getInstance() {
if (null == _instance) {
_instance = new HttpManager._internal();
}
return _instance;
}
//以 _ 开头的函数、变量无法在库外使用
HttpManager._internal() {
///基础配置
BaseOptions options = new BaseOptions(
baseUrl: Api.baseUrl, //基础地址
connectTimeout: 5000, //连接服务器超时时间,单位是毫秒
receiveTimeout: 3000, //读取超时
);
_dio = new Dio(options);
}
request(url, {String method = "get"}) async {
try {
///默认使用get请求
Options option = new Options(method: method);
Response response = await _dio.request(url, options: option);
///一般来说,提供的是json字符串,response.data得到的就是这个json对应的map
return response.data;
} catch (e) {
return null;
}
}
}导入库
在Flutter工程中存在一个`pubspec.yaml`文件。此文件类似`build.gradle`,在这个文件中进行我们整个工程的一些配置,其中就包括了库的导入。配置完成之后,点击右上角的`Packages get`就能自动下载依赖。
库的最新版本可以进入https://pub.dartlang.org/ 搜索。
添加文章详情——路由与导航
路由管理
路由(Route)在移动开发中通常指页面(Page),在Android中通常指一个Activity。所谓路由管理,就是管理页面之间如何跳转,通常也可被称为导航管理。这和原生开发类似,无论是Android还是iOS,导航管理都会维护一个路由栈,路由入栈(push)操作对应打开一个新页面,路由出栈(pop)操作对应页面关闭操作,而路由管理主要是指如何来管理路由栈。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: MainRoute(),
);
}
}
class MainRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("主页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () {
//导航到新路由
Navigator.push(context, MaterialPageRoute(builder: (context) {
return SecondRoute();
}));
},
child: Text("进入第二页"),
)
],
),
);
}
}
class SecondRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("第二页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () {
//路由pop弹出
Navigator.pop(context);
},
child: Text("返回"),
)
],
),
);
}
}命名路由
命名路由(Named Route)即给路由起一个名字,然后可以通过路由名字直接打开新的路由。这为路由管理带来了一种直观、简单的方式。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
//home: MainRoute(),
//注册路由表
routes: {
/// '/'是特殊地址,第一个页面
"/" :(context) => MainRoute(),
"new_page": (context) => SecondRoute(),
},
);
}
}
class MainRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("主页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () async {
//导航到新路由
var result = await Navigator.pushNamed(context, "new_page");
debugPrint("返回:$result");
},
child: Text("进入第二页"),
)
],
),
);
}
}
class SecondRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("第二页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () {
//路由pop弹出
Navigator.pop(context, "结束");
},
child: Text("返回"),
)
],
),
);
}
}命名路由的最大优点是直观,我们可以通过语义化的字符串来管理路由。但其有一个明显的缺点:不能直接传递路由参数。假设SecondRoute,需要接受一个字符串参数`tip`,然后再在屏幕中心将`tip`的内容显示出来。因为命名路由需要提前注册到路由表中,所以就无法动态修改`tip`参数。
自定义路由切换动画
Material库中提供了MaterialPageRoute,它在Android上会上下滑动切换。如果想自定义路由切换动画,可以使用PageRouteBuilder。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: MainRoute(),
);
}
}
class MainRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("主页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () async {
//导航到新路由
var result = await Navigator.push(
context,
PageRouteBuilder(
///动画时间
transitionDuration: Duration(milliseconds: 500),
pageBuilder: (BuildContext context, Animation animation,
Animation secondaryAnimation) {
///平移
return SlideTransition(
///Tween:在补间动画中,定义开始点结束点
position: new Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: const Offset(0.0, 0.0),
).animate(animation),
child: SecondRoute(),
);
},
),
);
debugPrint("返回:$result");
},
child: Text("进入第二页"),
)
],
),
);
}
}
class SecondRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("第二页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () {
//路由pop弹出
Navigator.pop(context, "结束");
},
child: Text("返回"),
)
],
),
);
}
}同时我们也可以对动画进行组合
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: MainRoute(),
);
}
}
class MainRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("主页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () async {
//导航到新路由
var result = await Navigator.push(
context,
PageRouteBuilder(
///动画时间
transitionDuration: Duration(milliseconds: 500),
pageBuilder: (BuildContext context, Animation animation,
Animation secondaryAnimation) {
///透明渐变与旋转
return new FadeTransition(
opacity: animation,
child: new RotationTransition(
turns: new Tween<double>(begin: 0.5, end: 1.0)
.animate(animation),
child: SecondRoute(),
),
);
},),
);
debugPrint("返回:$result");
},
child: Text("进入第二页"),
)
],
),
);
}
}
class SecondRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("第二页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () {
//路由pop弹出
Navigator.pop(context, "结束");
},
child: Text("返回"),
)
],
),
);
}
}注意点
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
appBar: AppBar(
title: Text("主页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () {
///Navigator.push内部其实就是 Navigator.of(context).push
Navigator.of(context).push(MaterialPageRoute(builder: (_){
return new SecondRoute();
}));
},
child: Text("进入第二页"),
)
],
),
),
);
}
}
class SecondRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("第二页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () {
//路由pop弹出
Navigator.pop(context);
},
child: Text("返回"),
)
],
),
);
}
}
这段代码运行会出现错误:!路由异常
问题关键点在于**Navigator operation requested with a context that does not include a Navigator.**(导航操作请求使用了不包含Navigator的上下文context)
`Navigator`实际上也是一个Widget,这个异常出现在`Navigator.of(context)`路由器的获取上,而这句代码会**从当前的context的父级一层层向上去查找一个`Navigator`**,我们当前传递的context就是MyApp,它的父级是root——UI根节点。`Navigator`这个widget的并不是由root创建的,因此在root下一级的上下文中无法获得`Navigator`。
> 在之前所有的路由案例中,我们的上下文是MainRoute,它的父级是MaterialApp。MaterialApp内部就会创建一个Navigator。
>
> MaterialApp->\_MaterialAppState->WidgetsApp->\_WidgetsAppState
>
> 
所以问题就在于,`Navigator`需要通过MaterialApp或者它孩子的上下文。
解决一
按照此笔记最开始的正常路由演示案例来进行修改。
解决二
使用`Builder`
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
appBar: AppBar(
title: Text("主页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
///
Builder(builder: (context){
return RaisedButton(
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(builder: (_){
return new SecondRoute();
}));
},
child: Text("进入第二页"),
);
})
],
),
),
);
}
}
class SecondRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("第二页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () {
//路由pop弹出
Navigator.pop(context);
},
child: Text("返回"),
)
],
),
);
}
}使用Builder嵌套,Builder的参数可以看成一个回调,接收自身的context并返回布局配置。现在路由是从Builder的父亲开始查找啦,自然能找到Navigator。

解决三
使用`navigatorKey`
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey();
@override
Widget build(BuildContext context) {
return new MaterialApp(
///指定路由器widget的key
navigatorKey: navigatorKey,
title: 'Flutter Demo',
home: Scaffold(
appBar: AppBar(
title: Text("主页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () {
///输出Navigator
debugPrint(navigatorKey.currentWidget.runtimeType.toString());
navigatorKey.currentState.push(MaterialPageRoute(builder: (_){
return new SecondRoute();
}));
},
child: Text("进入第二页"),
)
],
),
),
);
}
}
class SecondRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("第二页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () {
//路由pop弹出
Navigator.pop(context);
},
child: Text("返回"),
)
],
),
);
}
}

在创建`Navigator`的时候,会给一个**key**,这个key可以看成一个Widget的id。这里的**_navigator**就是我们指定的**navigatorKey**(如果我们没指定,会给默认值的,所以不要疑惑不指定是不是就不创建`Navigator`了)。而通过这个key,就能够获得这个`Navigator`。直接获得了路由自然不需要再去查找了!
完善第一个页面
之前我们编写了第一个页面显示了banner与文章列表,一般的,在点击banner与文章条目时需要进入新界面显示文章详情,而wanandroid的文章详情是一个web页面,因此我们需要增加一个新界面用于展示web
import 'package:flutter/material.dart';
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';
class WebViewPage extends StatefulWidget {
final data;
WebViewPage(this.data);
@override
_WebViewPageState createState() => _WebViewPageState();
}
class _WebViewPageState extends State<WebViewPage> {
bool isLoad = true;
FlutterWebviewPlugin flutterWebViewPlugin;
@override
void initState() {
super.initState();
flutterWebViewPlugin = new FlutterWebviewPlugin();
flutterWebViewPlugin.onStateChanged.listen((state) {
if (state.type == WebViewState.finishLoad) {
// 加载完成
setState(() {
isLoad = false;
});
} else if (state.type == WebViewState.startLoad) {
setState(() {
isLoad = true;
});
}
});
}
@override
void dispose() {
flutterWebViewPlugin.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
///WebView插件
return WebviewScaffold(
appBar: AppBar(
title: Text(widget.data['title']),
///appbar下边摆放一个进度条
bottom: PreferredSize(
preferredSize: const Size.fromHeight(1.0),
child: const LinearProgressIndicator()),
///透明度
bottomOpacity: isLoad ? 1.0 : 0.0,
),
withLocalStorage: true, //缓存,数据存储
url: widget.data['url'],
withJavascript: true);
}
}> flutter_webview_plugin是一个插件,需要引入:flutter_webview_plugin: ^0.3.0+2
首先我们加入banner的点击事件,在`page_article.dart`中修改`_bannerView`方法:
Widget _bannerView() {
var list = banners.map((item) {
///自动增加点击水波纹的widget
return InkWell(
child: Image.network(item['imagePath'], fit: BoxFit.cover), //fit 图片充满容器
///点击事件
onTap: () {
///跳转页面
Navigator.of(context).push(new MaterialPageRoute(builder: (context) {
return WebViewPage(item);
}));
},
);
}).toList();
return list.isNotEmpty
? BannerView(
list,
intervalDuration: const Duration(seconds: 3),
)
: null;
}然后还有文章条目的点击,在`article_item.dart`文件中修改`build`方法最后return的内容
return Card(
///阴影效果
elevation: 4.0,
child: InkWell(
child: column,
onTap: () {
Navigator.of(context).push(new MaterialPageRoute(builder: (context) {
///因为webview获取的是map的url地址,而文章数据中文章详情的key是link
itemData['url'] = itemData['link'];
return WebViewPage(itemData);
}));
},
),
);
动画与打包
动画
Flutter中的动画系统基于`Animation`对象的,和之前的手势不同,它不是一个Widget,这是因为`Animation`对象本身和UI渲染没有任何关系。Animation是一个抽象类,就相当于一个定时器,它用于保存动画的插值和状态,并执行数值的变化。widget可以在`build`函数中读取`Animation`对象的当前值, 并且可以监听动画的状态改变。
AnimationController
AnimationController用于控制动画,它包含动画的启动`forward()`、停止`stop()` 、反向播放 `reverse()`等方法。AnimationController会在动画的每一帧,就会生成一个新的值。默认情况下,AnimationController在给定的时间段内线性的生成从0.0到1.0(默认区间)的数字。
AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 2000), //动画时间
lowerBound: 10.0, //生成数字的区间
upperBound: 20.0, //10.0 - 20.0
vsync: this //TickerProvider 动画驱动器提供者
);Ticker
Ticker的作用是添加屏幕刷新回调,每次屏幕刷新都会调用`TickerCallback`。使用Ticker来驱动动画会防止屏幕外动画(动画的UI不在当前屏幕时,如锁屏时)消耗不必要的资源。因为Flutter中屏幕刷新时会通知Ticker,锁屏后屏幕会停止刷新,所以Ticker就不会再触发。最简单的做法为将`SingleTickerProviderStateMixin`添加到State的定义中。
import 'package:flutter/material.dart';
void main() => runApp(AnimationApp());
class AnimationApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "animation",
home: Scaffold(
appBar: AppBar(
title: Text('animation'),
),
body: AnimWidget(),
),
);
}
}
// 动画是有状态的
class AnimWidget extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _AnimWidgetState();
}
}
class _AnimWidgetState extends State<AnimWidget>
with SingleTickerProviderStateMixin {
AnimationController controller;
bool forward = true;
@override
void initState() {
super.initState();
controller = AnimationController(
// 动画的时长
duration: Duration(milliseconds: 2000),
lowerBound: 10.0,
upperBound: 100.0,
// 提供 vsync 最简单的方式,就是直接混入 SingleTickerProviderStateMixin
// 如果有多个AnimationController,则使用TickerProviderStateMixin。
vsync: this,
);
//状态修改监听
controller
..addStatusListener((AnimationStatus status) {
debugPrint("状态:$status");
})
..addListener(() {
setState(() => {});
});
debugPrint("controller.value:${controller.value}");
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Container(
width: controller.value,
height: controller.value,
color: Colors.blue,
),
RaisedButton(
child: Text("播放"),
onPressed: () {
if (forward) {
controller.forward();
} else {
controller.reverse();
}
forward = !forward;
},
),
RaisedButton(
child: Text("停止"),
onPressed: () {
controller.stop();
},
)
],
);
}
}> 动画状态监听:在forword结束之后状态为completed。在reverse结束之后状态为dismissed
Tween
默认情况下,`AnimationController`对象值为:double类型,范围是0.0到1.0 。如果我们需要不同的范围或不同的数据类型,则可以使用Tween来配置动画以生成不同的范围或数据类型的值。要使用Tween对象,需要调用其`animate()`方法,然后传入一个控制器对象,同时动画过程中产生的数值由`Tween`的`lerp`方法决定。
import 'package:flutter/material.dart';
void main() => runApp(AnimationApp());
class AnimationApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "animation",
home: Scaffold(
appBar: AppBar(
title: Text('animation'),
),
body: AnimWidget(),
),
);
}
}
// 动画是有状态的
class AnimWidget extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _AnimWidgetState();
}
}
class _AnimWidgetState extends State<AnimWidget>
with SingleTickerProviderStateMixin {
AnimationController controller;
bool forward = true;
Tween<Color> tween;
@override
void initState() {
super.initState();
controller = AnimationController(
// 动画的时长
duration: Duration(milliseconds: 2000),
// 提供 vsync 最简单的方式,就是直接继承 SingleTickerProviderStateMixin
vsync: this,
);
//使用Color
tween = ColorTween(begin: Colors.blue, end: Colors.yellow);
//添加动画值修改监听
tween.animate(controller)..addListener(() => setState(() {}));
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Container(
width: 100,
height: 100,
//获取动画当前值
color: tween.evaluate(controller),
),
RaisedButton(
child: Text("播放"),
onPressed: () {
if (forward) {
controller.forward();
} else {
controller.reverse();
}
forward = !forward;
},
),
RaisedButton(
child: Text("停止"),
onPressed: () {
controller.stop();
},
)
],
);
}
}Curve
动画过程默认是线性的(匀速),如果需要非线形的,比如:加速的或者先加速后减速等。Flutter中可以通过Curve(曲线)来描述动画过程。
import 'package:flutter/material.dart';
void main() => runApp(AnimationApp());
class AnimationApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "animation",
home: Scaffold(
appBar: AppBar(
title: Text('animation'),
),
body: AnimWidget(),
),
);
}
}
// 动画是有状态的
class AnimWidget extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _AnimWidgetState();
}
}
class _AnimWidgetState extends State<AnimWidget>
with SingleTickerProviderStateMixin {
AnimationController controller;
Animation<double> animation;
bool forward = true;
@override
void initState() {
super.initState();
controller = AnimationController(
// 动画的时长
duration: Duration(milliseconds: 2000),
// 提供 vsync 最简单的方式,就是直接继承 SingleTickerProviderStateMixin
vsync: this,
);
//弹性
animation = CurvedAnimation(parent: controller, curve: Curves.bounceIn);
//使用Color
animation = Tween(begin: 10.0, end: 100.0).animate(animation)
..addListener(() {
setState(() => {});
});
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Container(
//不需要转换
width: animation.value,
height: animation.value,
//获取动画当前值
color: Colors.blue,
),
RaisedButton(
child: Text("播放"),
onPressed: () {
if (forward) {
controller.forward();
} else {
controller.reverse();
}
forward = !forward;
},
),
RaisedButton(
child: Text("停止"),
onPressed: () {
controller.stop();
},
)
],
);
}
}AnimatedWidget
通过上面的学习我们能够感受到`Animation`对象本身和UI渲染没有任何关系。而通过`addListener()`和`setState()` 来更新UI这一步其实是通用的,如果每个动画中都加这么一句是比较繁琐的。AnimatedWidget类封装了调用`setState()`的细节,简单来说就是自动调用`setState()`。
Flutter中已经封装了很多动画,比如对widget进行缩放,可以直接使用`ScaleTransition
import 'package:flutter/material.dart';
void main() => runApp(AnimationApp());
class AnimationApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "animation",
home: Scaffold(
appBar: AppBar(
title: Text('animation'),
),
body: AnimWidget(),
),
);
}
}
// 动画是有状态的
class AnimWidget extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _AnimWidgetState();
}
}
class _AnimWidgetState extends State<AnimWidget>
with SingleTickerProviderStateMixin {
AnimationController controller;
Animation<double> animation;
bool forward = true;
@override
void initState() {
super.initState();
controller = AnimationController(
// 动画的时长
duration: Duration(milliseconds: 2000),
// 提供 vsync 最简单的方式,就是直接继承 SingleTickerProviderStateMixin
vsync: this,
);
//弹性
animation = CurvedAnimation(parent: controller, curve: Curves.bounceIn);
//使用Color
animation = Tween(begin: 10.0, end: 100.0).animate(animation);
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
ScaleTransition(
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
scale: controller,
),
RaisedButton(
child: Text("播放"),
onPressed: () {
if (forward) {
controller.forward();
} else {
controller.reverse();
}
forward = !forward;
},
),
RaisedButton(
child: Text("停止"),
onPressed: () {
controller.stop();
},
)
],
);
}
}Hero动画
Hero动画就是在路由切换时,有一个共享的Widget可以在新旧路由间切换,由于共享的Widget在新旧路由页面上的位置、外观可能有所差异,所以在路由切换时会逐渐过渡,这样就会产生一个Hero动画。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
appBar: AppBar(
title: Text("主页"),
),
body: Route1()),
);
}
}
// 路由A
class Route1 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.topCenter,
child: InkWell(
child: Hero(
tag: "avatar", //唯一标记,前后两个路由页Hero的tag必须相同
child: CircleAvatar(
backgroundImage: AssetImage(
"assets/banner.jpeg",
),
),
),
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (_) {
return Route2();
}));
},
),
);
}
}
class Route2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Hero(
tag: "avatar", //唯一标记,前后两个路由页Hero的tag必须相同
child: Image.asset("assets/banner.jpeg")),
);
}
}组合动画
有些时候我们可能会需要执行一个动画序列执行一些复杂的动画。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: Route(),
);
}
}
class Route extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return RouteState();
}
}
class RouteState extends State<Route> with SingleTickerProviderStateMixin {
Animation<Color> color;
Animation<double> width;
AnimationController controller;
@override
void initState() {
super.initState();
controller = AnimationController(
// 动画的时长
duration: Duration(milliseconds: 2000),
// 提供 vsync 最简单的方式,就是直接继承 SingleTickerProviderStateMixin
vsync: this,
);
//高度动画
width = Tween<double>(
begin: 100.0,
end: 300.0,
).animate(
CurvedAnimation(
parent: controller,
curve: Interval(
//间隔,前60%的动画时间 1200ms执行高度变化
0.0, 0.6,
),
),
);
color = ColorTween(
begin: Colors.green,
end: Colors.red,
).animate(
CurvedAnimation(
parent: controller,
curve: Interval(
0.6, 1.0, //高度变化完成后 800ms 执行颜色编码
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("主页"),
),
body: InkWell(
///1、不用显式的去添加帧监听器,再调用setState()
///2、缩小动画构建的范围,如果没有builder,setState()将会在父widget上下文调用,导致父widget的build方法重新调用,现在只会导致动画widget的build重新调用
child: AnimatedBuilder(
animation: controller,
builder: (context, child) {
return Container(
color: color.value,
width: width.value,
height: 100.0,
);
}),
onTap: () {
controller.forward().whenCompleteOrCancel(() => controller.reverse());
},
),
);
}
}打包
Flutter在打Release包时候回使用AOT,因此在对一个Flutter测试时候务必使用Release来进行测试。打包命令:`flutter build apk` 。当然我们需要打包时,还需要配置一些比如签名的内容。配置这些内容和普通Android工程没有区别,都是在`build.gradle`中进行,只是Flutter工程AS没有提供GUI。
在Flutter工程的`android/app`下面的`build.gradle`可以修改包名、版本等信息,这就不用多说了。获得签名文件之后,将它复制到flutter的android目录:

然后在app的`build.gradle`中配置:
groovy
signingConfigs {
release {
keyAlias 'enjoy'
keyPassword '123456'
// 因为是放到父级的根目录,使用rootProject
// 如果放在这个build.gradle的同级,直接使用file
storeFile rootProject.file('enjoy.jks')
storePassword '123456'
}
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.release
}
}饼图
<https://github.com/google/charts>
> Stack布局中的fit属性与Image的fit类似,表示内容的扩充情况。默认为`StackFit.loose`表示`Stack`与内容一样大。如果设置为`StackFit.passthrough`则表示`Stack`父Widget的约束会传给Stack内部非Positioned的子Widget。效果如代码中的`StackFit.dart`
摘自:享学课堂Lance老师笔记
Flutter开发的WanAndroid
Flutter开发的WanAndroid-玩Android - wanandroid.com
推荐7个Flutter重磅开源项目!
推荐7个Flutter重磅开源项目! - 哔哩哔哩