1.ListView
- 类似于iOS的UITableview;
属性(ListView) | 类型 | 可选? | 作用 |
---|---|---|---|
children | List<Widget> | 命名可选 | 列表内显示的子组件 |
scrollDirection | Axis(enum) | 命名可选(默认:Axis.vertical) | 滚动方向 |
itemExtent | double | 命名可选 | 行高(如果设置横向滚动,则表示列宽) |
属性(ListTile),类似于tableviewcell | 类型 | 可选? | 作用 |
leading | Widget | 命名可选 | 左侧图标(不一定是图标,只要是Widget) |
title | Widget | 命名可选 | 主标题 |
subtitle | Widget | 命名可选 | 副标题 |
trailing | Widget | 命名可选 | 右侧图标(不一定是图标,只要是Widget) |
onTap | 闭包 | 命名可选 | 点击回调 |
1.1 ListView基本使用
ListView可以沿一个方向(垂直或水平方向,默认是垂直方向)来排列其所有子Widget。
一种最简单的使用方式是直接将所有需要排列的子Widget放在ListView的children属性中即可。
我们来看一下直接使用ListView的代码演练:
class MyHomeBody extends StatelessWidget {
const MyHomeBody({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ListView(
children: List.generate(39, (index) {
//ListTile类似于tableviewcell
return ListTile(
leading: Icon(Icons.pets),
trailing: IconButton(icon: Icon(Icons.favorite,color: Colors.red,),),
title: Text('第 ${index+1} 行'),
subtitle: Text('副标题'),
onTap: () {
print('点击了tableview ${index+1}');
},
);
}),
);
}
}
1.2 ListView的滚动方向--scrollDirection
class MyHomeBody2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView(
scrollDirection: Axis.horizontal,
//children 在滚动方向的高度(垂直滚动)或者宽度(横向滚动)
itemExtent: 100,
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),
],
);
}
}
1.3 ListView的构造方法
构造方法 | 使用场景 |
---|---|
ListView();默认的构造方法 | 当已经有固定个数的chidren,且chidren的个数不是十分庞大的时候可以使用这个方法,因为这个方法默认会尽可能多地创建出子组件,以方便ListView进行展示 |
ListView.builder(); | 当有不确定数量,或者大量的cell显示的时候,可以使用这个方式,系统会自动在ListView将要显示某个cell的时候,才把它build出来; |
ListView.separated(); | 有3个@required参数: 前面两个和buider方法一样,第三个参数是separatorBuilder,用来添加一个cell与cell之间的分割线的,这个方法创建ListView不可以给cell设置高度 |
//使用builder构造器创建ListView
class MyHomeBody2 extends StatelessWidget {
@override
Widget build(BuildContext context) {
final double screenWidth = MediaQuery.of(context).size.width;
return ListView.builder(
itemBuilder: (BuildContext context, int index) {
return ListTile(
//左侧图标
leading: Icon(Icons.pets),
//右侧图标
trailing: IconButton(
icon: Icon(
Icons.favorite,
color: Colors.red,
),
),
//主标题
title: Text('第 ${index + 1} 行'),
//副标题
subtitle: Text('副标题'),
onTap: () {
print('点击了tableview ${index + 1}');
});
},
itemCount: 10,
scrollDirection: Axis.vertical,
);
}
}
//separated
class MyHomeBody3 extends StatelessWidget {
@override
Widget build(BuildContext context) {
final double screenWidth = MediaQuery.of(context).size.width;
return ListView.separated(
itemBuilder: (BuildContext context, int index) {
return ListTile(
//左侧图标
leading: Icon(Icons.pets),
//右侧图标
trailing: IconButton(
icon: Icon(
Icons.favorite,
color: Colors.red,
),
),
//主标题
title: Text('第 ${index + 1} 行'),
//副标题
subtitle: Text('副标题'),
onTap: () {
print('点击了tableview ${index + 1}');
});
},
separatorBuilder: (BuildContext context, int index){
return Divider(
color: Colors.red,
//Divider的高度
height: 1,
//左侧边距
indent: 10,
//右侧边距
endIndent: 30,
//分割线的高度
thickness: 1,
);
},
itemCount: 100);
}
}
2. GridView
构造方法 | 意义 |
---|---|
GridView() | 默认构造方法中有个感觉参数:gridDelegate,用来指定构造的方式的 |
GridView.builder() | 作用类似于ListView的builder();方法 |
GridView.count() | 在默认构造方法的基础上选择了SliverGridDelegateWithFixedCrossAxisCount作为delegate |
GridView.extent() | 这个构造方法是在默认构造器方法的基础上,指定了代理模式为: SliverGridDelegateWithMaxCrossAxisExtent |
2.1 Demo
//GridView_builder_Demo
class GridView_builder_Demo extends StatelessWidget {
const GridView_builder_Demo({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final double width = MediaQuery.of(context).size.width;
return GridView.builder(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: width/3,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: 0.8,
),
itemBuilder: (BuildContext context, int index) {
return Container(
color:Color.fromARGB(100, Random().nextInt(256),
Random().nextInt(256), Random().nextInt(256)),
);
},
itemCount: 10,
);
}
}
//GridView 的默认构造器案例 代理2
class GridViewDemo_Constructor2 extends StatelessWidget {
const GridViewDemo_Constructor2({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final width = (MediaQuery.of(context).size.width)/2;
return Padding(
padding: EdgeInsets.only(left: 8, right: 8, top: 8, bottom: 8),
child: GridView(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: width,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: 0.8,
),
children: List.generate(30, (index) {
return Container(
color: Color.fromARGB(100, Random().nextInt(256),
Random().nextInt(256), Random().nextInt(256)),
width: 20,
height: 20,
);
}),
),
);
}
}
//GridView 的默认构造器案例 代理1
class GridViewDemo_Constructor1 extends StatelessWidget {
const GridViewDemo_Constructor1({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(left: 8, right: 8, top: 8, bottom: 8),
child: GridView(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
//指定交叉轴上,可以摆放多少个item
crossAxisCount: 3,
//指定item的 宽/高 比例
childAspectRatio: 1,
//设定交叉轴上 item 之间的间隔
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
children: List.generate(30, (index) {
return Container(
color: Color.fromARGB(100, Random().nextInt(256),
Random().nextInt(256), Random().nextInt(256)),
width: 20,
height: 20,
);
}),
),
);
}
}
3.Sliver
- ListView 继承于 BoxScrollView ,
- BoxScrollView 继承于 ScrollView ,且BoxScrollView是abstract(抽象类)
- ScrollView 继承于 StatelessWidget , ScrollView 是 abstract(抽象类)
直接继承于Widget的类才会
@override
(重写)Widget build(BuildContext context);
方法
看一下ScrollView源码中重写的build方法
@override
Widget build(BuildContext context) {
final List<Widget> slivers = buildSlivers(context);
//这个才是滚动视图内,真正可以滚动的内容
// 继续查源码,可以发现buildSlivers();方法在ScrollView内没有实现,需要它的子类去实现
// Build the list of widgets to place inside the viewport.Subclasses should override this method to build the slivers for the inside of the viewport.
// 翻译: 构建要放置在视窗内的小部件列表。子类应该重写这个方法来为视窗内部构建切片。
// 那么ScrollView 的子类有哪些呢?
//1.CustomScrollView 2.BoxScrollView(abstract)
// BoxScrollVeiw的子类 : 1. ListView 2.GridView
//后续实现部分省略,可自行查看Flutter源码
}
BoxScrollVeiw中重写的 buildSlivers();
@override
List<Widget> buildSlivers(BuildContext context) {
Widget sliver = buildChildLayout(context);
EdgeInsetsGeometry effectivePadding = padding;
if (padding == null) {
final MediaQueryData mediaQuery = MediaQuery.of(context, nullOk: true);
if (mediaQuery != null) {
// Automatically pad sliver with padding from MediaQuery.
final EdgeInsets mediaQueryHorizontalPadding =
mediaQuery.padding.copyWith(top: 0.0, bottom: 0.0);
final EdgeInsets mediaQueryVerticalPadding =
mediaQuery.padding.copyWith(left: 0.0, right: 0.0);
// Consume the main axis padding with SliverPadding.
effectivePadding = scrollDirection == Axis.vertical
? mediaQueryVerticalPadding
: mediaQueryHorizontalPadding;
// Leave behind the cross axis padding.
sliver = MediaQuery(
data: mediaQuery.copyWith(
padding: scrollDirection == Axis.vertical
? mediaQueryHorizontalPadding
: mediaQueryVerticalPadding,
),
child: sliver,
);
}
}
if (effectivePadding != null)
sliver = SliverPadding(padding: effectivePadding, sliver: sliver);
// 最后返回的是一个Widget数组,数组元素是sliver,
// sliver的初始化在上面:
/// Subclasses should override this method to build the layout model.
// 子类需要重写此方法
// Widget sliver = buildChildLayout(context);
// 所以BoxScrollVeiw的子类: 1. ListView 2.GridView 重写这个方法来实现滚动视图
return <Widget>[ sliver ];
}
3.1. Slivers的基本使用
因为我们需要把很多的Sliver放在一个CustomScrollView中,所以CustomScrollView有一个slivers属性,里面让我们放对应的一些Sliver:
- SliverList:类似于我们之前使用过的ListView;
- SliverFixedExtentList:类似于SliverList只是可以设置滚动的高度;
- SliverGrid:类似于我们之前使用过的GridView;
- SliverPadding:设置Sliver的内边距,因为可能要单独给Sliver设置内边距;
- SliverAppBar:添加一个AppBar,通常用来作为CustomScrollView的HeaderView;
- SliverSafeArea:设置内容显示在安全区域(比如不让齐刘海挡住我们的内容)
使用CustomScrollView演示一下:SliverGrid+SliverPadding+SliverSafeArea的组合
CustomScrollView的参数/属性 | 类型 | 可选? | 作用 | |
---|---|---|---|---|
scrollDirection | Axis (enum) | 命名可选 | 滑动方向 | |
controller | ScrollController | 命名可选 | 类似于管理器 | |
slivers | <Widget>[] | 命名可选 | 被滚动视图显示的item内容 | |
SliverGrid | ||||
delegate | SliverChildDelegate | @required | 用来构建单元格内容的 | |
gridDelegate | SliverGridDelegate | @required | 用来构造滚动视图样式的 |
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CustomScrollView(
//滚动方向
scrollDirection: Axis.vertical,
//item
slivers: [
//SliverSafeArea 用来设置 滚动视图在安全区域内展示,
SliverSafeArea(
//SliverPadding ,用来设置滚动视图中,单元格在横向和纵向的 paddding
sliver: SliverPadding(
padding: EdgeInsets.symmetric(horizontal: 8,vertical: 8),
sliver: SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
//每一行显示多少个
crossAxisCount: 3,
//item的 宽/高 比
childAspectRatio: 1,
//主轴上,单元格之间的讲个
mainAxisSpacing: 8,
//交叉轴上单元格的间隔
crossAxisSpacing: 8,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
color: Color.fromARGB(
Random().nextInt(256),
Random().nextInt(256),
Random().nextInt(256),
Random().nextInt(256)),
child: Text('item${index}',style: TextStyle(color: Colors.red,fontSize: 30),),
alignment: Alignment.center,
);
},
childCount: 30,
),
),
),
),
],
);
}
}
3.2. Slivers的组合使用
这里我使用官方的示例程序,将SliverAppBar+SliverGrid+SliverFixedExtentList做出如下界面:
class HomeContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return showCustomScrollView();
}
Widget showCustomScrollView() {
return new CustomScrollView(
slivers: <Widget>[
const SliverAppBar(
expandedHeight: 250.0,
flexibleSpace: FlexibleSpaceBar(
title: Text('Coderwhy Demo'),
background: Image(
image: NetworkImage(
"https://tva1.sinaimg.cn/large/006y8mN6gy1g72j6nk1d4j30u00k0n0j.jpg",
),
fit: BoxFit.cover,
),
),
),
new SliverGrid(
gridDelegate: new SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200.0,
mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0,
childAspectRatio: 4.0,
),
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
return new Container(
alignment: Alignment.center,
color: Colors.teal[100 * (index % 9)],
child: new Text('grid item $index'),
);
},
childCount: 10,
),
),
SliverFixedExtentList(
itemExtent: 50.0,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return new Container(
alignment: Alignment.center,
color: Colors.lightBlue[100 * (index % 9)],
child: new Text('list item $index'),
);
},
childCount: 20
),
),
],
);
}
}
4.滚动的监听
在滚动视图中实现滚动监听,有两种方案
- 方案1: 使用controller进行监听
- 方案2: 使用NotificationListener进行监听
方案1:demo
class RF_ContentPage extends StatefulWidget {
const RF_ContentPage({
Key key,
}) : super(key: key);
@override
_RF_ContentPageState createState() => _RF_ContentPageState();
}
class _RF_ContentPageState extends State<RF_ContentPage> {
//定义一个ScrollController 对象,对滚动视图进行管理的
final ScrollController rfc = ScrollController(initialScrollOffset: 100);
// bool值,用于判断是否需要显示floatbutton
bool isShowFb = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('监听滑动'),
),
body: ListView.builder(
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: Icon(Icons.person),
title: Text('第 ${index + 1}行'),
);
},
controller: rfc,
),
//floatbutton,根据isShowFB这个bool值进行判断显示
floatingActionButton: isShowFb ? FloatingActionButton(onPressed: (){
rfc.animateTo(0, duration: Duration(seconds: 1), curve: Curves.easeInQuad);
},
child: Icon(Icons.arrow_upward),
) : null,
);
}
// 重写initState方法,对滚动进行监听;
@override
void initState() {
// TODO: implement initState
super.initState();
//ListView的controller通过添加listener添加滚动监听
rfc.addListener(() {
print('当前偏移量:${rfc.offset}');
setState(() {
isShowFb = rfc.offset >= 100;
});
});
}
@override
void dispose() {
// TODO: implement dispose
super.dispose();
rfc.dispose();
}
}
方案2demo
class RF_ContentPage extends StatefulWidget {
const RF_ContentPage({
Key key,
}) : super(key: key);
@override
_RF_ContentPageState createState() => _RF_ContentPageState();
}
class _RF_ContentPageState extends State<RF_ContentPage> {
final ScrollController rfc = ScrollController(initialScrollOffset: 100);
bool isShowFb = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('监听滑动'),
),
body: NotificationListener(
onNotification: (ScrollNotification notified) {
// if(notified is ScrollStartNotification ){
// print('开始滚动');
// }else if(notified is ScrollEndNotification){
// print('结束滚动');
// }else if( notified is ScrollUpdateNotification){
// print('正在滚动');
// }
//滚动偏移量
print('滚动偏移量:${notified.metrics.pixels}');
return true;
},
child: ListView.builder(
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: Icon(Icons.person),
title: Text('第 ${index + 1}行'),
);
},
controller: rfc,
),
),
floatingActionButton: isShowFb
? FloatingActionButton(
onPressed: () {
rfc.animateTo(0,
duration: Duration(seconds: 1), curve: Curves.easeInQuad);
},
child: Icon(Icons.arrow_upward),
)
: null,
);
}
@override
void dispose() {
rfc.dispose();
// TODO: implement dispose
super.dispose();
}
}
总结:
controller:可以直接去的滚动的控制对象,对齐进行控制,但是不能监听滚动的开始,结束状态
NotificationListener: 可以获取各种滚动状态,但是不持有滚动的管理对象