一、前言
本系列文章旨在快速复习并上手Flutter开发,并在适当分享在项目实战过程中遇到的一些比较有价值的知识内容:
本系列文章内容篇幅如下:
- 一、了解Flutter开发
*- Flutter的特性与应用场景
-
- Flutter绘制原理
-
- 与Flutter相关的技术原理
-
- 搭建Flutter开发环境
-
- 创建Flutter项目的几种方式
- 二、快速入门Flutter开发知识大纲
*- Dart语言快速入门
-
- Flutter的Widget
- 三、常见应用功能模块与开源项目
*- 常见应用功能模块
-
- 不错的开源项目
二、单子布局组件
单子布局组件的含义是其只有一个子组件,可以通过设置一些属性设置该子组件所在的位置信息等。
比较常用的单子布局组件有:Align、Center、Padding、Container。
1. Align组件
1.1. Align介绍
看到Align
这个词,我们就知道它有我们的对齐方式有关。
- 在其他端的开发中(iOS、Android、前端)Align通常只是一个属性而已
- 但是Flutter中Align也是一个组件
我们可以通过源码来看一下Align有哪些属性:
dart
const Align({
Key key,
this.alignment: Alignment.center, // 对齐方式,默认居中对齐
this.widthFactor, // 宽度因子,不设置的情况,会尽可能大
this.heightFactor, // 高度因子,不设置的情况,会尽可能大
Widget child // 要布局的子Widget
})
这里我们特别解释一下widthFactor
和heightFactor
作用:
- 因为子组件在父组件中的对齐方式必须有一个前提,就是父组件得知道自己的范围(宽度和高度);
- 如果
widthFactor
和heightFactor
不设置,那么默认Align会尽可能的大(尽可能占据自己所在的父组件); - 我们也可以对他们进行设置,比如widthFactor设置为3,那么相对于Align的宽度是子组件跨度的3倍;
1.2. Align演练
我们简单演练一下Align
dart
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
showSemanticsDebugger: false,
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: Text("我的导航栏"),
backgroundColor: Colors.green,
),
body: HomePageBody(),
);
}
}
class HomePageBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return HomePageAlign();
}
}
// 单子布局组件
class HomePageAlign extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Align(
child: Icon(Icons.pets, size: 36, color: Colors.red),
alignment: Alignment.bottomRight,
widthFactor: 3,
heightFactor: 3,
);
}
}
2. Center组件
2.1. Center介绍
Center组件我们在前面已经用过很多次了。
- 事实上Center组件继承自Align,只是将alignment设置为
Alignment.center
。
源码分析:
dart
class Center extends Align {
const Center({
Key key,
double widthFactor,
double heightFactor,
Widget child
}) : super(key: key, widthFactor: widthFactor, heightFactor: heightFactor, child: child);
}
2.2. Center演练
我们将上面的代码Align换成Center
dart
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
showSemanticsDebugger: false,
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: Text("我的导航栏"),
backgroundColor: Colors.green,
),
body: HomePageBody(),
);
}
}
class HomePageBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return HomePageCenter();
}
}
// 表单TextField
class HomePageCenter extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Center(
child: Icon(Icons.pets, size: 36, color: Colors.red),
widthFactor: 20,
heightFactor: 20,
);
}
}
3. Padding组件
3.1. Padding介绍
- Padding组件在其他端也是一个属性而已,但是在Flutter中是一个Widget,
- Flutter中没有Margin这样一个Widget,这是因为外边距也可以通过Padding来完成。
- Padding通常用于设置子Widget到父Widget的边距(你可以称之为是父组件的内边距或子Widget的外边距)。
源码分析:
dart
const Padding({
Key key,
@required this.padding, // EdgeInsetsGeometry类型(抽象类),使用EdgeInsets
Widget child,
})
3.2. Padding演练
代码演练:
dart
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
showSemanticsDebugger: false,
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: Text("我的导航栏"),
backgroundColor: Colors.green,
),
body: HomePageBody(),
);
}
}
class HomePageBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return HomePagePadding();
}
}
// 单子布局HomePagePadding
class HomePagePadding extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(20),
child: Text(
"莫听穿林打叶声,何妨吟啸且徐行。竹杖芒鞋轻胜马,谁怕?一蓑烟雨任平生。",
style: TextStyle(
color: Colors.redAccent,
fontSize: 18
),
),
);;
}
}
4. Container组件
- Container组件类似于其他Android中的View,iOS中的
UIView
。 - 如果你需要一个视图,有一个
背景颜色
、图像
、有固定的尺寸
、需要一个边框
、圆角
等效果,那么就可以使用Container组件
。
4.1. Container介绍
Container在开发中被使用的频率是非常高的,特别是我们经常会将其作为容器组件。
下面我们来看一下Container有哪些属性:
dart
Container({
this.alignment,
this.padding, //容器内补白,属于decoration的装饰范围
Color color, // 背景色
Decoration decoration, // 背景装饰
Decoration foregroundDecoration, //前景装饰
double width,//容器的宽度
double height, //容器的高度
BoxConstraints constraints, //容器大小的限制条件
this.margin,//容器外补白,不属于decoration的装饰范围
this.transform, //变换
this.child,
})
大多数属性在介绍其它容器时都已经介绍过了,不再赘述,但有两点需要说明:
- 容器的大小可以通过
width
、height
属性来指定,也可以通过constraints
来指定,如果同时存在时,width
、height
优先。实际上Container内部会根据width
、height
来生成一个constraints
; color
和decoration
是互斥的,实际上,当指定color时,Container内会自动创建一个decoration;decoration
属性稍后我们详细学习;
4.2. Container演练
简单进行一个演示:
dart
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
showSemanticsDebugger: false,
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: Text("我的导航栏"),
backgroundColor: Colors.green,
),
body: HomePageBody(),
);
}
}
class HomePageBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return HomePageContainer();
}
}
// 单子布局HomePagePadding
class HomePageContainer extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Container(
color: Color.fromRGBO(3, 3, 255, .5),
width: 100,
height: 100,
child: Icon(Icons.pets, size: 32, color: Colors.white),
),
);
}
}
4.3. BoxDecoration
Container
有一个非常重要的属性 decoration
:
- 他对应的类型是Decoration类型,但是它是一个抽象类。
- 在开发中,我们经常使用它的实现类
BoxDecoration
来进行实例化。
BoxDecoration常见属性:
dart
const BoxDecoration({
this.color, // 颜色,会和Container中的color属性冲突
this.image, // 背景图片
this.border, // 边框,对应类型是Border类型,里面每一个边框使用BorderSide
this.borderRadius, // 圆角效果
this.boxShadow, // 阴影效果
this.gradient, // 渐变效果
this.backgroundBlendMode, // 背景混合
this.shape = BoxShape.rectangle, // 形变
})
部分效果演示:
dart
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
showSemanticsDebugger: false,
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: Text("我的导航栏"),
backgroundColor: Colors.green,
),
body: HomePageBody(),
);
}
}
class HomePageBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return HomePageContainer();
}
}
// 单子布局HomePagePadding
class HomePageContainer extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Container(
// color: Color.fromRGBO(3, 3, 255, .5),
width: 150,
height: 150,
child: Icon(Icons.pets, size: 32, color: Colors.white),
decoration: BoxDecoration(
color: Colors.amber, // 背景颜色
border: Border.all(
color: Colors.redAccent,
width: 3,
style: BorderStyle.solid
), // 这里也可以使用Border.all统一设置
// top: BorderSide(
// color: Colors.redAccent,
// width: 3,
// style: BorderStyle.solid
// ),
borderRadius: BorderRadius.circular(20), // 这里也可以使用.only分别设置
boxShadow: [
BoxShadow(
offset: Offset(5, 5),
color: Colors.purple,
blurRadius: 5
)
],
// shape: BoxShape.circle, // 会和borderRadius冲突
gradient: LinearGradient(
colors: [
Colors.green,
Colors.red
]
)
),
),
);
}
}
4.4. 实现圆角图像
上一个章节我们提到可以通过 Container+BoxDecoration
来实现圆角图像。
实现代码如下:
dart
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
showSemanticsDebugger: false,
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: Text("我的导航栏"),
backgroundColor: Colors.green,
),
body: HomePageBody(),
);
}
}
class HomePageBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return HomePageContainer();
}
}
// 单子布局HomePagePadding
class HomePageContainer extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Container(
width: 200,
height: 200,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
image: DecorationImage(
image: NetworkImage("https://tva1.sinaimg.cn/large/006y8mN6gy1g7aa03bmfpj3069069mx8.jpg"),
)
),
),
);
}
}
三、多子布局组件
在开发中,我们经常需要将多个Widget放在一起进行布局,比如水平方向、垂直方向排列,甚至有时候需要他们进行层叠,比如图片上面放一段文字等;
这个时候我们需要使用多子布局组件(Multi-child layout widgets)。
比较常用的多子布局组件是Row、Column、Stack,我们来学习一下他们的使用。
1. Flex组件
事实上,我们即将学习的Row
组件和Column
组件都继承自Flex
组件.
- Flex组件和Row、Column属性主要的区别就是多一个direction。
- 当direction的值为Axis.horizontal的时候,则是Row。
- 当direction的值为Axis.vertical的时候,则是Column。
在学习Row和Column之前,我们先学习主轴
和交叉轴
的概念。
因为Row是一行排布,Column是一列排布,那么它们都存在两个方向,并且两个Widget排列的方向应该是对立的。
它们之中都有主轴(MainAxis)和交叉轴(CrossAxis)的概念:
- 对于Row来说,主轴(MainAxis)和交叉轴(CrossAxis)分别是下图
- 对于Column来说,主轴(MainAxis)和交叉轴(CrossAxis)分别是下图
2. Row组件
2.1 Row介绍
Row组件用于将所有的子Widget排成一行,实际上这种布局应该是借鉴于Web的Flex布局。
如果熟悉Flex布局,会发现非常简单。
从源码中查看Row的属性:
dart
Row({
Key key,
MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start, // 主轴对齐方式
MainAxisSize mainAxisSize = MainAxisSize.max, // 水平方向尽可能大
CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center, // 交叉处对齐方式
TextDirection textDirection, // 水平方向子widget的布局顺序(默认为系统当前Locale环境的文本方向(如中文、英语都是从左往右,而阿拉伯语是从右往左))
VerticalDirection verticalDirection = VerticalDirection.down, // 表示Row纵轴(垂直)的对齐方向
TextBaseline textBaseline, // 如果上面是baseline对齐方式,那么选择什么模式(有两种可选)
List<Widget> children = const <Widget>[],
})
部分属性详细解析:(不过文字是真的难描述,后续推出视频学习较差)
mainAxisSize
:
- 表示Row在主轴(水平)方向占用的空间,默认是
MainAxisSize.max
,表示尽可能多的占用水平方向的空间,此时无论子widgets实际占用多少水平空间,Row的宽度始终等于水平方向的最大宽度 - 而
MainAxisSize.min
表示尽可能少的占用水平空间,当子widgets没有占满水平剩余空间,则Row的实际宽度等于所有子widgets占用的的水平空间;
mainAxisAlignment
:表示子Widgets在Row所占用的水平空间内对齐方式
- 如果mainAxisSize值为
MainAxisSize.min
,则此属性无意义,因为子widgets的宽度等于Row的宽度 - 只有当mainAxisSize的值为
MainAxisSize.max
时,此属性才有意义 MainAxisAlignment.start
表示沿textDirection的初始方向对齐,- 如textDirection取值为
TextDirection.ltr
时,则MainAxisAlignment.start
表示左对齐,textDirection取值为TextDirection.rtl
时表示从右对齐。 - 而
MainAxisAlignment.end
和MainAxisAlignment.start
正好相反; MainAxisAlignment.center
表示居中对齐。
crossAxisAlignment
:表示子Widgets在纵轴方向的对齐方式
- Row的高度等于子Widgets中最高的子元素高度
- 它的取值和MainAxisAlignment一样(包含
start
、end
、center
三个值) - 不同的是crossAxisAlignment的参考系是verticalDirection,即verticalDirection值为
VerticalDirection.down
时crossAxisAlignment.start
指顶部对齐,verticalDirection值为VerticalDirection.up
时,crossAxisAlignment.start
指底部对齐;而crossAxisAlignment.end
和crossAxisAlignment.start
正好相反;
2.2 Row演练
我们来对部分属性进行简单的代码演练,其他一些属性大家自己学习一下
dart
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
showSemanticsDebugger: false,
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("我的导航栏"),
backgroundColor: Colors.green,
),
body: HomePageBody(),
);
}
}
class HomePageBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return HomePageRow();
}
}
// 单子布局HomePageRow
class HomePageRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child:Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,//子控件间的间隔均匀分布
crossAxisAlignment: CrossAxisAlignment.end,// Row的 交叉轴(也就是竖轴) 的结束那条线(也就是底部)对齐
mainAxisSize: MainAxisSize.max,//间隔尽可能大
children: <Widget>[
Container(color: Colors.red, width: 60, height: 60),
Container(color: Colors.blue, width: 80, height: 80),
Container(color: Colors.green, width: 70, height: 70),
Container(color: Colors.orange, width: 100, height: 100),
],
),
);
}
}
2.3 mainAxisSize
默认情况下,Row会尽可能占据多的宽度,让子Widget在其中进行排布,这是因为mainAxisSize属性默认值是MainAxisSize.max
。
我们来看一下,如果这个值被修改为MainAxisSize.min
会什么变化:
2.4 TextBaseline
关于TextBaseline的取值解析
2.5 Expanded
如果我们希望红色和黄色的Container Widget
不要设置固定的宽度,而是占据剩余的部分,这个时候应该如何处理呢?
这个时候我们可以使用 Expanded
来包裹 Container Widget
,并且将它的宽度不设置值;
- flex属性,弹性系数,
Row
会根据两个Expanded
的弹性系数来决定它们占据剩下空间的比例
dart
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
showSemanticsDebugger: false,
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("我的导航栏"),
backgroundColor: Colors.green,
),
body: HomePageBody(),
);
}
}
class HomePageBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return HomePageRow();
}
}
// // 多子布局HomePageRow
// class HomePageRow extends StatelessWidget {
// @override
// Widget build(BuildContext context) {
// return Center(
// child:Row(
// mainAxisAlignment: MainAxisAlignment.spaceEvenly,//子控件间的间隔均匀分布
// crossAxisAlignment: CrossAxisAlignment.end,// Row的 交叉轴(也就是竖轴) 的结束那条线(也就是底部)对齐
// mainAxisSize: MainAxisSize.max,//间隔尽可能大/小
// children: <Widget>[
// Container(color: Colors.red, width: 60, height: 60),
// Container(color: Colors.blue, width: 80, height: 80),
// Container(color: Colors.green, width: 70, height: 70),
// Container(color: Colors.orange, width: 100, height: 100),
// ],
// ),
// );
// }
// }
// 多子布局HomePageRow+Expanded
class HomePageRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child:Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Expanded(
flex: 1,
child: Container(color: Colors.red, height: 60),
),
Container(color: Colors.blue, width: 80, height: 80),
Container(color: Colors.green, width: 70, height: 70),
Expanded(
flex: 1,
child: Container(color: Colors.orange, height: 100),
)
],
),
);
}
}
3. Column组件
Column组件用于将所有的子Widget排成一列,学会了前面的Row后,Column只是和row的方向不同而已。
3.1 Column介绍
我们直接看它的源码:我们发现和Row属性是一致的,不再解释
dart
Column({
Key key,
MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
MainAxisSize mainAxisSize = MainAxisSize.max,
CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
TextDirection textDirection,
VerticalDirection verticalDirection = VerticalDirection.down,
TextBaseline textBaseline,
List<Widget> children = const <Widget>[],
})
3.2 Column演练
我们直接将Row的代码中Row改为Column,查看代码运行效果:
dart
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
showSemanticsDebugger: false,
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("我的导航栏"),
backgroundColor: Colors.green,
),
body: HomePageBody(),
);
}
}
class HomePageBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return HomePageColumn();
}
}
// 多子布局HomePageColumn+Expanded
class HomePageColumn extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child:Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Expanded(
flex: 1,
child: Container(color: Colors.red, width: 60),
),
Container(color: Colors.blue, width: 80, height: 80),
Container(color: Colors.green, width: 70, height: 70),
Expanded(
flex: 1,
child: Container(color: Colors.orange, width: 100),
)
],
),
);
}
}
4. Stack组件
在开发中,我们多个组件很有可能需要重叠显示,比如在一张图片上显示文字或者一个按钮等。
- 在Android中可以使用Frame来实现
- 在Web端可以使用绝对定位
- 在Flutter中我们需要使用层叠布局Stack
4.1. Stack介绍
我们还是通过源码来看一下Stack有哪些属性:
dart
Stack({
Key key,
this.alignment = AlignmentDirectional.topStart,
this.textDirection,
this.fit = StackFit.loose,
this.overflow = Overflow.clip,
List<Widget> children = const <Widget>[],
})
参数解析:
alignment
:- 此参数决定如何去对齐没有定位(没有使用Positioned)或部分定位的子widget。
- 所谓部分定位,在这里 特指没有在某一个轴上定位:
- left、right为横轴
- top、bottom为纵轴
- 只要包含某个轴上的一个定位属性就算在该轴上有定位
textDirection
:- 和Row、Wrap的textDirection功能一样,都用于决定alignment对齐的参考系即:
- textDirection的值为
TextDirection.ltr
,则alignment的start
代表左,end
代表右; - textDirection的值为
TextDirection.rtl
,则alignment的start
代表右,end
代表左。
fit
:- 此参数用于决定没有定位的子widget如何去适应Stack的大小。
StackFit.loose
表示使用子widget的大小,StackFit.expand
表示扩伸到Stack的大小。
overflow
:- 此属性决定如何显示超出Stack显示空间的子widget,
- 值为
Overflow.clip
时,超出部分会被剪裁(隐藏) - 值为
Overflow.visible
时则不会。
4.2. Stack演练
Stack会经常和Positioned一起来使用,Positioned可以决定组件在Stack中的位置,用于实现类似于Web中的绝对定位效果。
我们来看一个简单的演练:
dart
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
showSemanticsDebugger: false,
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("我的导航栏"),
backgroundColor: Colors.green,
),
body: HomePageBody(),
);
}
}
class HomePageBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return HomePageStack();
}
}
// 多子布局HomePageStack
class HomePageStack extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child:Stack(
children: <Widget>[
Container(
color: Colors.deepOrangeAccent,
width: 300,
height: 300,
),
Positioned(
left: 20,
top: 20,
child: Icon(Icons.favorite, size: 50, color: Colors.white)
),
Positioned(
bottom: 20,
right: 20,
child: Text("你好啊,李银河", style: TextStyle(fontSize: 20, color: Colors.white)),
)
],
),
);
}
}
注意: Positioned组件只能在Stack中使用。
四、JSON读取和解析
在开发中,我们经常会使用本地JSON或者从服务器请求数据后回去到JSON,拿到JSON后通常会将JSON转成Model对象来进行后续的操作,因为这样操作更加的方便,也更加的安全。
所以学习JSON的相关操作以及读取JSON后如何转成Model对象对于Flutter开发也非常重要。
1. JSON资源配置
JSON也属于一种资源,所以在使用之前需要先进行相关的配置
我们之前在学习使用Image组件时,用到了本地图片,本地图片必须在pubspec.yaml
中进行配置:
2. JSON读取解析
JSON资源读取
如果我们希望读取JSON资源
,可以使用package:flutter/services.dart
包中的rootBundle
。
在rootBundle
中有一个loadString
方法,可以去加载JSON资源
- 但是注意,查看该方法的源码,你会发现这个操作是一个异步的。
- 关于Future和async,这里就不再展开讲解,可以去查看之前的dart语法。
dart
Future<String> loadString(String key, { bool cache = true }) async {
...省略具体代码,可以自行查看源码
}
代码如下:(不要试图拷贝这个代码去运行,是没办法运行的)
dart
import 'package:flutter/services.dart' show rootBundle;
// 打印读取的结果是一个字符串
rootBundle.loadString("assets/yz.json").then((value) => print(value));
JSON字符串转化
拿到JSON字符串后,我们需要将其转成成我们熟悉的List和Map类型。
我们可以通过dart:convert
包中的json.decode
方法将其进行转化
代码如下:
dart
// 1.读取json文件
String jsonString = await rootBundle.loadString("assets/yz.json");
// 2.转成List或Map类型
final jsonResult = json.decode(jsonString);
对象Model定义
将JSON转成了List和Map类型后,就可以将List中的一个个Map转成Model对象,所以我们需要定义自己的Model
dart
class Anchor {
String nickname;
String roomName;
String imageUrl;
Anchor({
this.nickname,
this.roomName,
this.imageUrl
});
Anchor.withMap(Map<String, dynamic> parsedMap) {
this.nickname = parsedMap["nickname"];
this.roomName = parsedMap["roomName"];
this.imageUrl = parsedMap["roomSrc"];
}
}
3. JSON解析代码
上面我们给出了解析的一个个步骤,下面我们给出完整的代码逻辑
这里我单独创建了一个anchor.dart的文件,在其中定义了所有的相关代码:
-
准备一个Mock数据
json[ { "nickname" : "昵称1", "roomName" : "名字1", "roomSrc" : "链接1" }, { "nickname" : "昵称2", "roomName" : "名字2", "roomSrc" : "链接2" }, { "nickname" : "昵称3", "roomName" : "名字3", "roomSrc" : "链接3" } ]
-
之后外界只需要调用我内部的
getAnchors
就可以获取到解析后的数据了dartimport 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show rootBundle; import 'dart:convert'; import 'dart:async'; import 'dart:core'; main(List<String> args) { runApp( MaterialApp( showSemanticsDebugger: false, home: Scaffold( appBar: AppBar( title: Text("我的导航栏"), ), body: Center( child: Column( children: <Widget>[ Text("Json 转 模型"), MaterialButton( color: Colors.blue, textColor: Colors.white, child: Text("执行转换"), onPressed: (){ loadJsonData().then((List<Anchor> list){ final anchor = list.first; print(anchor.nickname); print(anchor.roomName); print(anchor.roomSrc); print("==========我是一条分割线==========="); }); } ), ], ), ), ), ) ); } class Anchor { String nickname = ""; String roomName = ""; String roomSrc = ""; Anchor({ required this.nickname, required this.roomName, required this.roomSrc}); factory Anchor.fromJson(Map<String, dynamic> json) { return Anchor( nickname: json["nickname"], roomName: json["roomName"], roomSrc: json["roomSrc"] ); } } Future<List<Anchor>> loadJsonData() async { // 1.读取json文件 String jsonString = await rootBundle.loadString("assets/json/mock.json"); // 类型检查 // final temp = json.decode(jsonString); // print('Type of temp: ${temp.runtimeType}'); // 2.转成List或Map类型 List<dynamic> jsonMap = json.decode(jsonString); // 3.遍历List,并且转成Anchor对象放到另一个List中 List<Anchor> anchors = []; for (Map<String, dynamic> map in jsonMap) { anchors.add(Anchor.fromJson(map)); } return anchors; }
五、ListView组件
移动端数据量比较大时,我们都是通过列表来进行展示的,比如商品数据、聊天列表、通信录、朋友圈等。
在Android中,我们可以使用ListView或RecyclerView来实现,在iOS中,我们可以通过UITableView来实现。
在Flutter中,我们也有对应的列表Widget,就是ListView。
1. ListView基础
1.1 ListView基本使用
ListView可以沿一个方向(垂直或水平方向,默认是垂直方向)来排列其所有子Widget。
一种最简单的使用方式是直接将所有需要排列的子Widget放在ListView的children属性中即可。
我们来看一下直接使用ListView的代码演练:
- 为了让文字之间有一些间距,我使用了Padding Widget
dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'dart:convert';
import 'dart:async';
import 'dart:core';
main(List<String> args) {
runApp(
MaterialApp(
showSemanticsDebugger: false,
home: HomePage(),
)
);
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("我的导航栏"),
),
body: HomePageBody()
);
}
}
class HomePageBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: HomePageListView()
);
}
}
class HomePageListView extends StatelessWidget {
final TextStyle textStyle = TextStyle(fontSize: 20, color: Colors.redAccent);
@override
Widget build(BuildContext context) {
return ListView(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: Text("人的一切痛苦,本质上都是对自己无能的愤怒。", style: textStyle),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text("人活在世界上,不可以有偏差;而且多少要费点劲儿,才能把自己保持到理性的轨道上。", style: textStyle),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text("我活在世上,无非想要明白些道理,遇见些有趣的事。", style: textStyle),
)
],
);
}
}
1.2 ListTile的使用
在开发中,我们经常见到一种列表,有一个图标或图片(Icon),有一个标题(Title),有一个子标题(Subtitle),还有尾部一个图标(Icon)。
这个时候,我们可以使用ListTile来实现:
dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'dart:convert';
import 'dart:async';
import 'dart:core';
main(List<String> args) {
runApp(
MaterialApp(
showSemanticsDebugger: false,
home: HomePage(),
)
);
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("我的导航栏"),
backgroundColor: Colors.green,
),
body: HomePageBody()
);
}
}
class HomePageBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: HomePageListView()
);
}
}
class HomePageListView extends StatelessWidget {
final TextStyle textStyle = TextStyle(fontSize: 20, color: Colors.redAccent);
@override
Widget build(BuildContext context) {
return ListView(
children: <Widget>[
ListTile(
leading: Icon(Icons.people, size: 36,),
title: Text("联系人"),
subtitle: Text("联系人信息"),
trailing: Icon(Icons.arrow_forward_ios),
),
ListTile(
leading: Icon(Icons.email, size: 36,),
title: Text("邮箱"),
subtitle: Text("邮箱地址信息"),
trailing: Icon(Icons.arrow_forward_ios),
),
ListTile(
leading: Icon(Icons.message, size: 36,),
title: Text("消息"),
subtitle: Text("消息详情信息"),
trailing: Icon(Icons.arrow_forward_ios),
),
ListTile(
leading: Icon(Icons.map, size: 36,),
title: Text("地址"),
subtitle: Text("地址详情信息"),
trailing: Icon(Icons.arrow_forward_ios),
)
],
);
}
}
1.3 垂直方向滚动
我们可以通过设置 scrollDirection
参数来控制视图的滚动方向。
我们通过下面的代码实现一个水平滚动的内容:
- 这里需要注意,我们需要给
Container
设置width,否则它是没有宽度的,就不能正常显示。 - 或者我们也可以给ListView设置一个
itemExtent
,该属性会设置滚动方向上每个item所占据的宽度。
dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'dart:convert';
import 'dart:async';
import 'dart:core';
main(List<String> args) {
runApp(
MaterialApp(
showSemanticsDebugger: false,
home: HomePage(),
)
);
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("我的导航栏"),
backgroundColor: Colors.green,
),
body: HomePageBody()
);
}
}
class HomePageBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: HomePageListView()
);
}
}
class HomePageListView extends StatelessWidget {
final TextStyle textStyle = TextStyle(fontSize: 20, color: Colors.redAccent);
@override
Widget build(BuildContext context) {
return ListView(
scrollDirection: Axis.horizontal,//设置滚动方向
itemExtent: 300,/*
在Flutter中,itemExtent是在某些可滚动的小部件中使用的一个属性,
比如ListView和GridView。它代表每个列表或网格中单个项的范围(高度或宽度,取决于滚动方向)。
例如,在垂直方向的ListView中,itemExtent属性指定了每个项的高度,确保所有项都具有固定的高度。
同样,在水平方向的ListView中,itemExtent将指定每个项的宽度。
使用itemExtent的好处是,当列表或网格中的项具有固定的尺寸时,它可以通过避免动态测量每个项的大小来提高性能。这在处理包含许多项的大型列表或网格时特别有用。
*/
children: <Widget>[
Container(color: Colors.red, width: 200),
Container(color: Colors.green, width: 200),
Container(color: Colors.blue, width: 200),
Container(color: Colors.purple, width: 200),
Container(color: Colors.orange, width: 200),
],
);
}
}
2. ListView.build
通过构造函数中的children传入所有的子Widget有一个问题:默认会创建出所有的子Widget。
但是对于用户来说,一次性构建出所有的Widget并不会有什么差异,但是对于我们的程序来说会产生性能问题,而且会增加首屏的渲染时间。
我们可以ListView.build来构建子Widget,提供性能。
2.1. ListView.build基本使用
ListView.build适用于子Widget比较多的场景(类似于iOS中的UITableView)
- 该构造函数将创建子Widget交给了一个抽象的方法,交给ListView进行管理
- ListView会在真正需要的时候去创建子Widget,而不是一开始就全部初始化好。
该方法有两个重要参数:
itemBuilder
:列表项创建的方法。当列表滚动到对应位置的时候,ListView会自动调用该方法来创建对应的子Widget。类型是IndexedWidgetBuilder,是一个函数类型。itemCount
:表示列表项的数量,如果为空,则表示ListView为无限列表。
我们还是通过一个简单的案例来认识它:
dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'dart:convert';
import 'dart:async';
import 'dart:core';
main(List<String> args) {
runApp(
MaterialApp(
showSemanticsDebugger: false,
home: HomePage(),
)
);
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("我的导航栏"),
backgroundColor: Colors.green,
),
body: HomePageBody()
);
}
}
class HomePageBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: HomePageListView()
);
}
}
class HomePageListView extends StatelessWidget {
final TextStyle textStyle = TextStyle(fontSize: 20, color: Colors.redAccent);
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: 100,
itemExtent: 80,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("标题$index"), subtitle: Text("详情内容$index"));
}
);
}
}
2.2. ListView.build动态数据
在之前,我们搞了一个yz.json数据,我们现在动态的来通过JSON数据展示一个列表。
思考:这个时候是否依然可以使用StatelessWidget
:
答案:不可以,因为当前我们的数据是异步加载的,刚开始界面并不会展示数据(没有数据),后面从JSON中加载出来数据(有数据)后,再次展示加载的数据。
- 这里是有状态的变化的,从无数据,到有数据的变化。
- 这个时候,我们需要使用
StatefulWidget
来管理组件。
展示代码如下:
dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'dart:convert';
import 'dart:async';
import 'dart:core';
main(List<String> args) {
runApp(
MaterialApp(
showSemanticsDebugger: false,
home: HomePage(),
)
);
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("我的导航栏"),
backgroundColor: Colors.green,
),
body: HomePageBody()
);
}
}
class HomePageBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: HomePageListView()
);
}
}
class HomePageListView extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return HomePageListViewState();
}
}
class HomePageListViewState extends State<HomePageListView> {
List<Anchor> anchors = [];
// 在初始化状态的方法中加载数据
@override
void initState() {
getAnchors().then((anchors) {
setState(() {
this.anchors = anchors;
});
});
super.initState();
}
@override
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder: (BuildContext context, int index) {
print("index:($index)");
return Padding(
padding: EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Image.network(
anchors[index%3].imageUrl,
fit: BoxFit.fitWidth,
width: MediaQuery.of(context).size.width,
),
SizedBox(height: 8),
Text(anchors[index%3].nickname, style: TextStyle(fontSize: 20),),
SizedBox(height: 5),
Text(anchors[index%3].roomName)
],
),
);
},
);
}
}
class Anchor {
String nickname = "";
String roomName = "";
String imageUrl = "";
Anchor({ required this.nickname, required this.roomName, required this.imageUrl});
factory Anchor.fromJson(Map<String, dynamic> json) {
return Anchor(
nickname: json["nickname"],
roomName: json["roomName"],
imageUrl: json["roomSrc"]
);
}
}
Future<List<Anchor>> getAnchors() async {
// 1.读取json文件
String jsonString = await rootBundle.loadString("assets/json/mock.json");
// 类型检查
// final temp = json.decode(jsonString);
// print('Type of temp: ${temp.runtimeType}');
// 2.转成List或Map类型
List<dynamic> jsonMap = json.decode(jsonString);
// 3.遍历List,并且转成Anchor对象放到另一个List中
List<Anchor> anchors = [];
for (Map<String, dynamic> map in jsonMap) {
anchors.add(Anchor.fromJson(map));
}
return anchors;
}
2.3. ListView.separated
ListView.separated
可以生成列表项之间的分割器
- 它比
ListView.builder
多了一个separatorBuilder
参数 - 该参数是一个分割器生成器。
下面我们看一个例子:奇数行添加一条蓝色下划线,偶数行添加一条红色下划线:
dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'dart:core';
main(List<String> args) {
runApp(
MaterialApp(
showSemanticsDebugger: false,
home: HomePage(),
)
);
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("我的导航栏"),
backgroundColor: Colors.green,
),
body: HomePageBody()
);
}
}
class HomePageBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: HomePageListView()
);
}
}
class HomePageListView extends StatelessWidget {
Divider blueColor = Divider(color: Colors.blue);
Divider redColor = Divider(color: Colors.red);
@override
Widget build(BuildContext context) {
return ListView.separated(
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: Icon(Icons.people),
title: Text("联系人${index+1}"),
subtitle: Text("联系人电话${index+1}"),
);
},
separatorBuilder: (BuildContext context, int index) {
return index % 2 == 0 ? redColor : blueColor;
},
itemCount: 100
);
}
}
六、GridView组件
GridView(类似于iOS的UICollectionView)用于展示多列的展示
- 在开发中也非常常见,比如直播App中的主播列表、电商中的商品列表等等。
- 在Flutter中我们可以使用GridView来实现,使用方式和ListView也比较相似。
1. GridView构造函数
我们先学习GridView构造函数的使用方法
一种使用GridView的方式就是使用构造函数来创建,和ListView对比有一个特殊的参数:gridDelegate
gridDelegate
用于控制交叉轴的item数量或者宽度,需要传入的类型是SliverGridDelegate,但是它是一个抽象类,所以我们需要传入它的子类:
SliverGridDelegateWithFixedCrossAxisCount
dart
SliverGridDelegateWithFixedCrossAxisCount({
@required double crossAxisCount, // 交叉轴的item个数
double mainAxisSpacing = 0.0, // 主轴的间距
double crossAxisSpacing = 0.0, // 交叉轴的间距
double childAspectRatio = 1.0, // 子Widget的宽高比
})
代码演练:
dart
SliverGridDelegateWithMaxCrossAxisExtent
dart
SliverGridDelegateWithMaxCrossAxisExtent({
double maxCrossAxisExtent, // 交叉轴的item宽度
double mainAxisSpacing = 0.0, // 主轴的间距
double crossAxisSpacing = 0.0, // 交叉轴的间距
double childAspectRatio = 1.0, // 子Widget的宽高比
})
代码演练:
dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'dart:core';
main(List<String> args) {
runApp(
MaterialApp(
showSemanticsDebugger: false,
home: HomePage(),
)
);
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("我的导航栏"),
backgroundColor: Colors.green,
),
body: HomePageBody()
);
}
}
class HomePageBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: HomePageGridView()
);
}
}
class HomePageGridView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GridView(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 1.0
),
children: getGridWidgets(),
);
}
List<Widget> getGridWidgets() {
return List.generate(100, (index) {
return Container(
color: Colors.purple,
alignment: Alignment(0, 0),
child: Text("item$index", style: TextStyle(fontSize: 20, color: Colors.white)),
);
});
}
}
前面两种方式也可以不设置delegate
可以分别使用:GridView.count构造函数
和GridView.extent
构造函数实现相同的效果,这里不再赘述。
2. GridView.build
和ListView一样,使用构造函数会一次性创建所有的子Widget,会带来性能问题,所以我们可以使用GridView.build
来交给GridView自己管理需要创建的子Widget。
我们直接使用之前的数据来进行代码演练:
dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'dart:convert';
import 'dart:async';
import 'dart:core';
main(List<String> args) {
runApp(
MaterialApp(
showSemanticsDebugger: false,
home: HomePage(),
)
);
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("我的导航栏"),
backgroundColor: Colors.green,
),
body: HomePageBody()
);
}
}
class HomePageBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return HomePageGridView();
}
}
class HomePageGridView extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return HomePageGridViewState();
}
}
class HomePageGridViewState extends State<HomePageGridView> {
List<Anchor> anchors = [];
@override
void initState() {
getAnchors().then((anchors) {
setState(() {
this.anchors = anchors;
});
});
super.initState();
}
@override
Widget build(BuildContext context) {
double screenWidth = MediaQuery.of(context).size.width;
double itemWidth = screenWidth/2.0-15;
double itemHeight= itemWidth + 100;
return Padding(
padding: const EdgeInsets.all(8.0),
child: GridView.builder(
shrinkWrap: true,
physics: ClampingScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 1.2
),
itemCount: anchors.length,
itemBuilder: (BuildContext context, int index) {
return Container(
// width: itemWidth,
// height: 500,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
ClipRect(
child:Container(
width: itemWidth,
height: 100,
child: Image(
image: NetworkImage(anchors[index].imageUrl),
fit: BoxFit.fitWidth,
),
),
),
SizedBox(height: 5),
Text(anchors[index].nickname, style: TextStyle(fontSize: 16),),
Text(anchors[index].roomName, maxLines: 1, overflow: TextOverflow.ellipsis,)
],
),
);
}
),
);
}
}
class Anchor {
String nickname = "";
String roomName = "";
String imageUrl = "";
Anchor({ required this.nickname, required this.roomName, required this.imageUrl});
factory Anchor.fromJson(Map<String, dynamic> json) {
return Anchor(
nickname: json["nickname"],
roomName: json["roomName"],
imageUrl: json["roomSrc"]
);
}
}
Future<List<Anchor>> getAnchors() async {
// 1.读取json文件
String jsonString = await rootBundle.loadString("assets/json/mock.json");
// 类型检查
// final temp = json.decode(jsonString);
// print('Type of temp: ${temp.runtimeType}');
// 2.转成List或Map类型
List<dynamic> jsonMap = json.decode(jsonString);
// 3.遍历List,并且转成Anchor对象放到另一个List中
List<Anchor> anchors = [];
for (Map<String, dynamic> map in jsonMap) {
anchors.add(Anchor.fromJson(map));
}
return anchors;
}
七、Slivers
我们考虑一个这样的布局:
- 一个滑动的视图中包括一个标题视图(HeaderView),
- 一个列表视图(ListView) 一个网格视图(GridView)。
我们怎么可以让它们做到统一的滑动效果呢?使用前面的滚动是很难做到的。
- Flutter中有一个可以完成这样滚动效果的Widget:
CustomScrollView
,可以统一管理多个滚动视图。 - 在
CustomScrollView
中,每一个独立的,可滚动的Widget被称之为Sliver。 - 补充:Sliver可以翻译成裂片、薄片,你可以将每一个独立的滚动视图当做一个小裂片。
1. Slivers的基本使用
因为我们需要把很多的Sliver放在一个CustomScrollView中,所以CustomScrollView有一个slivers属性,里面让我们放对应的一些Sliver:
- SliverList:类似于我们之前使用过的ListView;
- SliverFixedExtentList:类似于SliverList,只是可以设置滚动的高度;
- SliverGrid:类似于我们之前使用过的GridView;
- SliverPadding:设置Sliver的内边距,因为可能要单独给Sliver设置内边距;
- SliverAppBar:添加一个AppBar,通常用来作为CustomScrollView的HeaderView;
- SliverSafeArea:设置内容显示在安全区域(比如不让齐刘海挡住我们的内容)
我们简单演示一下:SliverGrid+SliverPadding+SliverSafeArea
的组合
dart
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: <Widget>[
SliverSafeArea(
sliver: SliverPadding(
padding: EdgeInsets.all(8),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
alignment: Alignment(0, 0),
color: Colors.orange,
child: Text("item$index"),
);
},
childCount: 20
),
),
),
)
],
);
}
}
2. Slivers的组合使用
这里我使用官方的示例程序,将SliverAppBar+SliverGrid+SliverFixedExtentList
做出如下界面:
dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'dart:convert';
import 'dart:async';
import 'dart:core';
main(List<String> args) {
runApp(
MaterialApp(
showSemanticsDebugger: false,
home: HomePage(),
)
);
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: HomePageBody()
);
}
}
class HomePageBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return HomePageCustomScrollView();
}
}
class HomePageCustomScrollView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return showCustomScrollView();
}
Widget showCustomScrollView() {
return CustomScrollView(
slivers: <Widget>[
const SliverAppBar(
expandedHeight: 250.0,
flexibleSpace: FlexibleSpaceBar(
title: Text('VanZhang Demo'),
background: Image(
image: NetworkImage(
"https://tva1.sinaimg.cn/large/006y8mN6gy1g72j6nk1d4j30u00k0n0j.jpg",
),
fit: BoxFit.cover,
),
),
),
SliverGrid(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200.0,
mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0,
childAspectRatio: 4.0,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
alignment: Alignment.center,
color: Colors.teal[100 * (index % 9)],
child: Text('grid item $index'),
);
},
childCount: 10,
),
),
SliverFixedExtentList(
itemExtent: 50.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
alignment: Alignment.center,
color: Colors.lightBlue[100 * (index % 9)],
child: Text('list item $index'),
);
},
childCount: 20
),
),
],
);
}
}
八、监听滚动事件
- 对于滚动的视图,我们经常需要监听它的一些滚动事件,在监听到的时候去做对应的一些事情。
- 比如视图滚动到底部时,我们可能希望做上拉加载更多;
- 比如滚动到一定位置时显示一个回到顶部的按钮,点击回到顶部的按钮,回到顶部;
- 比如监听滚动什么时候开始,什么时候结束;
- 在Flutter中监听滚动相关的内容由两部分组成:
ScrollController
和ScrollNotification
1. ScrollController
在Flutter中,Widget并不是最终渲染到屏幕上的元素(真正渲染的是RenderObject),因此通常这种监听事件以及相关的信息并不能直接从Widget中获取,而是必须通过对应的Widget的Controller来实现。
ListView、GridView的组件控制器是ScrollController,我们可以通过它来获取视图的滚动信息,并且可以调用里面的方法来更新视图的滚动位置。
另外,通常情况下,我们会根据滚动的位置来改变一些Widget的状态信息,所以ScrollController通常会和StatefulWidget一起来使用,并且会在其中控制它的初始化、监听、销毁等事件。
我们来做一个案例,当滚动到1000位置的时候,显示一个回到顶部的按钮:
jumpTo(double offset)
、animateTo(double offset,...)
:- 这两个方法用于跳转到指定的位置,它们不同之处在于,后者在跳转时会执行一个动画,而前者不会。
- ScrollController 间接继承自Listenable,我们可以根据ScrollController来监听滚动事件。
dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'dart:core';
main(List<String> args) {
runApp(
MaterialApp(
showSemanticsDebugger: false,
home: HomePage(),
)
);
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: HomePageBody()
);
}
}
class HomePageBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return HomePageScrollController();
}
}
class HomePageScrollController extends StatefulWidget {
@override
State<StatefulWidget> createState() => HomePageScrollControllerState();
}
class HomePageScrollControllerState extends State<HomePageScrollController> {
// 初始化ScrollController
ScrollController _controller = ScrollController();
bool _isShowTop = false;
@override
void initState() {
// 监听滚动
_controller.addListener(() {
var tempSsShowTop = _controller.offset >= 1000;
if (tempSsShowTop != _isShowTop) {
setState(() {
_isShowTop = tempSsShowTop;
});
}
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("ListView展示"),
),
body: ListView.builder(
itemCount: 100,
itemExtent: 60,
controller: _controller,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("item$index"));
}
),
floatingActionButton: !_isShowTop ? null : FloatingActionButton(
child: Icon(Icons.arrow_upward),
onPressed: () {
_controller.animateTo(0, duration: Duration(milliseconds: 1000), curve: Curves.ease);
},
),
);
}
}
2. NotificationListener
如果我们希望监听什么时候开始滚动,什么时候结束滚动,这个时候我们可以通过NotificationListener
。
- NotificationListener是一个Widget,模板参数T是想监听的通知类型,如果省略,则所有类型通知都会被监听,如果指定特定类型,则只有该类型的通知会被监听。
- NotificationListener需要一个onNotification回调函数,用于实现监听处理逻辑。
- 该回调可以返回一个布尔值,代表是否阻止该事件继续向上冒泡,如果为
true
时,则冒泡终止,事件停止向上传播,如果不返回或者返回值为false
时,则冒泡继续。
案例: 列表滚动, 并且在中间显示滚动进度
dart
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'dart:core';
main(List<String> args) {
runApp(
MaterialApp(
showSemanticsDebugger: false,
home: HomePage(),
)
);
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: HomePageBody()
);
}
}
class HomePageBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
return HomePageNotificationListener();
}
}
class HomePageNotificationListener extends StatefulWidget {
@override
State<StatefulWidget> createState() => HomePageNotificationListenerState();
}
class HomePageNotificationListenerState extends State<HomePageNotificationListener> {
int _progress = 0;
@override
Widget build(BuildContext context) {
return NotificationListener(
onNotification: (ScrollNotification notification) {
// 1.判断监听事件的类型
if (notification is ScrollStartNotification) {
print("开始滚动.....");
} else if (notification is ScrollUpdateNotification) {
// 当前滚动的位置和总长度
final currentPixel = notification.metrics.pixels;
final totalPixel = notification.metrics.maxScrollExtent;
double progress = currentPixel / totalPixel;
setState(() {
_progress = (progress * 100).toInt();
});
print("正在滚动:${notification.metrics.pixels} - ${notification.metrics.maxScrollExtent}");
} else if (notification is ScrollEndNotification) {
print("结束滚动....");
}
return false;
},
child: Stack(
alignment: Alignment(.9, .9),
children: <Widget>[
ListView.builder(
itemCount: 100,
itemExtent: 60,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("item$index"));
}
),
CircleAvatar(
radius: 30,
child: Text("$_progress%"),
backgroundColor: Colors.black54,
)
],
),
);
}
}