Flutter
中, 当内容超过显示视图时,若是没有特殊处理,Flutter
则会提示Overflow
错误Flutter
提供了多种可滚动(Scrollable Widget
)用于显示列表和长布局Widget
都直接或间接包含一个Scrollable
, 下面是经常使用的几个可滚动的Widget
SingleChildScrollView
ListView
GridView
CustomScrollView
ScrollController
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,
})
复制代码
主轴和纵轴git
widget
的坐标描述中,一般将滚动方向称为主轴,非滚动方向称为纵轴。widget
的默认方向通常都是沿垂直方向,因此默认状况下主轴就是指垂直方向,水平方向同理SingleChildScrollView
相似于开发中经常使用的ScrollView
, 再也不详细介绍了, 下面看一下具体使用介绍吧github
const SingleChildScrollView({
Key key,
// 设置滚动的方向, 默认垂直方向
this.scrollDirection = Axis.vertical,
// 设置显示方式
this.reverse = false,
// 内边距
this.padding,
// 是否使用默认的controller
bool primary,
// 设置可滚动Widget如何响应用户操做
this.physics,
this.controller,
this.child,
})
复制代码
设置视图的滚动方向(默认垂直方向), 须要对应的设置其子Widget
是Column
或者Row
, 不然会报Overflow
错误算法
scrollDirection: Axis.vertical,
// 枚举值
enum Axis {
/// 水平滚动
horizontal,
/// 垂直滚动
vertical,
}
复制代码
reverse: false
,则滚动内容头部和左侧对其, 那么滑动方向就是从左向右reverse: true
时,则滚动内容尾部和右侧对其, 那么滑动方向就是从右往左。widget
的初始滚动位置是在头仍是尾,取false
时,初始滚动位置在头,反之则在尾ScrollPhysics
对象,它决定可滚动Widget
如何响应用户操做Flutter
会根据具体平台分别使用不一样的ScrollPhysics
对象,应用不一样的显示效果,如当滑动到边界时,继续拖动的话,在iOS
上会出现弹性效果,而在Android
上会出现微光效果。Flutter SDK
中包含了两个ScrollPhysics
的子类能够直接使用:
ClampingScrollPhysics
:安卓下微光效果。BouncingScrollPhysics
:iOS
下弹性效果。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
是最经常使用的可滚动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,
})
复制代码
widget
的总长度来设置ListView
的长度,默认值为false
。ListView
的会在滚动方向尽量多的占用空间ListView
在一个无边界(滚动方向上)的容器中时,shrinkWrap
必须为true
null
,则会强制children
的"长度"为itemExtent
的值widget
的长度,即若是滚动方向是垂直方向,则表明子widget
的高度,若是滚动方向为水平方向,则表明子widget
的长度ListView
中,指定itemExtent
比让子widget
本身决定自身长度会更高效,这是由于指定itemExtent
后,滚动系统能够提早知道列表的长度,而不是老是动态去计算,尤为是在滚动位置频繁变化时AutomaticKeepAlive
中AutomaticKeepAlive
中,在该列表项滑出视口时该列表项不会被GC,它会使用KeepAliveNotification
来保存其状态KeepAlive
状态,那么此参数必须置为false
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
是Flutter
给咱们准备好的用于建立ListView
的子widget
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
适合列表项比较多(或者无限)的状况,由于只有当子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,
})
复制代码
列表项的数量,若是为null,则为无限列表数组
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.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,
})
复制代码
该参数是一个分割生成器, 一样是一个IndexedWidgetBuilder
类型的参数markdown
typedef IndexedWidgetBuilder = Widget Function(BuildContext context, int index);
复制代码
奇数行添加一条红色下划线,偶数行添加一条蓝色下划线。app
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;
},
);
}
}
复制代码
设置每个子WIdget
的分割线less
const Divider({
Key key,
// 分割线所在的SizedBox的高度, 除内边距以外的距离上面的间距
this.height = 16.0,
// 分割线左侧间距
this.indent = 0.0,
// 分割线颜色
this.color
})
复制代码
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,
})
复制代码
其实在ListView
的前面几种构造函数中, 都默认设置了childrenDelegate
这个属性, 更多可参考官方文档ide
// 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.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
, 下面咱们分别介绍该子类实现了一个横轴为固定数量子元素的排列算法,其构造函数为:
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
构造函数内部使用了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)
],
);
}
}
复制代码
该子类实现了一个侧轴子元素为固定最大长度的排列算法,其构造函数为:
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
构造函数内部使用了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
都须要一个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'),
),
);
},
);
}
}
复制代码
和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
使用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
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
用于控制可滚动widget
的滚动位置,这里以ListView
为例,展现一下ScrollController
的具体用法ScrollController
的构造函数ScrollController({
// 初始滚动位置
double initialScrollOffset = 0.0,
// 是否保存滚动位置
this.keepScrollOffset = true,
// 调试使用的输出标签
this.debugLabel,
})
复制代码
可滚动Widget
当前滚动的位置
跳转到指定的位置, 没有动画效果
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);
}
复制代码
跳转到指定的位置, 跳转时会有一个动画效果
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);
}
复制代码
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
另外几个方法的实现
// 建立一个存储位置信息的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
);
}
),
);
}
}
复制代码