内容简介:主轴和纵轴设置视图的滚动方向(默认垂直方向), 需要对应的设置其子
- Flutter和Dart系列文章 和 代码GitHub地址
- 在
Flutter中, 当内容超过显示视图时,如果没有特殊处理,Flutter则会提示Overflow错误 -
Flutter提供了多种可滚动(Scrollable Widget)用于显示列表和长布局 - 可滚动
Widget都直接或间接包含一个Scrollable, 下面是常用的几个可滚动的WidgetSingleChildScrollView ListView GridView CustomScrollView ScrollController
Scrollbar
-
Scrollbar是一个Material风格的滚动指示器(滚动条),如果要给可滚动widget添加滚动条,只需将Scrollbar作为可滚动widget的父widget即可 -
CupertinoScrollbar是iOS风格的滚动条,如果你使用的是Scrollbar,那么在iOS平台它会自动切换为CupertinoScrollbar -
Scrollbar和CupertinoScrollbar都是通过ScrollController来监听滚动事件来确定滚动条位置,关于ScrollController详细的内容我们将在后面专门一节介绍 - 下面是
Scrollbar和CupertinoScrollbar的构造函数, 都只有一个child属性, 用于接受一个可滚动的Widget
const Scrollbar({
Key key,
@required this.child,
})
const CupertinoScrollbar({
Key key,
@required this.child,
})
复制代码
主轴和纵轴
widget widget
SingleChildScrollView
SingleChildScrollView 类似于开发中常用的 ScrollView , 不再详细介绍了, 下面看一下具体使用介绍吧
const SingleChildScrollView({
Key key,
// 设置滚动的方向, 默认垂直方向
this.scrollDirection = Axis.vertical,
// 设置显示方式
this.reverse = false,
// 内边距
this.padding,
// 是否使用默认的controller
bool primary,
// 设置可滚动Widget如何响应用户操作
this.physics,
this.controller,
this.child,
})
复制代码
scrollDirection
设置视图的滚动方向(默认垂直方向), 需要对应的设置其子 Widget 是 Column 或者 Row , 否则会报 Overflow 错误
scrollDirection: Axis.vertical,
// 枚举值
enum Axis {
/// 水平滚动
horizontal,
/// 垂直滚动
vertical,
}
复制代码
reverse
- 是否按照阅读方向相反的方向滑动
- 设置水平滚动时
reverse: false reverse: true
- 其实此属性本质上是决定可滚动
widget的初始滚动位置是在头还是尾,取false时,初始滚动位置在头,反之则在尾
physics
- 此属性接受一个
ScrollPhysics对象,它决定可滚动Widget如何响应用户操作 - 比如用户滑动完抬起手指后,继续执行动画;或者滑动到边界时,如何显示。
- 默认情况下,
Flutter会根据具体平台分别使用不同的ScrollPhysics对象,应用不同的显示效果,如当滑动到边界时,继续拖动的话,在iOS上会出现弹性效果,而在Android上会出现微光效果。 - 如果你想在所有平台下使用同一种效果,可以显式指定,
Flutter SDK中包含了两个ScrollPhysics的子类可以直接使用:-
ClampingScrollPhysics:安卓下微光效果。 -
BouncingScrollPhysics:iOS下弹性效果。
-
controller
- 此属性接受一个
ScrollController对象 -
ScrollController的主要作用是控制滚动位置和监听滚动事件。 - 默认情况下,
widget中会有一个默认的PrimaryScrollController,如果子widget中的可滚动widget没有显式的指定controller并且primary属性值为true时(默认就为true),可滚动widget会使用这个默认的PrimaryScrollController - 这种机制带来的好处是父
widget可以控制子树中可滚动widget的滚动,例如,Scaffold使用这种机制在iOS中实现了"回到顶部"的手势
代码示例
class ScrollView extends StatelessWidget {
@override
Widget build(BuildContext context) {
String str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
return Scrollbar(
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
reverse: true,
padding: EdgeInsets.all(0.0),
physics: BouncingScrollPhysics(),
child: Center(
child: Column(
//动态创建一个List<Widget>
children: str.split("")
//每一个字母都用一个Text显示,字体为原来的两倍
.map((c) => Text(c, textScaleFactor: 2.0))
.toList(),
),
),
),
);
}
}
复制代码
ListView
-
ListView是最常用的可滚动widget,它可以沿一个方向线性排布所有子widget, 类似于ReactNative中的ListView -
ListView共有四种构造函数ListView() ListView.builder() ListView.separated() ListView custom()
ListView({
// 公共参数上面都介绍过了
Key key,
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
EdgeInsetsGeometry padding,
// 是否根据子widget的总长度来设置ListView的长度,默认值为false
bool shrinkWrap = false,
// cell高度
this.itemExtent,
// 子widget是否包裹在AutomaticKeepAlive中
bool addAutomaticKeepAlives = true,
// 子widget是否包裹在RepaintBoundary中
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
// 设置预加载的区域, moren 0.0
double cacheExtent,
//子widget列表
List<Widget> children = const <Widget>[],
// 子widget的个数
int semanticChildCount,
})
复制代码
属性介绍
shrinkWrap
- 表示是否根据子
widget的总长度来设置ListView的长度,默认值为false。 - 默认情况下,
ListView的会在滚动方向尽可能多的占用空间 - 当
ListView在一个无边界(滚动方向上)的容器中时,shrinkWrap必须为true
itemExtent
- 该参数如果不为
null,则会强制children的"长度"为itemExtent的值 - 这里的"长度"是指滚动方向上子
widget的长度,即如果滚动方向是垂直方向,则代表子widget的高度,如果滚动方向为水平方向,则代表子widget的长度 - 在
ListView中,指定itemExtent比让子widget自己决定自身长度会更高效,这是因为指定itemExtent后,滚动系统可以提前知道列表的长度,而不是总是动态去计算,尤其是在滚动位置频繁变化时
addAutomaticKeepAlives
- 表示是否将列表项包裹在
AutomaticKeepAlive中 - 在一个懒加载列表中,如果将列表项包裹在
AutomaticKeepAlive中,在该列表项滑出视口时该列表项不会被GC,它会使用KeepAliveNotification来保存其状态 - 如果列表项自己维护其
KeepAlive状态,那么此参数必须置为false
addRepaintBoundaries
- 性表示是否将列表项包裹在
RepaintBoundary中 - 当可滚动
widget滚动时,将列表项包裹在RepaintBoundary中可以避免列表项重绘,但是当列表项重绘的开销非常小(如一个颜色块,或者一个较短的文本)时,不添加RepaintBoundary反而会更高效 - 和
addAutomaticKeepAlive一样,如果列表项自己维护其KeepAlive状态,那么此参数必须置为false
使用示例
class ScrollView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView(
itemExtent: 60,
cacheExtent: 100,
addAutomaticKeepAlives: false,
children: renderCell(),
);
}
List<Widget> renderCell() {
String str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
return str.split("")
.map((item) => ListTile(
title: Text('字母--$item'),
subtitle: Text('这是字母列表'),
leading: Icon(Icons.wifi),
)).toList();
}
}
复制代码
ListTile
-
ListTile是Flutter给我们准备好的用于创建ListView的子widget - 提供非常常见的构造和定义方式,包括文字,icon,点击事件,一般是能够满足基本需求,但是就不能自己定义了
const ListTile({
Key key,
// 前置(左侧)图标, Widget类型
this.leading,
// 标题, Widget类型
this.title,
// 副标题, Widget类型
this.subtitle,
// 后置(右侧)图标, Widget类型
this.trailing,
// 是否三行显示, subtitle不为空时才能使用
this.isThreeLine = false,
// 设置为true后字体变小
this.dense,
// 内容的内边距
this.contentPadding,
// 是否可被点击
this.enabled = true,
// 点击事件
this.onTap,
// 长按操作事件
this.onLongPress,
// 是否是选中状态
this.selected = false,
})
// 使用示例
return ListTile(
title: Text('index--$index'),
subtitle: Text('我是一只小鸭子, 咿呀咿呀哟; 我是一只小鸭子, 咿呀咿呀哟; 我是一只小鸭子, 咿呀咿呀哟;'),
leading: Icon(Icons.wifi),
trailing: Icon(Icons.keyboard_arrow_right),
isThreeLine: true,
dense: false,
contentPadding: EdgeInsets.all(10),
enabled: index % 3 != 0,
onTap: () => print('index = $index'),
onLongPress: () => print('long-Index = $index'),
selected: index % 2 == 0,
);
复制代码
ListView.builder
-
ListView.builder适合列表项比较多(或者无限)的情况,因为只有当子Widget真正显示的时候才会被创建 - 适用于自定义子
Widget且所有子Widget的样式一样
ListView.builder({
Key key,
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
bool shrinkWrap = false,
EdgeInsetsGeometry padding,
this.itemExtent,
//
@required IndexedWidgetBuilder itemBuilder,
// 列表项的数量,如果为null,则为无限列表
int itemCount,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
double cacheExtent,
int semanticChildCount,
})
复制代码
itemCount
列表项的数量,如果为null,则为无限列表
itemBuilder
- 它是列表项的构建器,类型为
IndexedWidgetBuilder,返回值为一个widget - 当列表滚动到具体的
index位置时,会调用该构建器构建列表项
代码示例
class ListBuild extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: 30,
itemBuilder: (content, index) {
return ListTile(
title: Text('index--$index'),
subtitle: Text('数字列表'),
leading: Icon(Icons.wifi),
);
},
);
}
}
复制代码
ListView.separated
ListView.separated 可以生成列表项之间的分割器,它除了比 ListView.builder 多了一个 separatorBuilder 参数外, 其他参数都一样
ListView.separated({
Key key,
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
bool shrinkWrap = false,
EdgeInsetsGeometry padding,
@required IndexedWidgetBuilder itemBuilder,
// 一个分割生成器
@required IndexedWidgetBuilder separatorBuilder,
@required int itemCount,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
double cacheExtent,
})
复制代码
separatorBuilder
该参数是一个分割生成器, 同样是一个 IndexedWidgetBuilder 类型的参数
typedef IndexedWidgetBuilder = Widget Function(BuildContext context, int index); 复制代码
代码示例
奇数行添加一条红色下划线,偶数行添加一条蓝色下划线。
lass SeparatedList extends StatelessWidget {
//下划线widget预定义以供复用。
Widget lineView1 = Divider(color: Colors.red, height: 2, indent: 10,);
Widget lineView2 = Divider(color: Colors.blue, height: 5, indent: 30);
@override
Widget build(BuildContext context) {
// TODO: implement build
return ListView.separated(
itemCount: 30,
itemBuilder: (content, index) {
return ListTile(
title: Text('index--$index'),
subtitle: Text('数字列表'),
leading: Icon(Icons.wifi),
);
},
separatorBuilder: (context, index) {
return index % 2 == 0 ? lineView1 : lineView2;
},
);
}
}
复制代码
Divider
设置每一个子 WIdget 的分割线
const Divider({
Key key,
// 分割线所在的SizedBox的高度, 除内边距之外的距离上面的间距
this.height = 16.0,
// 分割线左侧间距
this.indent = 0.0,
// 分割线颜色
this.color
})
复制代码
ListView.custom
- 大家可能对前两种比较熟悉,分别是传入一个子元素列表或是传入一个根据索引创建子元素的函数。
- 其实前两种方式都是
custom方式的“快捷方式” -
ListView内部是靠这个childrenDelegate属性动态初始化子元素的 - 我们使用
builder和separated比较多,这个custom相对来说就比较少了
const ListView.custom({
Key key,
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
bool shrinkWrap = false,
EdgeInsetsGeometry padding,
this.itemExtent,
// 动态初始化子元素
@required this.childrenDelegate,
double cacheExtent,
int semanticChildCount,
})
复制代码
childrenDelegate
其实在 ListView 的前面几种构造函数中, 都默认设置了 childrenDelegate 这个属性, 更多可参考官方文档
// ListView
ListView({
// ...
}) : childrenDelegate = SliverChildListDelegate(
children,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
), super();
// ListView.builder
ListView.builder({
// ...
}) : childrenDelegate = SliverChildBuilderDelegate(
itemBuilder,
childCount: itemCount,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries,
addSemanticIndexes: addSemanticIndexes,
), super();
// ListView.separated
ListView.separated({
// ...
}) : childrenDelegate = SliverChildBuilderDelegate(
// ...
), super();
复制代码
- 上面代码中可见,这里自动帮我们创建了一个
SliverChildListDelegate的实例 - 而
SliverChildListDelegate是抽象类SliverChildDelegate的子类 -
SliverChildListDelegate中主要逻辑就是实现了SliverChildDelegate中定义的build方法
Widget build(BuildContext context, int index) {
assert(builder != null);
if (index < 0 || (childCount != null && index >= childCount))
return null;
Widget child;
try {
child = builder(context, index);
} catch (exception, stackTrace) {
child = _createErrorWidget(exception, stackTrace);
}
if (child == null)
return null;
if (addRepaintBoundaries)
child = RepaintBoundary.wrap(child, index);
if (addSemanticIndexes) {
final int semanticIndex = semanticIndexCallback(child, index);
if (semanticIndex != null)
child = IndexedSemantics(index: semanticIndex + semanticIndexOffset, child: child);
}
if (addAutomaticKeepAlives)
child = AutomaticKeepAlive(child: child);
return child;
}
复制代码
- 从上面代码的逻辑可以看出, 就是根据传入的索引返回
children列表中对应的元素 - 每当
ListView的底层实现需要加载一个元素时,就会把该元素的索引传递给SliverChildDelegate的build方法,由该方法返回具体的元素 - 另外在
SliverChildDelegate内部,除了定义了build方法外,还定义了 一个名为didFinishLayout的方法
void didFinishLayout() {
assert(debugAssertChildListLocked());
final int firstIndex = _childElements.firstKey() ?? 0;
final int lastIndex = _childElements.lastKey() ?? 0;
widget.delegate.didFinishLayout(firstIndex, lastIndex);
}
复制代码
- 每当
ListView完成一次layout之后都会调用该方法, 同时传入两个索引值 - 这两个值分别是此次
layout中第一个元素和最后一个元素在ListView所有子元素中的索引值, 也就是可视区域内的元素在子元素列表中的位置 - 然而不论是
SliverChildListDelegate还是SliverChildBuilderDelegate的代码中,都没有didFinishLayout的具体实现。所以我们需要编写一个它们的子类
class MySliverBuilderDelegate extends SliverChildBuilderDelegate {
MySliverBuilderDelegate(
Widget Function(BuildContext, int) builder, {
int childCount,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
}) : super(builder,
childCount: childCount,
addAutomaticKeepAlives: addAutomaticKeepAlives,
addRepaintBoundaries: addRepaintBoundaries);
@override
void didFinishLayout(int firstIndex, int lastIndex) {
print('firstIndex: $firstIndex, lastIndex: $lastIndex');
}
}
复制代码
然后我们创建一个 ListView.custom 的列表视图
class CustomList extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return ListView.custom(
childrenDelegate: MySliverBuilderDelegate(
(BuildContext context, int index) {
return ListTile(
title: Text('index--$index'),
subtitle: Text('数字列表'),
leading: Icon(Icons.wifi),
);
}, childCount: 30,
),
);
}
}
复制代码
GridView
GridView 可以构建二维网格列表, 系统给出了五中构造函数
GridView() GridView.count GridView.extent GridView.builder GridView.custom
// 默认构造函数
GridView({
Key key,
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
bool shrinkWrap = false,
EdgeInsetsGeometry padding,
@required this.gridDelegate,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
double cacheExtent,
List<Widget> children = const <Widget>[],
int semanticChildCount,
})
复制代码
- 可以看到, 除了
gridDelegate属性外, 其他属性和ListView的属性都一样, 含义也都相同 -
gridDelegate参数的类型是SliverGridDelegate,它的作用是控制GridView子widget如何排列 -
SliverGridDelegate是一个抽象类,定义了GridView排列相关接口,子类需要通过实现它们来实现具体的布局算法 -
Flutter中提供了两个SliverGridDelegate的子类SliverGridDelegateWithFixedCrossAxisCount和SliverGridDelegateWithMaxCrossAxisExtent, 下面我们分别介绍
SliverGridDelegateWithFixedCrossAxisCount
该子类实现了一个横轴为固定数量子元素的排列算法,其构造函数为:
const SliverGridDelegateWithFixedCrossAxisCount({
// 横轴子元素的数量,此属性值确定后子元素在横轴的长度就确定了,即ViewPort横轴长度/crossAxisCount。
@required this.crossAxisCount,
// 主轴方向的间距
this.mainAxisSpacing = 0.0,
// 侧轴方向子元素的间距
this.crossAxisSpacing = 0.0,
// 子元素在侧轴长度和主轴长度的比例, 由于crossAxisCount指定后子元素横轴长度就确定了,然后通过此参数值就可以确定子元素在主轴的长度
this.childAspectRatio = 1.0,
})
复制代码
从上面的个属性可以发现,子元素的大小是通过 crossAxisCount 和 childAspectRatio 两个参数共同决定的。注意,这里的子元素指的是子 widget 的最大显示空间,注意确保子 widget 的实际大小不要超出子元素的空间, 代码示例如下
class ScrollView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GridView(
padding: EdgeInsets.all(10),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 1,
mainAxisSpacing: 10,
crossAxisSpacing: 10
),
children: <Widget>[
Container(color: Colors.orange),
Container(color: Colors.blue),
Container(color: Colors.orange),
Container(color: Colors.yellow),
Container(color: Colors.pink)
],
);
}
}
复制代码
GridView.count
GridView.count 构造函数内部使用了 SliverGridDelegateWithFixedCrossAxisCount ,我们通过它可以快速的创建横轴固定数量子元素的 GridView
GridView.count({
Key key,
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
bool shrinkWrap = false,
EdgeInsetsGeometry padding,
@required int crossAxisCount,
double mainAxisSpacing = 0.0,
double crossAxisSpacing = 0.0,
double childAspectRatio = 1.0,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
double cacheExtent,
List<Widget> children = const <Widget>[],
int semanticChildCount,
})
复制代码
上面 SliverGridDelegateWithFixedCrossAxisCount 中给出的示例代码等价于:
class CountGridView extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return GridView.count(
padding: EdgeInsets.all(10),
crossAxisCount: 3,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 1,
children: <Widget>[
Container(color: Colors.orange),
Container(color: Colors.blue),
Container(color: Colors.orange),
Container(color: Colors.yellow),
Container(color: Colors.pink)
],
);
}
}
复制代码
SliverGridDelegateWithMaxCrossAxisExtent
该子类实现了一个侧轴子元素为固定最大长度的排列算法,其构造函数为:
const SliverGridDelegateWithMaxCrossAxisExtent({
@required this.maxCrossAxisExtent,
this.mainAxisSpacing = 0.0,
this.crossAxisSpacing = 0.0,
this.childAspectRatio = 1.0,
})
复制代码
maxCrossAxisExtent Widget SliverGridDelegateWithFixedCrossAxisCount
class ExtentScrollView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GridView(
padding: EdgeInsets.all(10),
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 100,
childAspectRatio: 1,
mainAxisSpacing: 10,
crossAxisSpacing: 10
),
children: <Widget>[
Container(color: Colors.orange),
Container(color: Colors.blue),
Container(color: Colors.orange),
Container(color: Colors.yellow),
Container(color: Colors.pink)
],
);
}
}
复制代码
GridView.extent
同样 GridView.extent 构造函数内部使用了 SliverGridDelegateWithMaxCrossAxisExtent ,我们通过它可以快速的创建侧轴子元素为固定最大长度的的 GridView
GridView.extent({
Key key,
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
bool shrinkWrap = false,
EdgeInsetsGeometry padding,
@required double maxCrossAxisExtent,
double mainAxisSpacing = 0.0,
double crossAxisSpacing = 0.0,
double childAspectRatio = 1.0,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
List<Widget> children = const <Widget>[],
int semanticChildCount,
})
复制代码
上面 SliverGridDelegateWithMaxCrossAxisExtent 中给出的示例代码等价于:
class ExtentScrollView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GridView.extent(
padding: EdgeInsets.all(10),
maxCrossAxisExtent: 100,
childAspectRatio: 1,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
children: <Widget>[
Container(color: Colors.orange),
Container(color: Colors.blue),
Container(color: Colors.orange),
Container(color: Colors.yellow),
Container(color: Colors.pink)
],
);
}
}
复制代码
GridView.builder
- 上面我们介绍的
GridView都需要一个Widget数组作为其子元素,这些方式都会提前将所有子widget都构建好,所以只适用于子Widget数量比较少时 - 当子
widget比较多时,我们可以通过GridView.builder来动态创建子Widget
GridView.builder({
Key key,
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
bool shrinkWrap = false,
EdgeInsetsGeometry padding,
@required this.gridDelegate,
@required IndexedWidgetBuilder itemBuilder,
int itemCount,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
double cacheExtent,
int semanticChildCount,
})
复制代码
- 可以看出
GridView.builder必须指定的参数有两个,其中gridDelegate之前已经介绍过了 - 属性
itemBuilder在之前ListView中也有介绍过类似的, 用于构建子Widget - 使用示例如下
class BuilderGridView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GridView.builder(
itemCount: 50,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
mainAxisSpacing: 10,
crossAxisSpacing: 10
),
itemBuilder: (content, index) {
return Container(
color: Colors.orange,
child: Center(
child: Text('$index'),
),
);
},
);
}
}
复制代码
GridView.custom
和 ListView.custom 一样, 用于构建自定义子 Widget , 有两个必须指定的参数, 这里就不在赘述了
const GridView.custom({
Key key,
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
bool shrinkWrap = false,
EdgeInsetsGeometry padding,
@required this.gridDelegate,
@required this.childrenDelegate,
double cacheExtent,
int semanticChildCount,
})
复制代码
CustomScrollView
-
CustomScrollView使用sliver来自定义滚动模型(效果, 它可以包含多种滚动模型 - 假设有一个页面,顶部需要一个
GridView,底部需要一个ListView,而要求整个页面的滑动效果是统一的,即它们看起来是一个整体 - 如果使用
GridView+ListView来实现的话,就不能保证一致的滑动效果,因为它们的滚动效果是分离的,所以这时就需要一个"胶水",把这些彼此独立的可滚动widget"粘"起来,而CustomScrollView的功能就相当于“胶水”
const CustomScrollView({
Key key,
// 滑动方向
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
bool shrinkWrap = false,
double cacheExtent,
this.slivers = const <Widget>[],
int semanticChildCount,
})
复制代码
上述属性除了 slivers 之外, 前面都有提到过, 接受一个 Widget 数组, 但是这里的 Widget 必须是 Sliver 类型的, 至于原因, 下面会详解
什么是`Sliver` ??
- 在
Flutter中,Sliver通常指具有特定滚动效果的可滚动块 - 可滚动
widget,如ListView、GridView等都有对应的Sliver实现如SliverList、SliverGrid等 - 对于大多数
Sliver来说,它们和可滚动Widget最主要的区别是Sliver不会包含Scrollable,也就是说Sliver本身不包含滚动交互模型 - 正因如此,
CustomScrollView才可以将多个Sliver"粘"在一起,这些Sliver共用CustomScrollView的Scrollable,最终实现统一的滑动效果 - 前面之所以说“大多数“
Sliver都和可滚动Widget对应,是由于还有一些如SliverPadding、SliverAppBar等是和可滚动Widget无关的 - 它们主要是为了结合
CustomScrollView一起使用,这是因为CustomScrollView的子widget必须都是Sliver
SliverAppBar
-
AppBar和SliverAppBar是Material Design中的导航栏 -
AppBar和SliverAppBar都是继承StatefulWidget类,二者的区别在于AppBar位置的固定的应用最上面的;而SliverAppBar是可以跟随内容滚动的 - 其中大部分的属性和
AppBar都一样
const SliverAppBar({
Key key,
// 导航栏左侧weidget
this.leading,
// 如果leading为null,是否自动实现默认的leading按钮
this.automaticallyImplyLeading = true,
// 导航栏标题
this.title,
// 导航栏右侧按钮, 接受一个数组
this.actions,
// 一个显示在AppBar下方的控件,高度和AppBar高度一样,可以实现一些特殊的效果,该属性通常在SliverAppBar中使用
this.flexibleSpace,
// 一个AppBarBottomWidget对象, 设置TabBar
this.bottom,
//中控件的z坐标顺序,默认值为4,对于可滚动的SliverAppBar,当 SliverAppBar和内容同级的时候,该值为0,当内容滚动 SliverAppBar 变为 Toolbar 的时候,修改elevation的值
this.elevation = 4.0,
// 背景颜色,默认值为 ThemeData.primaryColor。改值通常和下面的三个属性一起使用
this.backgroundColor,
// 状态栏的颜色, 黑白两种, 取值: Brightness.dark
this.brightness,
// 设置导航栏上图标的颜色、透明度、和尺寸信息
this.iconTheme,
// 设置导航栏上文字样式
this.textTheme,
// 导航栏的内容是否显示在顶部, 状态栏的下面
this.primary = true,
// 标题是否居中显示,默认值根据不同的操作系统,显示方式不一样
this.centerTitle,
// 标题间距,如果希望title占用所有可用空间,请将此值设置为0.0
this.titleSpacing = NavigationToolbar.kMiddleSpacing,
// 展开的最大高度
this.expandedHeight,
// 是否随着华东隐藏标题
this.floating = false,
// 是否固定在顶部
this.pinned = false,
// 只跟floating相对应,如果为true,floating必须为true,也就是向下滑动一点儿,整个大背景就会动画显示全部,网上滑动整个导航栏的内容就会消失
this.snap = false,
})
复制代码
使用示例
class CustomScrollViewTestRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
//因为本路由没有使用Scaffold,为了让子级Widget(如Text)使用
//Material Design 默认的样式风格,我们使用Material作为本路由的根。
return Material(
child: CustomScrollView(
slivers: <Widget>[
//AppBar,包含一个导航栏
SliverAppBar(
pinned: true,
expandedHeight: 250.0,
flexibleSpace: FlexibleSpaceBar(
title: const Text('Demo'),
background: Image.asset(
"./images/avatar.png", fit: BoxFit.cover,),
),
),
SliverPadding(
padding: const EdgeInsets.all(8.0),
sliver: new SliverGrid( //Grid
gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, //Grid按两列显示
mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0,
childAspectRatio: 4.0,
),
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
//创建子widget
return new Container(
alignment: Alignment.center,
color: Colors.cyan[100 * (index % 9)],
child: new Text('grid item $index'),
);
},
childCount: 20,
),
),
),
//List
new SliverFixedExtentList(
itemExtent: 50.0,
delegate: new SliverChildBuilderDelegate(
(BuildContext context, int index) {
//创建列表项
return new Container(
alignment: Alignment.center,
color: Colors.lightBlue[100 * (index % 9)],
child: new Text('list item $index'),
);
},
childCount: 50 //50个列表项
),
),
],
),
);
}
}
复制代码
ScrollController
-
ScrollController用于控制可滚动widget的滚动位置,这里以ListView为例,展示一下ScrollController的具体用法 - 最后再介绍一下路由切换时如何来保存滚动位置
- 下面先看一下
ScrollController的构造函数
ScrollController({
// 初始滚动位置
double initialScrollOffset = 0.0,
// 是否保存滚动位置
this.keepScrollOffset = true,
// 调试使用的输出标签
this.debugLabel,
})
复制代码
相关属性和方法
offset
可滚动 Widget 当前滚动的位置
jumpTo()
跳转到指定的位置, 没有动画效果
void jumpTo(double value) {
assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.');
for (ScrollPosition position in List<ScrollPosition>.from(_positions))
position.jumpTo(value);
}
复制代码
animateTo()
跳转到指定的位置, 跳转时会有一个动画效果
Future<void> animateTo(double offset, {
@required Duration duration,
@required Curve curve,
}) {
assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.');
final List<Future<void>> animations = List<Future<void>>(_positions.length);
for (int i = 0; i < _positions.length; i += 1)
animations[i] = _positions[i].animateTo(offset, duration: duration, curve: curve);
return Future.wait<void>(animations).then<void>((List<void> _) => null);
}
复制代码
positions
- 一个
ScrollController可以同时被多个Scrollable使用,ScrollController会为每一个Scrollable创建一个ScrollPosition对象,这些ScrollPosition保存在ScrollController的positions属性中(是一个数组) -
ScrollPosition是真正保存滑动位置信息的对象,offset只是一个便捷属性, 其他更多属性可查看相关官方文档 - 一个
ScrollController虽然可以对应多个Scrollable,但是有一些操作,如读取滚动位置offset,则需要一对一,但是我们仍然可以在一对多的情况下,通过其它方法读取滚动位置
// controller的offset属性 double get offset => position.pixels; // 读取相关的滚动位置 controller.positions.elementAt(0).pixels controller.positions.elementAt(1).pixels 复制代码
滚动监听
ScrollController 间接继承自 Listenable ,我们可以根据 ScrollController 来监听滚动事件。如:
controller.addListener(()=>print(controller.offset)) 复制代码
ScrollController控制原理
先看一下 ScrollController 另外几个方法的实现
// 创建一个存储位置信息的ScrollPosition
ScrollPosition createScrollPosition(
ScrollPhysics physics,
ScrollContext context,
ScrollPosition oldPosition,
) {
return ScrollPositionWithSingleContext(
physics: physics,
context: context,
initialPixels: initialScrollOffset,
keepScrollOffset: keepScrollOffset,
oldPosition: oldPosition,
debugLabel: debugLabel,
);
}
// 注册位置信息
void attach(ScrollPosition position) {
assert(!_positions.contains(position));
_positions.add(position);
position.addListener(notifyListeners);
}
// 注销位置信息
void detach(ScrollPosition position) {
assert(_positions.contains(position));
position.removeListener(notifyListeners);
_positions.remove(position);
}
// 销毁ScrollController
@override
void dispose() {
for (ScrollPosition position in _positions)
position.removeListener(notifyListeners);
super.dispose();
}
复制代码
- 当
ScrollController和Scrollable关联时,Scrollable首先会调用ScrollController的createScrollPosition()方法来创建一个ScrollPosition来存储滚动位置信息 - 然后
Scrollable会调用attach()方法,将创建的ScrollPosition添加到ScrollController的positions属性中,这一步称为“注册位置”,只有注册后animateTo()和jumpTo()才可以被调用 - 当
Scrollable销毁时,会调用ScrollController的detach()方法,将其ScrollPosition对象从ScrollController的positions属性中移除,这一步称为“注销位置”,注销后animateTo()和jumpTo()将不能再被调用 - 需要注意的是,
ScrollController的animateTo()和jumpTo()内部会调用所有ScrollPosition的animateTo()和jumpTo(),以实现所有和该ScrollController关联的Scrollable都滚动到指定的位置
代码示例
创建一个 ListView ,当滚动位置发生变化时,我们先打印出当前滚动位置,然后判断当前位置是否超过1000像素,如果超过则在屏幕右下角显示一个“返回顶部”的按钮,该按钮点击后可以使 ListView 恢复到初始位置;如果没有超过1000像素,则隐藏“返回顶部”按钮。代码如下
class ScrollControllerTestRoute extends StatefulWidget {
@override
ScrollControllerTestRouteState createState() {
return new ScrollControllerTestRouteState();
}
}
class ScrollControllerTestRouteState extends State<ScrollControllerTestRoute> {
ScrollController _controller = new ScrollController();
bool showToTopBtn = false; //是否显示“返回到顶部”按钮
@override
void initState() {
//监听滚动事件,打印滚动位置
_controller.addListener(() {
print(_controller.offset); //打印滚动位置
if (_controller.offset < 1000 && showToTopBtn) {
setState(() {
showToTopBtn = false;
});
} else if (_controller.offset >= 1000 && showToTopBtn == false) {
setState(() {
showToTopBtn = true;
});
}
});
}
@override
void dispose() {
//为了避免内存泄露,需要调用_controller.dispose
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("滚动控制")),
body: Scrollbar(
child: ListView.builder(
itemCount: 100,
itemExtent: 50.0, //列表项高度固定时,显式指定高度是一个好习惯(性能消耗小)
controller: _controller,
itemBuilder: (context, index) {
return ListTile(title: Text("$index"),);
}
),
),
floatingActionButton: !showToTopBtn ? null : FloatingActionButton(
child: Icon(Icons.arrow_upward),
onPressed: () {
//返回到顶部时执行动画
_controller.animateTo(.0,
duration: Duration(milliseconds: 200),
curve: Curves.ease
);
}
),
);
}
}
复制代码
以上所述就是小编给大家介绍的《Flutter之可滑动Widget》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Out of their Minds
Dennis Shasha、Cathy Lazere / Springer / 1998-07-02 / USD 16.00
This best-selling book is now available in an inexpensive softcover format. Imagine living during the Renaissance and being able to interview that eras greatest scientists about their inspirations, di......一起来看看 《Out of their Minds》 这本书的介绍吧!