目录
1. ListView
2. SingleChildScrollView
3. GridView(二维网格列表)
Flutter官方并没有对Widget进行分类,对其分类主要是为了对Widget进行功能区分。
当组件超过显示窗口时,Flutter会提示Overflow错误。为此,Flutter提供了多种可滚动组件用于显示列表和长布局。
/*
Flutter有两种布局模型:
1. 基于RenderBox的盒模型布局。
2. 基于RenderSliver (Sliver) 按需加载列表布局。
*/
主纵轴
滚动方向称为主轴,非滚动方向称为纵轴。
可滚动组件的组成部分
1. Scrollable (继承自StatefulWidget)
处理滑动手势,确定滑动偏移,滑动偏移变化时构建Viewport 。
2. Viewport
渲染当前视口中需要显示的Sliver。
父组件为Scrollable。
3. Sliver
对子组件进行构建和布局。
父组件为Viewport。
具体过程
1. Scrollable监听到用户滑动行为后,根据最新的滑动偏移构建Viewport 。
2. Viewport将当前视口信息和配置信息通过SliverConstraints传递给Sliver。
3. Sliver中对子组件(RenderBox)按需进行构建和布局,然后确认自身的位置、绘制等信息,保存在geometry(SliverGeometry类型的对象)中。
基于Sliver的延迟构建模型
通常可滚动组件的子组件非常多、占用高度非常大,如果一次性将子组件全部构建将会非常昂贵。为此,Flutter提出Sliver(薄片)概念,如果一个可滚动组件支持Sliver模型,则可以分成许多Sliver,且只有当Sliver出现在视口中时才去构建它。
支持:ListView、GridView。不支持:SingleChildScrollView。
公共属性(最终会透传给Scrollable和Viewport)
1. scrollDirection
滚动方向。
Axis.vertica垂直方向(默认),Axis.horizontal水平方向。
2. reverse
是否按照阅读方向相反的方向滑动。
决定可滚动组件的初始滚动位置是在“头”还是“尾”,取false时初始滚动位置在“头”,反之则在“尾”。
3. primary
是否使用widget树中默认的PrimaryScrollController;
当滑动方向为垂直方向且没有指定controller时,primary默认为true。
4. padding
内边距
5. controller(ScrollController类型)
控制滚动位置和监听滚动事件。
当子树中的可滚动组件没有显式指定controller且primary属性值为true时(默认就为true),可滚动组件会使用Widget树中默认的PrimaryScrollController。这种机制的好处是父组件可以控制子树中可滚动组件的滚动行为,例如,Scaffold正是使用这种机制在iOS中实现了点击导航栏回到顶部的功能。
6. physics(ScrollPhysics类型)
决定可滚动组件如何响应用户操作。比如用户滑动完抬起手指后,继续执行动画;或者滑动到边界时,如何显示。NeverScrollableScrollPhysics():禁止滚动。
Flutter默认会根据各平台分别使用不同的ScrollPhysics对象,应用不同的显示效果,如当滑动到边界时,继续拖动的话,在iOS上会出现弹性效果,而在Android上会出现微光效果。如果想在所有平台下使用同一种效果,可以显式指定:
1. ClampingScrollPhysics:Android下微光效果。
2. BouncingScrollPhysics:iOS下弹性效果。
cacheExtent
预渲染的高度(下图中顶部和底部灰色的区域)。
如果RenderBox进入这个区域,即使它未显示在屏幕上,也要先进行构建,预渲染是为了后面进入Viewport时更流畅。
默认值是250,在构建可滚动列表时可以指定这个值(最终会传给 Viewport)。
ListView
- Scrollable、Viewport、Sliver
Scrollable({
this.axisDirection = AxisDirection.down, // 滚动方向
this.controller,
this.physics,
// 滑动时Scrollable会调用此回调构建新的Viewport,同时传递一个ViewportOffset类型的offset参数(描述Viewport该显示哪一部分)。
// 重新构建Viewport(本身也是Widget,只是配置信息)不是一个昂贵的操作,Viewport变化时对应的RenderViewport会更新信息,并不会随着Widget进行重新构建。
@required this.viewportBuilder,
})
Viewport({
Key? key,
this.axisDirection = AxisDirection.down,
this.crossAxisDirection,
this.anchor = 0.0,
// 滚动偏移。Scrollabel构建Viewport 时传入(描述了Viewport该显示哪一部分)。
required ViewportOffset offset,
// 类型为Key,表示从什么地方开始绘制,默认是第一个元素
this.center,
this.cacheExtent, // 预渲染区域
// pixel:cacheExtent值为预渲染区域的具体像素长度
// viewport:cacheExtent值是一个乘数,预渲染区域的像素长度=cacheExtent*viewport。
this.cacheExtentStyle = CacheExtentStyle.pixel,
this.clipBehavior = Clip.hardEdge,
List<Widget> slivers = const <Widget>[], // 需要显示的 Sliver 列表
})
Sliver对应的渲染对象类型是RenderSliver。
RenderSliver和RenderBox的相同点是都继承自RenderObject类,不同点是在布局时约束信息不同。RenderBox在布局时父组件传递给它的约束信息是BoxConstraints(只包含最大宽高的约束);而 RenderSliver在布局时父组件传递给它的约束是SliverConstraints。
- Scrollbar (Material风格的滚动条)
使用:作为可滚动组件的任意一个父组件。
Scrollbar(
child: SingleChildScrollView(
...
),
);
Scrollbar在iOS平台会自动切换为CupertinoScrollbar(iOS风格)。
Scrollbar和CupertinoScrollbar都是通过监听滚动通知来确定滚动条位置的。
- ScrollController(间接继承自Listenable)
可滚动组件都有一个controller属性(控制和监听滚动)
ScrollController({
double initialScrollOffset = 0.0, // 初始滚动位置
this.keepScrollOffset = true,// 是否保存滚动位置
...
})
监听滚动事件
controller.addListener(()=>print(controller.offset))
常用的属性和方法:
1. offset
可滚动组件当前的滚动位置。
2. jumpTo(double offset)、animateTo(double offset,...)
用于跳转到指定的位置,不同之处在于,后者在跳转时会执行一个动画。
示例
创建一个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() {
super.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
);
}
),
);
}
}
滚动位置恢复
PageStorage是一个用于保存页面(路由)相关数据的功能型组件,它拥有一个存储桶,子树中的Widget可以通过指定不同的PageStorageKey来存储各自的数据或状态。
每次滚动结束,可滚动组件都会将滚动位置offset存储到PageStorage中,当可滚动组件重新创建时再恢复。
ScrollController.keepScrollOffset为false,则滚动位置将不会被存储,可滚动组件重新创建时会使用ScrollController.initialScrollOffset;
ScrollController.keepScrollOffset为true时,可滚动组件在第一次创建时,会滚动到initialScrollOffset处,因为这时还没有存储过滚动位置。在接下来的滚动中就会存储、恢复滚动位置,忽略initialScrollOffset。
当一个路由中包含多个可滚动组件时,如果发现在进行一些跳转或切换操作后,滚动位置不能正确恢复,这时可以通过显式指定不同的PageStorageKey来分别跟踪不同的可滚动组件的位置,如:
ListView(key: PageStorageKey(1), ... );
ListView(key: PageStorageKey(2), ... );
注意:一个路由中包含多个可滚动组件时,如果要分别跟踪它们的滚动位置,并非一定就得给他们分别提供PageStorageKey。这是因为Scrollable本身是一个StatefulWidget,它的状态中也会保存当前滚动位置,所以,只要可滚动组件本身没有被从树上detach掉,那么其State就不会销毁,滚动位置就不会丢失。只有当Widget发生结构变化,导致可滚动组件的State销毁或重新构建时才会丢失状态,这种情况就需要显式指定PageStorageKey,通过PageStorage来存储滚动位置,一个典型的场景是在使用TabBarView时,在Tab发生切换时,Tab页中的可滚动组件的State就会销毁,这时如果想恢复滚动位置就需要指定PageStorageKey。
ScrollPosition
真正保存滑动位置信息的对象。
offset只是一个便捷属性:double get offset => position.pixels;
一个ScrollController对象可以同时被多个可滚动组件使用,ScrollController会为每一个可滚动组件创建一个ScrollPosition对象,并保存在positions属性中(List<ScrollPosition>)。
controller.positions.elementAt(0).pixels
controller.positions.elementAt(1).pixels
controller.positions.length 被几个可滚动组件使用
ScrollController的animateTo() 和 jumpTo(),内部最终都会调用ScrollPosition的同名方法(真正来控制跳转滚动位置)。
ScrollController控制原理
ScrollPosition createScrollPosition(
ScrollPhysics physics,
ScrollContext context,
ScrollPosition oldPosition);
void attach(ScrollPosition position) ;
void detach(ScrollPosition position) ;
当ScrollController和可滚动组件关联时,可滚动组件首先会调用ScrollController的createScrollPosition()方法来创建一个ScrollPosition来存储滚动位置信息,接着,可滚动组件会调用attach()方法,将创建的ScrollPosition添加到ScrollController的positions属性中,这一步称为“注册位置”,只有注册后animateTo() 和 jumpTo()才可以被调用。
当可滚动组件销毁时,会调用ScrollController的detach()方法,将其ScrollPosition对象从ScrollController的positions属性中移除,这一步称为“注销位置”,注销后animateTo() 和 jumpTo() 将不能再被调用。
注意:ScrollController的animateTo() 和 jumpTo()内部会调用【所有】ScrollPosition的同名方法。
滚动监听
Flutter Widget树中子Widget可以通过发送通知(Notification)与父(包括祖先)Widget通信。父级组件可以通过NotificationListener组件来监听自己关注的通知,这种通信方式类似于Web开发中浏览器的事件冒泡。
可滚动组件在滚动时会发送ScrollNotification类型的通知,ScrollBar正是通过监听滚动通知来实现的。通过NotificationListener监听滚动事件和通过ScrollController有两个主要的不同:
1. 通过NotificationListener可以在从可滚动组件到widget树根之间任意位置都能监听。而ScrollController只能和具体的可滚动组件关联后才可以。
2. 收到滚动事件后获得的信息不同;NotificationListener在收到滚动事件时,通知中会携带当前滚动位置和ViewPort的一些信息,而ScrollController只能获取当前滚动位置。
在接收到滚动事件时,参数类型为ScrollNotification,它包括一个metrics属性,它的类型是ScrollMetrics,该属性包含当前ViewPort及滚动位置等信息:
1. pixels:当前滚动位置。
2. maxScrollExtent:最大可滚动长度。
3. extentBefore:滑出ViewPort顶部的长度;此示例中相当于顶部滑出屏幕上方的列表长度。
4. extentInside:ViewPort内部长度;此示例中屏幕显示的列表部分的长度。
5. extentAfter:列表中未滑入ViewPort部分的长度;此示例中列表底部未显示到屏幕范围部分的长度。
6. atEdge:是否滑到了可滚动组件的边界。
示例
import 'package:flutter/material.dart';
class ScrollNotificationTestRoute extends StatefulWidget {
@override
_ScrollNotificationTestRouteState createState() =>
new _ScrollNotificationTestRouteState();
}
class _ScrollNotificationTestRouteState
extends State<ScrollNotificationTestRoute> {
String _progress = "0%"; // 保存进度百分比
@override
Widget build(BuildContext context) {
return Scrollbar( // 进度条
// 监听滚动通知
child: NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
double progress = notification.metrics.pixels /
notification.metrics.maxScrollExtent;
// 重新构建
setState(() {
_progress = "${(progress * 100).toInt()}%";
});
print("BottomEdge: ${notification.metrics.extentAfter == 0}");
//return true; // 放开此行注释后,进度条将失效
},
child: Stack(
alignment: Alignment.center,
children: <Widget>[
ListView.builder(
itemCount: 100,
itemExtent: 50.0,
itemBuilder: (context, index) {
return ListTile(title: Text("$index"));
}
),
CircleAvatar( //显示进度百分比
radius: 30.0,
child: Text(_progress),
backgroundColor: Colors.black54,
)
],
),
),
);
}
}
1. ListView
沿一个方向线性排列所有子组件。支持基于Sliver的延迟构建模型。
/*
1. ListView中的列表项组件是RenderBox,并不是Sliver。
2. 一个ListView中只有一个Sliver(对列表项进行按需加载),默认是SliverList,如果指定了itemExtent,则为SliverFixedExtentList;如果prototypeItem属性不为空,则为SliverPrototypeExtentList。
3. 可以通过ListView.custom自定义列表项生成模型,它需要实现一个SliverChildDelegate用来给ListView生成列表项组件。
4. 可滚动组件的构造函数如果需要一个列表项Builder则支持基于Sliver的懒加载模型的,反之则不支持。
5. ListView高度边界无法确定时会异常
*/
ListView({
// 可滚动widget公共参数
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
EdgeInsetsGeometry padding,
// ListView各个构造函数的共同参数
double itemExtent,
Widget? prototypeItem, // 列表项原型
bool shrinkWrap = false,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
double cacheExtent,
// 子widget列表
// 这种方式适合只有少量的子组件数量已知且比较少的情况,反之则应该使用ListView.builder 按需动态构建列表项。
List<Widget> children = const <Widget>[],
})
说明:(强烈建议指定itemExtent或prototypeItem)
1. itemExtent
如果不为null,则表示滚动方向上子组件的长度。
指定后滚动系统可以提前知道列表的长度,而无需每次构建子组件时都去再计算,会更加高效。
2. prototypeItem(列表项原型)
如果所有列表项长度相同但不知道具体多少,可以指定一个列表项prototypeItem,可滚动组件会在layout时计算一次它延主轴方向的长度,和指定itemExtent一样。
注意:itemExtent和prototypeItem互斥,不能同时指定。
3. shrinkWrap
是否根据子组件的总长度来设置ListView的长度。
默认false ,ListView的会在滚动方向尽可能多的占用空间。当ListView在一个无边界(滚动方向上)的容器中时,shrinkWrap必须为true。
4. addAutomaticKeepAlives
是否将列表项(子组件)包裹在AutomaticKeepAlive 组件中;
如果设置为true(默认为true,在懒加载列表中会为每一个列表项添加AutomaticKeepAlive父组件),在列表项滑出视口时不会被GC(垃圾回收),它会使用KeepAliveNotification来保存其状态。
如果列表项自己维护其KeepAlive状态,那么此参数必须置为false。
5. addRepaintBoundaries
是否将列表项(子组件)包裹在RepaintBoundary组件中。默认为true。
将列表项包裹在RepaintBoundary中可以在滚动时避免列表项重绘,但是当列表项重绘的开销非常小时,不添加RepaintBoundary反而会更高效。
如果列表项自己维护其KeepAlive状态,那么此参数必须置为false
示例
ListView(
children: [
imgSection,
titleSection,
buttonSection,
textSection,
],
),
ListView(
padding: EdgeInsets.all(10),
children: [
ListTitle(
title:Text('hello'),
subTitle:('world'),
),
ListTitle(
leading:Icon(Icons.settings,color:Colors.yellow,size:30),
trailing:Image.network("http://.../1.png"),
title:Text(
'hello',
style: TextStyle(
fontSize: 24,
),
),
subTitle:('world'),
),
],
),
- 默认构造函数
有一个接受Widget列表的children参数。
适合只有少量的子组件的情况。不支持基于Sliver的懒加载模型。
通过此方式创建的ListView和使用SingleChildScrollView+Column的方式没有本质的区别。
示例
ListView(
shrinkWrap: true,
padding: const EdgeInsets.all(20.0),
children: <Widget>[
const Text('I\'m dedicating every day to you'),
const Text('Domestic life was never quite my style'),
const Text('When you smile, you knock me out, I fall apart'),
const Text('And I thought I was so smart'),
],
);
示例2
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final title = 'Basic List';
return new MaterialApp(
title: title,
home: new Scaffold(
appBar: new AppBar(
title: new Text(title),
),
body: new ListView(
children: <Widget>[
new ListTile(
leading: new Icon(Icons.map),
title: new Text('Map'),
),
new ListTile(
leading: new Icon(Icons.photo),
title: new Text('Album'),
),
new ListTile(
leading: new Icon(Icons.phone),
title: new Text('Phone'),
),
],
),
),
);
}
}
示例3(水平滚动)
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final title = 'Horizontal List';
return new MaterialApp(
title: title,
home: new Scaffold(
appBar: new AppBar(
title: new Text(title),
),
body: new Container(
margin: new EdgeInsets.symmetric(vertical: 20.0),
height: 200.0,
child: new ListView(
scrollDirection: Axis.horizontal, // 水平滚动
children: <Widget>[
new Container(
width: 260.0,
color: Colors.red,
),
new Container(
width: 260.0,
color: Colors.blue,
),
],
),
),
),
);
}
}
- ListView.builder 构造函数
适合列表项比较多(或不确定)的情况。支持基于Sliver的懒加载模型的。
ListView.builder({
...
// 列表项的构建器,返回值为一个widget。当滚动到对应index位置时会调用。
@required IndexedWidgetBuilder itemBuilder,
// 列表项的数量,如果为null,则为无限列表。
int itemCount,
})
示例
ListView.builder(
itemCount: 100,
itemExtent: 50.0, // 高度为50.0
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("$index"));
}
);
示例2
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
void main() {
runApp(new MyApp(
items: new List<String>.generate(10000, (i) => "Item $i"),
));
}
class MyApp extends StatelessWidget {
final List<String> items; // 数据源
MyApp({Key key, @required this.items}) : super(key: key);
@override
Widget build(BuildContext context) {
final title = 'Long List';
return new MaterialApp(
title: title,
home: new Scaffold(
appBar: new AppBar(
title: new Text(title),
),
body: new ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return new ListTile(
title: new Text('${items[index]}'),
);
},
),
),
);
}
}
示例3(不同类型的item)
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
void main() {
runApp(new MyApp(
items: new List<ListItem>.generate(
1000,
(i) => i % 6 == 0
? new HeadingItem("Heading $i")
: new MessageItem("Sender $i", "Message body $i"),
),
));
}
class MyApp extends StatelessWidget {
final List<ListItem> items;
MyApp({Key key, @required this.items}) : super(key: key);
@override
Widget build(BuildContext context) {
final title = 'Mixed List';
return new MaterialApp(
title: title,
home: new Scaffold(
appBar: new AppBar(
title: new Text(title),
),
body: new ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
if (item is HeadingItem) {
return new ListTile(
title: new Text(
item.heading,
style: Theme.of(context).textTheme.headline,
),
);
} else if (item is MessageItem) {
return new ListTile(
title: new Text(item.sender),
subtitle: new Text(item.body),
);
}
},
),
),
);
}
}
abstract class ListItem {}
class HeadingItem implements ListItem {
final String heading;
HeadingItem(this.heading);
}
class MessageItem implements ListItem {
final String sender;
final String body;
MessageItem(this.sender, this.body);
}
- ListView.separated
比ListView.builder多了一个separatorBuilder参数(在生成的列表项之间添加分割组件)。
示例(奇数行添加一条蓝色下划线,偶数行添加一条绿色下划线)
class ListView3 extends StatelessWidget {
@override
Widget build(BuildContext context) {
Widget divider1=Divider(color: Colors.blue,);
Widget divider2=Divider(color: Colors.green);
return ListView.separated(
itemCount: 100,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("$index"));
},
separatorBuilder: (BuildContext context, int index) { // 分割器构造器
return index%2==0?divider1:divider2;
},
);
}
}
示例2
从数据源异步分批拉取一些数据,然后用ListView展示,当滑动到列表末尾时,判断是否需要再去拉取数据,如果是,则去拉取,拉取过程中在表尾显示一个loading,拉取成功后将数据插入列表;如果不需要再去拉取,则在表尾提示"没有更多"
class InfiniteListView extends StatefulWidget {
@override
_InfiniteListViewState createState() => new _InfiniteListViewState();
}
class _InfiniteListViewState extends State<InfiniteListView> {
static const loadingTag = "##loading##"; //表尾标记
var _words = <String>[loadingTag];
@override
void initState() {
super.initState();
_retrieveData();
}
@override
Widget build(BuildContext context) {
return ListView.separated(
itemCount: _words.length,
itemBuilder: (context, index) {
// 如果到了表尾
if (_words[index] == loadingTag) {
//不足100条,继续获取数据
if (_words.length - 1 < 100) {
// 获取数据
_retrieveData();
// 加载时显示loading
return Container(
padding: const EdgeInsets.all(16.0),
alignment: Alignment.center,
child: SizedBox(
width: 24.0,
height: 24.0,
child: CircularProgressIndicator(strokeWidth: 2.0)
),
);
} else {
// 已经加载了100条数据,不再获取数据。
return Container(
alignment: Alignment.center,
padding: EdgeInsets.all(16.0),
child: Text("没有更多了", style: TextStyle(color: Colors.grey),)
);
}
}
// 显示单词列表项
return ListTile(title: Text(_words[index]));
},
separatorBuilder: (context, index) => Divider(height: .0),
);
}
void _retrieveData() {
Future.delayed(Duration(seconds: 2)).then((e) {
setState(() {
// 重新构建列表
_words.insertAll(_words.length - 1,
// 每次生成20个单词
generateWordPairs().take(20).map((e) => e.asPascalCase).toList()
);
});
});
}
}
- 添加固定的列表头
不太好的写法:
@override
Widget build(BuildContext context) {
return Column(children: <Widget>[
ListTile(title:Text("商品列表")),
SizedBox(
// Material设计规范中状态栏、导航栏、ListTile高度分别为24、56、56 。避免底部留白
height: MediaQuery.of(context).size.height-24-56-56,
child: ListView.builder(itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("$index"));
}),
)
]);
}
这种方法太不好,如果页面布局发生变化,比如表头布局调整导致表头高度改变,那么剩余空间的高度就得重新计算。修正:
// 自动拉伸ListView以填充屏幕剩余空间
@override
Widget build(BuildContext context) {
return Column(children: <Widget>[
ListTile(title:Text("商品列表")),
Expanded(
child: ListView.builder(itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text("$index"));
}),
),
]);
}
- AutomaticKeepAlive组件
将列表项的根RenderObject的keepAlive按需自动标记为true或false。
列表组件的Viewport区域+cacheExtent预渲染区域 称为加载区域 :
1. 当 keepAlive 标记为 false 时,如果列表项滑出加载区域时,列表组件将会被销毁。
2. 当 keepAlive 标记为 true 时,当列表项滑出加载区域后,Viewport 会将列表组件缓存起来;当列表项进入加载区域时,Viewport 从先从缓存中查找是否已经缓存,如果有则直接复用,如果没有则重新创建列表项。
子组件想改变是否需要缓存的状态时就向KeepAliveNotification通知,AutomaticKeepAlive收到消息后会去更改keepAlive的状态(从true变为false时,需要释放缓存)。
- 优化ListView
1.
列表项较多或不确定(上拉加载更多)时不要使用默认的构造函数,应该使用ListView.builder
2.
禁用addAutomaticKeepAlives(滑动过快时可能会出现短暂白屏)。
禁用addRepaintBoundaries,当列表元素布局较简单时(可提高流畅度)。
3.
列表中不可变子组件使用const修饰。 // children: [const ListImage()],
4.
指定itemExtent值,当可以提前知道时。
2. SingleChildScrollView
内容不会超过屏幕太多时使用SingleChildScrollView,只能接收一个子组件。
不支持基于Sliver的延迟实例化模型。
SingleChildScrollView({
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.padding,
bool primary,
this.physics,
this.controller,
this.child,
})
示例(将大写字母A-Z沿垂直方向显示)
class SingleChildScrollViewTestRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
String str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
return Scrollbar( // 显示进度条
child: SingleChildScrollView(
padding: EdgeInsets.all(16.0),
child: Center(
child: Column(
// 动态创建一个List<Widget>
children: str.split("")
// 每一个字母都用一个Text显示,字体为原来的两倍
.map((c) => Text(c, textScaleFactor: 2.0,))
.toList(),
),
),
),
);
}
}
运行结果
3. GridView(二维网格列表)
GridView({
Axis scrollDirection = Axis.vertical, // 滚动方向
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
bool shrinkWrap = false,
EdgeInsetsGeometry padding, // 内边距
// 控制子组件如何排列
// SliverGridDelegate定义了GridView Layout相关接口,子类需要通过实现它们来实现具体的布局算法。
// SliverGridDelegateWithFixedCrossAxisCount和SliverGridDelegateWithMaxCrossAxisExtent。
@required SliverGridDelegate gridDelegate,
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
double cacheExtent,
List<Widget> children = const <Widget>[], // 子列表
})
和ListView的大多数参数都是相同的。
- SliverGridDelegateWithFixedCrossAxisCount、GridView.count
横轴为固定数量子元素。
GridView.count构造函数内部使用了SliverGridDelegateWithFixedCrossAxisCount。
SliverGridDelegateWithFixedCrossAxisCount({
@required double crossAxisCount,
double mainAxisSpacing = 0.0,
double crossAxisSpacing = 0.0,
double childAspectRatio = 1.0,
})
说明:
1. crossAxisCount
横轴子元素的数量。
子元素在横轴的长度=ViewPort横轴长度/crossAxisCount。
2. childAspectRatio
子元素在横轴长度和主轴长度的比例。
子元素的大小是通过crossAxisCount和childAspectRatio两个参数共同决定的。
注意,这里的子元素指的是子组件的最大显示空间,确保子组件的实际大小不要超出子元素的空间。
3. mainAxisSpacing
主轴方向的间距。
4. crossAxisSpacing
横轴方向的间距。
示例
GridView(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3, // 横轴三个子widget
childAspectRatio: 1.0 // 宽高比为1
),
children:<Widget>[
Icon(Icons.ac_unit),
Icon(Icons.airport_shuttle),
Icon(Icons.all_inclusive),
Icon(Icons.beach_access),
Icon(Icons.cake),
Icon(Icons.free_breakfast)
]
);
上面的示例代码等价于(GridView.count):
GridView.count(
crossAxisCount: 3,
childAspectRatio: 1.0,
children: <Widget>[
Icon(Icons.ac_unit),
Icon(Icons.airport_shuttle),
Icon(Icons.all_inclusive),
Icon(Icons.beach_access),
Icon(Icons.cake),
Icon(Icons.free_breakfast),
],
);
示例(GridView.count)
import 'package:flutter/material.dart';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final title = 'Grid List';
return new MaterialApp(
title: title,
home: new Scaffold(
appBar: new AppBar(
title: new Text(title),
),
body: new GridView.count(
crossAxisCount: 2,
children: new List.generate(100, (index) {
return new Center(
child: new Text(
'Item $index',
style: Theme.of(context).textTheme.headline,
),
);
}),
),
),
);
}
}
- SliverGridDelegateWithMaxCrossAxisExtent、GridView.extent
横轴子元素为固定最大长度。
GridView.extent构造函数内部使用了SliverGridDelegateWithMaxCrossAxisExtent。
SliverGridDelegateWithMaxCrossAxisExtent({
double maxCrossAxisExtent, // 子元素在横轴上的最大长度
double mainAxisSpacing = 0.0,
double crossAxisSpacing = 0.0,
double childAspectRatio = 1.0,
})
示例
GridView(
padding: EdgeInsets.zero,
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 120.0,
childAspectRatio: 2.0 // 宽高比为2
),
children: <Widget>[
Icon(Icons.ac_unit),
Icon(Icons.airport_shuttle),
Icon(Icons.all_inclusive),
Icon(Icons.beach_access),
Icon(Icons.cake),
Icon(Icons.free_breakfast),
],
);
上面的示例代码等价于:
GridView.extent(
maxCrossAxisExtent: 120.0,
childAspectRatio: 2.0,
children: <Widget>[
Icon(Icons.ac_unit),
Icon(Icons.airport_shuttle),
Icon(Icons.all_inclusive),
Icon(Icons.beach_access),
Icon(Icons.cake),
Icon(Icons.free_breakfast),
],
);
- GridView.builder
通过GridView.builder来动态创建子widget。
GridView.builder(
...
@required SliverGridDelegate gridDelegate,
@required IndexedWidgetBuilder itemBuilder, // 子widget构建器
)
示例
从一个异步数据源(如网络)分批获取一些Icon,然后用GridView来展示
class InfiniteGridView extends StatefulWidget {
@override
_InfiniteGridViewState createState() => new _InfiniteGridViewState();
}
class _InfiniteGridViewState extends State<InfiniteGridView> {
List<IconData> _icons = []; //保存Icon数据
@override
void initState() {
// 初始化数据
_retrieveIcons();
}
@override
Widget build(BuildContext context) {
return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
// mainAxisSpacing: 10.0,
// crossAxisSpacing: 10.0,
crossAxisCount: 3, // 每行三列
childAspectRatio: 1.0 // 显示区域宽高相等
),
itemCount: _icons.length,
itemBuilder: (context, index) {
// 如果显示到最后一个并且Icon总数小于200时继续获取数据
if (index == _icons.length - 1 && _icons.length < 200) {
_retrieveIcons();
}
return Icon(_icons[index]);
}
);
}
void _retrieveIcons() {
Future.delayed(Duration(milliseconds: 200)).then((e) {
setState(() {
_icons.addAll([
Icons.ac_unit,
Icons.airport_shuttle,
Icons.all_inclusive,
Icons.beach_access, Icons.cake,
Icons.free_breakfast
]);
});
});
}
}
flutter_staggered_grid_view包可实现这种布局
网友评论