控件拓展
Flutter 中提供了上百种控件,这些控件组成了Flutter绚丽的UI效果。在学习时,不可能将时间都花在学习控件上,这里仅选取较为常用的,可能会用到的控件进行专题学习。
可滚动控件
Flutter 为我们提供了多种可滚动控件用于显示各种复杂的列表和长布局。
BoxScrollView子类系列Sliver家族系列- 长布局
SingleChildScrollView控件 - 其他列表系列
ScrollView 总览
ScrollView的类继承关系如下,其中ScrollView和BoxScrollView都是抽象类

CustomScrollView
包含多个子布局模型。用于创建各种自定义滚动效果的
ScrollViewBoxScrollView
包含单个子布局模型。通常使用其两个子类创建UI
ScrollView 由三部分组成:
Scrollable可滚动控件会直接或间接包含一个
Scrollable控件,它主要用于监听各种用户手势并实现滚动的交互模型,但不包含UI显示相关的逻辑。Viewport指视口。即
Widget在屏幕上的实际显示区域,通常滚动列表中会有无数的项,但只有该项滑动到视口区域才是可见的,滑出视口,则不可见。Sliver可以组合以创建各种滚动效果的小控件,如列表,网格和扩展标题。通常可滚动控件的子项会非常多,累积的总高度非常大,如果一次性将子控件全部构建出将会非常耗费性能,因此Flutter提出一个
Sliver的(“薄片”)概念。如果可滚动控件支持Sliver模型,则该滚动可以将子控件分成多个“薄片”(Sliver),只有当Sliver出现在视口中时才会去构建它,这种模型也称为“基于Sliver的延迟构建模型”。如ListView、GridView都支持该模型

ScrollView 常见属性
| 属性名 | 类型 | 简述 |
|---|---|---|
| scrollDirection | Axis |
滚动视图的滚动轴,即滚动的方向 |
| reverse | bool |
是否逆向。本质上是决定可滚动控件的初始滚动位置是在“头”还是“尾” |
| controller | ScrollController |
控制滚动位置和监听滚动事件 |
| primary | bool |
是否是与父级PrimaryScrollController关联的主滚动视图。当此值为true时,滚动视图是可滚动的,即使它没有足够的内容来实际滚动。否则,默认情况下,用户只能在视图有足够内容的情况下滚动视图。需注意,为true时,controller应为null |
| physics | ScrollPhysics |
决定可滚动控件如何响应用户操作,比如用户滑动完抬起手指后,继续执行动画;或者滑动到边界时,如何显示 |
| shrinkWrap | bool |
是否根据正在查看的内容确定滚动视图的范围。如果在指定滚动轴上的滚动视图没有包裹内容,那么滚动视图将扩展到滚动轴允许的最大尺寸。如果滚动视图在滚动轴上没有边界约束,那么该属性必须为true。包裹滚动视图的内容比扩展到最大尺寸要更消耗性能,因为在滚动过程中,内容可以扩展和收缩,意味着每次滚动时,需要重新计算滚动视图的尺寸。 |
| cacheExtent | double |
视口在可见区域之前和之后有一个区域,用于缓存用户滚动时即将可见的项目 |
physics参数的使用
BouncingScrollPhysics:允许滚动超出边界,但之后内容会反弹回来, iOS的原生效果ClampingScrollPhysics: 防止滚动超出边界,滚动到列表末尾时有一个蓝色水波纹效果,Android的原生效果AlwaysScrollableScrollPhysics:始终响应用户的滚动,即使没有足够的内容。PageScrollPhysics:用于PageView使用的ScrollPhysicsFixedExtentScrollPhysics:仅适用于使用FixedExtendScrollControllers的列表。表示仅滚动到子项而不存在任何偏移NeverScrollableScrollPhysics:不响应用户的滚动,即禁用滚动。
ScrollController
jumpTo()直接跳转到指定的位置animateTo()以动画方式跳转到指定的位置addListener()添加滚动监听offset获取当前的滚动位置
import 'package:flutter/material.dart';
class ScrollControllerTest extends StatefulWidget {
@override
ScrollControllerTestState createState() {
return ScrollControllerTestState();
}
}
class ScrollControllerTestState extends State<ScrollControllerTest> {
ScrollController _controller = ScrollController();
@override
void initState() {
super.initState();
//监听滚动事件,打印滚动位置
_controller.addListener(() {
print(_controller.offset);
});
}
@override
void 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: FloatingActionButton(
child: Icon(Icons.arrow_upward),
onPressed: () {
//以动画方式返回到顶部
_controller.animateTo(0.0, duration: Duration(milliseconds: 200),
curve: Curves.ease
);
}
),
);
}
}
需要注意,一个ScrollController可以同时被多个Scrollable使用,ScrollController会为每一个Scrollable创建一个ScrollPosition对象,这些ScrollPosition保存在ScrollController的positions属性中(它是一个数组)。
如果ScrollController被设置给多个滚动控件使用,那么要获取某个滚动控件的offset ,可以使用如下方式
// 查看controller的offset属性实现
double get offset => position.pixels;
// 读取相关的滚动位置的方式
controller.positions.elementAt(0).pixels
controller.positions.elementAt(1).pixels
Scrollbar
它是一个滚动条控件,如果要给可滚动控件添加滚动条,只需使用Scrollbar作为父控件包裹可滚动控件即可。
Scrollbar(
child: ListView(
///...
),
);
ScrollConfiguration
用于控制可滚动小控件在子树中的行为方式。它实际上是继承自InheritedWidget,内部共享了一个ScrollBehavior对象。我们使用该功能控件的目的,其实就是为了设置它的ScrollBehavior对象,从而更改某些滚动行为。
通常的,我们可以使用该控件去除那种Android式的蓝色的列表回弹效果
class MyBehavior extends ScrollBehavior{
@override
Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) {
if(Platform.isAndroid||Platform.isFuchsia){
return child;
}else{
return super.buildViewportChrome(context,child,axisDirection);
}
}
}
/// 使用ScrollConfiguration包裹列表
ScrollConfiguration(
behavior: MyBehavior(),
child: ListView(),
);
滚动监听
我们可以使用NotificationListener来监听ScrollNotification事件,这方式比ScrollController中的监听更强大。
NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
print("onNotification: ${notification.metrics.pixels}");
return true;
},
child: ListView.builder(
itemCount: 100,
itemExtent: 50.0,
itemBuilder: (context, index) {
return ListTile(title: Text("$index"));
}
),
),
onNotification回调的参数类型为ScrollNotification,它包括一个metrics属性,该属性包含一些信息:
pixels:当前滚动位置maxScrollExtent:最大可滚动长度extentBefore:滑出视口顶部的长度extentInside:视口内部长度。当没有内边距时,相当于列表的长度extentAfter:列表中未滑入视口部分的长度atEdge:是否滑到了可滚动控件的边界(相当于列表顶或底部)
常用滚动控件
这些控件提供了更简洁的使用方式,在实际开发中使用最多。
ListView
它的同名构造方法中有几个参数需要说明
shrinkWrap: 当为 false 时,列表会在主轴方向上扩展到可占用的最大空间,反之列表占用的空间是其列表项高度之和,此时会耗费更多性能,每当列表项发生变化时,都会重新计算高度itemExtent:如果主轴是垂直方向,则代表的是子项的高度,如果主轴为水平方向,则代表的是子项的长度。指定 该值能提升性能,每当列表项发生变化时,都不需要重新计算addAutomaticKeepAlives:表示是否将列表项包裹在AutomaticKeepAlive中。在一个懒加载列表里,如果子项需要保证自己在滑出视口时不被回收,就需要设置为 true,内保就会使用KeepAliveNotification来保存其状态。如果子项要自己维护其KeepAlive状态,此参数必须置为falseaddRepaintBoundaries:表示是否将列表项包裹在RepaintBoundary中。为 true 时,可以避免列表项重绘,提高性能。但当列表项重绘的开销非常小(如一个颜色块,或者一个较短的文本)时,不添加 RepaintBoundary 反而会更高效
与ListView配套使用的,还有一个ListTile控件,用于列表的子项,当然也可以单独使用
| 属性名 | 类型 | 简述 |
|---|---|---|
| leading | Widget |
列表项左侧的图标 |
| title | Widget |
标题,通常放置文本控件 |
| subtitle | Widget |
副标题 |
| trailing | Widget |
列表项右侧的图标 |
| isThreeLine | bool |
内容是否可显示3行。为true,副标题最多显示2行 |
| dense | bool |
是否密集显示。为true时,内容及图标将会变小,显示更紧密 |
| contentPadding | EdgeInsetsGeometry |
内边距 |
| enabled | bool |
设为 false,可禁止点击事件 |
| onTap | GestureTapCallback |
单击事件回调 |
| onLongPress | GestureLongPressCallback |
长按事件回调 |
| selected | bool |
是否选中。为true时,文本和图标的颜色变为主题的主色 |
GridView
主要构建网格视图。在用法上与ListView非常相似,可参照其用法。
该控件主要有一个gridDelegate参数需要设置,该参数类型是SliverGridDelegate,用于控制子项如何排列。该类是一个抽象类,Flutter框架给我们提供了两个子类实现
SliverGridDelegateWithFixedCrossAxisCountSliverGridDelegateWithMaxCrossAxisExtent
与GridView配套的,也有一个GridTile控件,用于网格的子项,它带有页眉和页脚的显示功能。其header、footer参数通常使用GridTileBar控件,该控件属性与ListTile有些类似,可参照ListTile。
GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2
),
itemBuilder: (context, index) {
return GridTile(
header: GridTileBar(
backgroundColor:Color.fromRGBO(0, 0, 0, 0.4),
title: Text('Header'),),
child: Container(
child: Image.network(
'https://picturehost.oss-cn-shenzhen.aliyuncs.com/img/fbb.jpg',
fit: BoxFit.cover,),
),
footer: GridTileBar(
backgroundColor:Color.fromRGBO(0, 0, 0, 0.6),
title: Text('Footer'),),
);
},
itemCount: 20,
)
需要注意,框架的GridView默认子元素显示空间是相等的,但在实际开发中,可能会遇到子元素大小不等的情况,这里推荐一个第三方库实现
PageView
用于页面滑动的控件,子项会占据当前屏幕的所有可视区域。它有三种构造方式
PageView()PageView.builder()PageView.custom()
它的主轴方法默认是水平方向,通常的我们也将之用于左右滑动切换,需要注意,我们也能将其主轴设置为垂直方向,用作列表式的上下滚动。
SingleChildScrollView
该控件通常用于长布局的滚动。它只能包含一个子项,通常只应在期望的内容不会超过屏幕太多时使用,因为它不支持基于Sliver的延迟实例化模型,所以包含的长布局超出屏幕尺寸太多时,性能会变差,此时应该考虑使用一些支持Sliver延迟加载的可滚动控件,如ListView之类。
如果非要使用SingleChildScrollView来实现一个短列表,可以使用SingleChildScrollView + ListBody组合的方式实现。ListBody是一个依照给定的轴方向,并按照顺序排列子元素的控件。
Sliver 家族
CustomScrollView是一种可以使用Sliver来自定义滚动模型(效果)的控件,它可以包含多种滚动模型。具体的,是在它的slivers属性里放置各种Sliver 系列控件。
Sliver通常指可滚动控件的子元素(像一个个薄片),即那些需要粘合起来的可滚动控件就Sliver。但在CustomScrollView中,如果直接将ListView、GridView作为Sliver是不行的,因为它们本身就是可滚动控件而不是Sliver。为了能让可滚动控件和CustomScrollView配合使用,Flutter为我们提供了一些Sliver版可滚动控件,如SliverList、SliverGrid等等。
Sliver版的可滚动控件和非Sliver版的区别主要是前者不包含滚动模型(自身不能再滚动),而后者包含滚动模型 ,这些Sliver共用CustomScrollView的Scrollable,最终实现统一的滑动效果
SliverAppBar
它类似于Android中的CollapsingToolbarLayout,可轻松实现页面头部可伸缩效果,且与AppBar的大部分的属性重合,很像AppBar的加强版。需要注意,它通常要结合 CustomScrollView 或者 NestedScrollView 来使用,且作为第一个Sliver元素。
与AppBar相比,SliverAppBar 特有的属性
| 属性名 | 类型 | 简述 |
|---|---|---|
| forceElevated | bool |
结合 elevation 使用,当elevation 不为 0 时,表示是否显示阴影 |
| expandedHeight | double |
展开时的高度 |
| floating | bool |
为true时下滑,则AppBar优先滑动展示,展示完成后才给滑动控件滑动 |
| snap | bool |
为true时则 floating 也要为 true 。会根据手指松开的位置展开或者收缩AppBar |
| pinned | bool |
当appBar 收缩到最小高度的时候是否可见 |
示例
Scaffold(
appBar: AppBar(title: Text("滚动列表")),
body: CustomScrollView(
slivers:<Widget>[
SliverAppBar(
floating:true,
pinned: true,
snap: false,
expandedHeight: 250.0,
flexibleSpace: FlexibleSpaceBar(
title: const Text('一人之下'),
background: Image.network(
"https://picturehost.oss-cn-shenzhen.aliyuncs.com/img/timg.jpg",fit: BoxFit.cover,),
),
),
SliverFixedExtentList(
itemExtent: 50.0,
delegate: SliverChildBuilderDelegate((BuildContext context, int index) {
return ListTile(
title: Text("item $index"),
);
},
childCount: 50
),
),
]
),
);
这里FlexibleSpaceBar 是一个具有折叠功能的控件,它的collapseMode属性有三个值,可以实现一些折叠效果
CollapseMode.none背景不跟随滚动CollapseMode.parallax背景滚动,且具有一些收缩效果CollapseMode.pin背景跟随滚动
另外,我们还可以在SliverAppBar的bottom属性中组合TabBar来使用。
SliverPadding
相当于Sliver 系列的Padding控件,可以给Sliver 系列控件加边距。如果在Sliver 系列中直接使用Padding会报错
SliverPadding(
padding: const EdgeInsets.all(8.0),
sliver: SliverAppBar(
/// ......
),
)
SliverSafeArea
与SafeArea功能相同,区别在于该控件是用于Sliver中,包装Sliver系列的控件。
SliverList
它类似于ListView,有两种表现形式
SliverChildBuilderDelegate用于加载不确定数量的列表SliverChildListDelegate只能加载固定的已知数量的列表
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index){
return ListTile(title: Text('item ${index+1}'),);
},
childCount: 10),
)
/// -------------------------------------------------
SliverList(
delegate: SliverChildListDelegate([
ListTile(title: Text('item 1'),),
ListTile(title: Text('item 2'),),
ListTile(title: Text('item 3'),),
]),
)
SliverFixedExtentList
比 SliverList 多一个itemExtent 属性,可用于固定 item 的高度 ,item 里面的子控件无法再改动高度。
SliverGrid
类似于GridView,它有三个构造函数
SliverGrid.count()指定了一行展示几列 item/// 一行有4列 SliverGrid.count(children: scrollItems, crossAxisCount: 4)SliverGrid.extent()指定item的最大宽度,然后让框架自动计算一行展示几列/// 任一列最大宽度为80 SliverGrid.extent(children: scrollItems, maxCrossAxisExtent: 80.0)SliverGrid()自定义item排列方式SliverGrid( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: products.length, ), delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return _buildItem(products[index]);; } );
自定义SliverGrid的item展示方式,需要设置gridDelegate参数,该参数有两种选项
SliverGridDelegateWithMaxCrossAxisExtent/// 根据给定的 maxCrossAxisExtent 宽度自动分配一行展示多少列 SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 150, mainAxisSpacing: 10.0, //主轴中间间距 crossAxisSpacing: 10.0, //交叉轴中间间距 childAspectRatio: 2.0, //item 宽高比 ),SliverGridDelegateWithFixedCrossAxisCount/// 固定一行展示多少列 SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 4, mainAxisSpacing: 10.0, //主轴中间间距 crossAxisSpacing: 10.0, //副轴中间间距 childAspectRatio: 2.0, //item 宽高比 )
综合示例
CustomScrollView(
slivers:<Widget>[
/// 首先放一个SliverAppBar
SliverAppBar(
floating:true,
pinned: true,
snap: false,
expandedHeight: 250.0,
flexibleSpace: FlexibleSpaceBar(
title: const Text('一人之下'),
background: Image.network(
"https://picturehost.oss-cn-shenzhen.aliyuncs.com/img/timg.jpg",fit: BoxFit.cover,),
),
),
/// 中间放一个SliverGrid
SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0,
childAspectRatio: 4.0,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
alignment: Alignment.center,
color: Colors.yellow[100 * (index % 9)],
child:GridTile(
child:Text('grid tile $index')
),
);
},
childCount: 30,
),
),
/// 最后放一个SliverFixedExtentList
SliverFixedExtentList(
itemExtent: 50.0,
delegate: SliverChildBuilderDelegate((BuildContext context, int index) {
return ListTile(
title: Text("item $index"),
);
},
childCount: 50 //50个列表项
),
),
]
),
SliverPersistentHeader
SliverAppBar实际上就是封装了SliverPersistentHeader,是其的简化版本。与SliverAppBar相比,该控件可以出现在Sliver的任何位置,不一定是头部,且还能实现更多复杂的效果。
要使用SliverPersistentHeader,主要是需要一个SliverPersistentHeaderDelegate类型的参数,由于该类是一个抽象类,且没有暴露出具体的子类实现,因此我们只能自定义一个类继承自SliverPersistentHeaderDelegate,并重写四个方法
minExtent:收起状态下控件的高度maxExtent:展开状态下控件的高度shouldRebuild:是否重新构建,一般默认返回truebuild:构建要显示的UI。其中shrinkOffset参数表示从maxExtent到minExtent的距离,当shrinkOffset为0时表示完全展开
class SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return null;
}
@override
double get maxExtent => null;
@override
double get minExtent => null;
@override
bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
return true;
}
}
Sliver头根据上下滑动实现背景渐变的过渡效果,具体示例
import 'package:flutter/material.dart';
class CustomSliverHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverPersistentHeader(
pinned: true,
delegate: SliverMyHeaderDelegate(
title: '一人之下',
appBarHeight: 60,
expandedHeight: 300,
paddingTop: MediaQuery.of(context).padding.top,
background: Image.network("https://picturehost.oss-cn-shenzhen.aliyuncs.com/img/timg.jpg",fit: BoxFit.cover,)
),
),
SliverFillRemaining(
child: Container(
padding: const EdgeInsets.all(16),
color: Colors.grey[300],
child: Text('这是一段文章'),
),
)
],
),
);
}
}
class SliverMyHeaderDelegate extends SliverPersistentHeaderDelegate {
final double appBarHeight;
final double expandedHeight;
final double paddingTop;
final Widget background;
final String title;
SliverMyHeaderDelegate({
this.appBarHeight,
this.expandedHeight,
this.paddingTop,
this.background,
this.title,
});
@override
double get minExtent => this.appBarHeight + this.paddingTop;
@override
double get maxExtent => this.expandedHeight;
@override
bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
return true;
}
Color _updateAppBarColor(shrinkOffset) {
final int alpha = (shrinkOffset / (this.maxExtent - this.minExtent) * 255).clamp(0, 255).toInt();
return Color.fromARGB(alpha, 255, 255, 255);
}
Color _updateAppBarTitleColor(shrinkOffset, isIcon) {
if(shrinkOffset <= 50) {
return isIcon ? Colors.white : Colors.transparent;
} else {
final int alpha = (shrinkOffset / (this.maxExtent - this.minExtent) * 255).clamp(0, 255).toInt();
return Color.fromARGB(alpha, 0, 0, 0);
}
}
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
return SizedBox(
height: maxExtent,
width: MediaQuery.of(context).size.width,
child: Stack(
fit: StackFit.expand,
children: <Widget>[
background,
Positioned(
left: 0,
right: 0,
top: 0,
child: Container(
color: _updateAppBarColor(shrinkOffset),
child: SafeArea(
child: SizedBox(
height: appBarHeight,
child: Row(
children: <Widget>[
IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: _updateAppBarTitleColor(shrinkOffset, true),
),
),
Text(
title,
style: TextStyle(
fontSize: 20,
color: _updateAppBarTitleColor(shrinkOffset, false),
),
),
],
),
),
),
),
),
],
),
);
}
}
SliverToBoxAdapter
如果想要在Sliver系列的可滚动视图中添加一个非Sliver控件,就可以使用SliverToBoxAdapter来包装,将各种其他的控件组合在一起。
SliverFillRemaining
可创建一个填充视口中剩余空间的Sliver控件。
SliverFillViewport
创建包含多个框状子项的Sliver控件,每个子项都会填充视口。它会将其子项沿主轴放置在线性数组中。
SliverFillViewport(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
color: Colors.brown[200],
child: Text('SliverFillViewport item $index'),
);
}, childCount: 5
),
viewportFraction:1.0,//占屏幕的比例
),
滑动嵌套
NestedScrollView
该控件是一个可以实现滑动嵌套的 ScrollView。有些时候,我们可能需要将一个列表嵌套入另一个可滚动控件中作为子项,这时候可能会存在滑动冲突,而NestedScrollView则可以作为父布局实现嵌套。
import 'package:flutter/material.dart';
class NestedScrollViewTest extends StatefulWidget {
@override
NestedScrollViewTestState createState() {
return NestedScrollViewTestState();
}
}
class NestedScrollViewTestState extends State<NestedScrollViewTest>
with SingleTickerProviderStateMixin {
TabController _controller;
@override
void initState() {
super.initState();
_controller = TabController(length: 2, vsync: this);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverAppBar(
floating: true,
pinned: true,
snap: true,
expandedHeight: 250.0,
flexibleSpace: FlexibleSpaceBar(
title: const Text('一人之下'),
background: PageView(
children: <Widget>[
Image.network(
"https://picturehost.oss-cn-shenzhen.aliyuncs.com/img/timg.jpg",
fit: BoxFit.cover,
),
Image.network(
"https://picturehost.oss-cn-shenzhen.aliyuncs.com/img/fbb.jpg",
fit: BoxFit.cover,
)
],
),
),
),
SliverPersistentHeader(
pinned: true,
delegate: StickyTabBarDelegate(
child: TabBar(
labelColor: Colors.black,
controller: _controller,
tabs: <Widget>[
Tab(text: '标签1'),
Tab(text: '标签2'),
],
),
),
),
];
},
body: TabBarView(
controller: _controller,
children: <Widget>[
_buildTabPage(0),
_buildTabPage(1),
],
),
),
);
}
_buildTabPage(int index) {
if (index == 0) {
return ListView.builder(
itemExtent: 60,
itemBuilder: (ctx, index) {
return ListTile(
title: Text("item $index"),
);
});
} else {
return Container(
alignment: Alignment.center,
child: Text("这是一个新的Tab页"),
);
}
}
}
/// 吸附式TabBar效果
class StickyTabBarDelegate extends SliverPersistentHeaderDelegate {
final TabBar child;
StickyTabBarDelegate({@required this.child});
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return Container(
color: Theme.of(context).backgroundColor,
child: this.child,
);
}
@override
double get maxExtent => this.child.preferredSize.height;
@override
double get minExtent => this.child.preferredSize.height;
@override
bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) {
return true;
}
}
其他滚动控件
ListWheelScrollView
它是一个带滚筒效果的ListView,用法上也和ListView相似。它有两种构造方式
ListWheelScrollView()ListWheelScrollView.useDelegate()
ListWheelScrollView.useDelegate(
itemExtent: 80,
childDelegate: ListWheelChildBuilderDelegate(
builder: (context, index) {
return Container(
margin: EdgeInsets.symmetric(vertical: 8, horizontal: 16),
color: Colors.primaries[index % 10],
alignment: Alignment.center,
child: Text('$index'),
);
},
childCount: 50),
)
ListWheelScrollView 常见属性
| 属性名 | 类型 | 简述 |
|---|---|---|
| diameterRatio | double |
圆筒直径和主轴窗口的高度比。值越小越小表示圆筒越圆 |
| perspective | double |
取值在(0,0.01]之间。为0时表示从无限远处看圆筒,0.01表示无限近处看 |
| offAxisFraction | double |
表示车轮水平偏离中心的程度 |
| useMagnifier | bool |
是否启用放大镜 |
| magnification | double |
放大倍率,与useMagnifier配合使用 |
| squeeze | double |
挤压程度。值越大,一屏展示的子项越多,也越挤压。 |
| onSelectedItemChanged | ValueChanged<int> |
选中回调 |
ExpansionPanelList
是一个子项可展开的列表控件。
class ExpansionTest extends StatefulWidget {
@override
ExpansionTestState createState() {
return ExpansionTestState();
}
}
class ItemData{
ItemData(this.index,this.isExpanded);
int index;
bool isExpanded;
}
class ExpansionTestState extends State<ExpansionTest> {
List<ItemData> dataList ;
@override
void initState() {
super.initState();
dataList = List.generate(30, (i){
return ItemData(i,false);
});
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: SingleChildScrollView(
child: ExpansionPanelList(
expansionCallback: (index, isExpanded) {
setState(() {
dataList[index].isExpanded = !isExpanded;
});
},
children: dataList.map((value) {
return ExpansionPanel(
isExpanded: value.isExpanded,
headerBuilder: (context, isExpanded) {
return ListTile(
title: Text('子项 - ${value.index}'),
);
},
body: Container(
alignment: Alignment.center,
height: 80,
color: Colors.grey[200],
child: Text("这是展开的内容"),
),
);
}).toList(),
),
),
),
);
}
}
ExpansionTile
是一个折叠菜单列表,严格的说,它并不是一个可滚动的列表。
ExpansionTile(
title: Text('折叠菜单'),
leading: Icon(Icons.label, color: Colors.lightBlue),
backgroundColor: Colors.white,
initiallyExpanded: false, /// 是否默认展开
children: <Widget>[
ListTile(
title:Text('这是主标题1'),
subtitle:Text('这是副标题1')
),
ListTile(
title:Text('这是主标题2'),
subtitle:Text('这是副标题2')
)
]
)
滚动相关控件小结
- ListView
- GridView
- PageView
- SingleChildScrollView
- NestedScrollView
- CustomScrollView
- Scrollbar
- NotificationListener
- ScrollConfiguration
- ListWheelScrollView
- ExpansionPanelList
- AnimatedList
其他控件
RefreshIndicator
Material Design下拉刷新指示器,用于包装一个可滚动控件。
List _data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
@override
Widget build(BuildContext context) {
return Scaffold(
body: RefreshIndicator(
onRefresh: () async {
setState(() {
_data = List.from(_data.reversed);
});
},
child: ListView.builder(
itemBuilder: (context, index) {
return ListTile(
title: Text('item ${_data[index]}'),
);
},
itemExtent: 50,
itemCount: _data.length,
),
),
);
}
CupertinoSliverRefreshControl
ios风格的下拉刷新控件。它和RefreshIndicator稍有不同,它需要放在CustomScrollView中使用。
List _data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: CustomScrollView(
physics: BouncingScrollPhysics(),
slivers: <Widget>[
CupertinoSliverRefreshControl(
onRefresh: () async {
setState(() {
_data = List.from(_data.reversed);
});
},
),
SliverList(
delegate: SliverChildBuilderDelegate((content, index) {
return ListTile(
title: Text('item ${_data[index]}'),
);
}, childCount: _data.length),
)
],
),
),
);
}
Flutter框架没有提供上拉加载功能,我们通过滚动监听也可以自行实现。这里推荐使用第三方库
GridPaper
可用于绘制一个像素宽度的网格背景,结合Stack可以实现表格效果。
Container(
constraints: BoxConstraints.expand(),
child: GridPaper(
color: Colors.red,
divisions: 1,
subdivisions: 4,
child: Text("这是 GridPaper 的显示效果"),
),
)
Form
通常在向服务器提交数据前,都会对各个输入框数据进行合法性校验,但是对每一个TextField都分别进行校验将会是一件很麻烦的事。有时候用户想清除一组TextField的内容,只能一个个清除非常麻烦。为此,Flutter提供了一个Form 组件,它可以对输入框进行分组,然后进行一些统一操作,如内容校验、输入框重置以及输入内容保存。
由于Form的子孙元素必须是FormField类型,为了方便使用,Flutter提供了一个TextFormField组件,它继承自FormField类,也是TextField的一个包装类。
var _account = '';
var _pwd = '';
final _formKey = GlobalKey<FormState>();
Form(
key: _formKey,
child: Column(
children: <Widget>[
TextFormField(
decoration: InputDecoration(hintText: '请输入账号'),
onSaved: (value) {
_name = value;
},
validator: (String value) {
return value.length >= 6 ? null : '最少6个字符';
},
),
TextFormField(
decoration: InputDecoration(hintText: '请输入密码'),
obscureText: true,
onSaved: (value) {
_pwd = value;
},
validator: (String value) {
return value.length >= 6 ? null : '最少6个字符';
},
),
RaisedButton(
child: Text('登录'),
onPressed: () {
var _state = _formKey.currentState;
if(_state.validate()){
_state.save();
login(_name,_pwd);
}
},
)
],
),
)
公众号“编程之路从0到1”