Flutter学习 可滚动Widget 中
Posted RikkaTheWorld
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter学习 可滚动Widget 中相关的知识,希望对你有一定的参考价值。
文章目录
5. AnimatedList
AnimatedList
和 ListView 功能差不多, 顾名思义,它在列表中插入节点或删除节点时会执行一些动画
它是一个 StatefulWidget
,对应的 State 是 AnimatedListState
,添加、删除元素的方法是:
void insetItem(int index, Duration duration = mDuration );
void removeItem(int index, AnimatedListRemovedItemBuilder builder, Duration duration = mDuration );
5.1 实例代码
class _AnimatedListRouteState extends State<AnimatedListRoute>
var data = <String>[];
int counter = 5;
final globalKey = GlobalKey<AnimatedListState>();
@override
void initState()
for (var i = 0; i < counter; i++)
data.add("$i + 1");
super.initState();
@override
Widget build(BuildContext context)
return Scaffold(
body: Stack(
children: [
AnimatedList(
key: globalKey,
initialItemCount: data.length,
itemBuilder: (BuildContext context, int index,
Animation<double> animation)
// 添加列表项时会执行渐显动画
return FadeTransition(
opacity: animation, child: buildItem(context, index));
),
// 创建一个添加按钮
buildAddBtn(),
],
),
);
Widget buildAddBtn()
return Positioned(
child: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: ()
// 添加一个列表项
data.add("$++counter");
// 告诉列表项有添加的列表项
globalKey.currentState!.insertItem(data.length - 1);
,
),
bottom: 30,
left: 0,
right: 0,
);
// 构建列表项
Widget buildItem(context, index)
String char = data[index];
return ListTile(
key: ValueKey(char),
title: Text(char),
trailing: IconButton(
icon: const Icon(Icons.delete),
// 点击时进行删除
onPressed: () => onDelete(context, index)),
);
void onDelete(context, index)
setState(()
globalKey.currentState!.removeItem(index, (context, animation)
// 删除过程执行的是反向动画, animation.value 会从 1 变成0
var item = buildItem(context, index);
data.removeAt(index);
return FadeTransition(
opacity: CurvedAnimation(
parent: animation,
curve: const Interval(0.5, 1.0),
),
// 不断缩小列表项的高度
child: SizeTransition(
sizeFactor: animation,
axisAlignment: 0.0,
child: item,
),
);
, duration: const Duration(milliseconds: 200));
);
6. GridView
GridView 用于构建网格列表,构造函数如下:
class GridView extends BoxScrollView
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>[],
...
GridView 也包含了大多数的 ListView 有的通用参数,比较重要的是 gridDelegate
这个属性,它接受一个 SliverGridDelegate
,控制 GridView
子组件如何排列
SliverGridDelegate
是一个抽象类, 定义了 GridView Layout相关接口,子类实现它来实现布局算法。 Flutter已经提供两个实现类,分别是:SliverGridDelegateWithFixedCrossAxisCount
和SliverGridDelegateWithMaxCrossAxisExtent
这两个用的应该会比较多,我们来使用并介绍它:
6.1 SliverGridDelegateWithFixedCrossAxisCount
横轴为固定数量子元素的布局算法,构造函数为:
SliverGridDelegateWithFixedCrossAxisCount(
required this.crossAxisCount,
this.mainAxisSpacing = 0.0,
this.crossAxisSpacing = 0.0,
this.childAspectRatio = 1.0,
this.mainAxisExtent,
crossAxisCount
横轴子元素数量, 此属性确定后, 子元素在横轴的长度就确定了,即ViewPort 横轴长度 / crossAxisCount
mainAxisSpacing
主轴方向的间距crossAxisSpacing
横轴方向子元素的间距childAspecRatio
子元素在横轴长度和主轴长度的比例, 用于 crossAxisCount 指定后,子元素横轴长度就确定了,然后通过此参数值可以确定子元素在主轴的长度mainAxisExtent
主轴上每个子元素的具体长度
这里看一个例子:
GridView(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
// 横轴三个子Widget
crossAxisCount: 4,
// 宽高比为 1:1
childAspectRatio: 1.0),
children: const [
Icon(Icons.add),
Icon(Icons.eleven_mp),
Icon(Icons.ten_k),
Icon(Icons.cake),
Icon(Icons.beach_access),
Icon(Icons.free_breakfast),
Icon(Icons.all_inclusive),
],
))
6.2 GridView.count
GridView.count 构造函数内部使用了 SliverGridDelegateWithFixedCrossAxisCount
,我们通过它可以快速的创建横轴固定数量子元素的 GridView, 上面的示例代码其实就等价于:
GridView.count(
crossAxisCount: 4,
childAspectRatio: 1.0,
children: const [
Icon(Icons.add),
Icon(Icons.eleven_mp),
Icon(Icons.ten_k),
Icon(Icons.cake),
Icon(Icons.beach_access),
Icon(Icons.free_breakfast),
Icon(Icons.all_inclusive),
],
)
6.3 SliverGridDelegateWithMaxCrossAxisExtent
实现了横轴子元素为固定最大长度的布局算法,构造函数为:
const SliverGridDelegateWithMaxCrossAxisExtent(
required this.maxCrossAxisExtent,
this.mainAxisSpacing = 0.0,
this.crossAxisSpacing = 0.0,
this.childAspectRatio = 1.0,
this.mainAxisExtent,
属性和之前所学基本一模一样,而 maxCrossAxisExtent
为子元素在横轴上的最大长度, 如果 ViewPort 的横轴长度是 450,那么当 maxCrossAxisExtent
的值在区间 [450/4, 450/3] 的话,子元素最终实际长度都是 112.5 而 childAspectRatio
是指子元素横轴和主轴的长度比
下面看一个例子:
GridView(
padding: EdgeInsets.zero,
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 120.0, childAspectRatio: 2.0 // 宽高比为2
),
children: const [
Icon(Icons.add),
Icon(Icons.eleven_mp),
Icon(Icons.ten_k),
Icon(Icons.cake),
Icon(Icons.beach_access),
Icon(Icons.free_breakfast),
Icon(Icons.all_inclusive),
],
),
6.4 GridView.extent
上面的代码等价于:
GridView.extent(
padding: EdgeInsets.zero,
maxCrossAxisExtent: 120.0, childAspectRatio: 2.0,
// 宽高比为2
children: const [
Icon(Icons.add),
Icon(Icons.eleven_mp),
Icon(Icons.ten_k),
Icon(Icons.cake),
Icon(Icons.beach_access),
Icon(Icons.free_breakfast),
Icon(Icons.all_inclusive),
],
),
6.5 GridView.builder
上面我们介绍 GridView 都需要一个 widget 数组作为其子元素,这些方式都会提前将所有子 widget 都构建好,所以只适用于子Widget数量比较少的时候,当使用较多的时候,和 ListView一样, 使用 GridView.builder
来构建子 Widget, GridView.builder 必须指定两个参数:
GridView.builder(
...
required SliverGridDelegate gridDelegate,
required IndexedWidgetBuilder itemBuilder,
)
6.5.1 范例
class _GridViewRouteRouteState extends State<GridViewRoute>
// icon 数据源
List<IconData> _icons = [];
@override
void initState()
super.initState();
// 初始化数据
_retrieveIcons();
// 模拟异步加载数据
void _retrieveIcons()
Future.delayed(const Duration(milliseconds: 200)).then((value)
setState(()
_icons.addAll([
Icons.add,
Icons.eleven_mp,
Icons.ten_k,
Icons.cake,
Icons.beach_access,
Icons.free_breakfast,
Icons.all_inclusive
]);
);
);
@override
Widget build(BuildContext context)
return Scaffold(
body: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
// 每行4列
crossAxisCount: 3,
childAspectRatio: 1.0),
itemCount: _icons.length,
itemBuilder: (context, index)
if (index == _icons.length -1 && _icons.length < 200)
_retrieveIcons();
return Icon(_icons[index]);
),
);
7. PageView 与 页面缓存
7.1 PageView
在android中,如果需要实现页面的切换,可以使用 PageView
,Flutter也有同名同作用的 Widget, 下面是它的构造函数:
PageView(
Key? key,
this.scrollDirection = Axis.horizontal,
this.reverse = false,
PageController? controller,
this.physics,
this.pageSnapping = true,
this.onPageChanged,
List<Widget> children = const <Widget>[],
this.dragStartBehavior = DragStartBehavior.start,
this.allowImplicitScrolling = false,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
this.scrollBehavior,
this.padEnds = true,
pageSnapping
每次滑动是否强制切换整个画面,如果为false,会根据实际的滑动距离显示页面this.allowImplicitScrolling
主要是配合辅助功能使用padEnds
下面会讲解
我们看一个 Tab 切换的实例,每个Tab都只显示一个数字,然后总共有6个Tab:
class _PageViewRouteState extends State<PageViewRoute>
@override
Widget build(BuildContext context)
var children = <Widget>[];
// 设置六个Tab
for (int i = 0; i < 6; i++)
children.add(Page(
text: "$i",
));
return Scaffold(body: PageView(children: children));
class Page extends StatefulWidget
const Page(Key? key, required this.text) : super(key: key);
final String text;
@override
State<StatefulWidget> createState() => _PageState();
class _PageState extends State<Page>
@override
Widget build(BuildContext context)
print("build $widget.text");
return Center(
child: Text(
widget.text,
textScaleFactor: 5,
));
接下来就可以正常滑动Tab了,实现还是比较简单的
7.2 页面缓存
在上面的示例中,每次页面的切换,可都会触发新 Page 页的 build
,这说明 PageView 默认没有缓存功能,一旦某个Page画出页面就会被销毁。
这是因为 PageView 没有透传 cacheExtent 给 Viewport,所以 Viewport 默认 cacheExtent为1, 但是却在 allowImplicitScrolling
为 true 时设置了预渲染区域, 此时将会设置缓存类型为 CacheExtentStyle.viewport
,则 cacheExtent 则表示缓存的长度是几个 Viewport 的宽度, cacheExtent 为1.0,则代表前后各缓存一页。
也就是说,将 PageView 的 allowImplicitScrolling
设置为 true 时,就会缓存前后两页的Page
问题的根源貌似是 在 PageView 中设置 cacheExtent 会和 ios的辅助功能有冲突,没有更好的解决方法,Flutter就带着这个问题。但是国内基本不用考虑用辅助功能,所以想到的解决方案就是 拷贝一份PageView 的源码,然后透传 cacheExtent 即可。
当然,Flutter还提供了更通用的解决方案,就是缓存子项的解决方案
8. 可滚动组件子项缓存 KeepAlive
在 ListView 的构造函数中有一个 addAutomaticKeepAlives
属性没有介绍,如果为 true, ListView就会为其每一个子项添加一个 AutomaticKeepAlive
父组件。 虽然 PageView 的默认构造函数和 PageView.build 构造函数中没有该参数,但他们最终都会生成一个 SliverChildDelegate
,这个组件会在每个列表子项构建完成时,为其添加一个 AutomaticKeepAlive
的父组件,下面来介绍一个这个父组件。
8.1 AutomaticKeepAlive
AutomaticKeepAlive 组件的主要作用是将列表项的 根RenderObject 的 keepAlive
按需自动标记为true或false。
就是 列表项的根 Widget, 这里将 Viewport+cacheExtent称为 加载区域。
- 当 keepAlive 为false时,如果列表项滑出加载区域,列表组件会被销毁
- 当 keepAlive 为true时,当列表项滑出加载区域后,Viewport会将列表项组件缓存起来,当列表项进入加载区域时,Viewport先从缓存中查找是否已经缓存,如果有直接复用,如果没有重新创建列表项
而 AutomaticKeepAlive 这个标识位的设置,其实全靠我们开发者来控制的。
所以为了让 PageView 实现多页面的缓存,我们的思路就是让 PageView 来讲 AutomaticKeepAlive 置为true,Flutter 的做法就是让列表项组件 混入一个 AutomaticKeepAliveClientMixin
,然后实现 wantKeepAlive()
就可以了,代码如下:
class _PageState extends State<Page> with AutomaticKeepAliveClientMixin
@override
Widget build(BuildContext context)
super.build(context);
...
@override
bool get wantKeepAlive => true; // 需要缓存
在实现了 wantKeepAlive
后,还必须要在 build
方法中调用一下 super.build(context)
,它会将 keepAlive 的信息通知出去。
需要注意的是,如果我们采用 PageView.cutom
构建页面时,没有给列表项包装 AutomaticKeepAlive 父组件,则上述方案不能正常工作。
9. TabBarView
TabBarView 是 Marterial 提供的 Tab 组件,通常和 TabBar 搭配使用
9.1 TabBarView
TabBarView 封装了 PageView,构造函数如下:
const TabBarView(
Key? key,
required this.children, // Tab 页面
this.controller, // TabController
this.physics,
this.dragStartBehavior = DragStartBehavior.start,
)
这里的 TabController
是用来监听和控制 TabBarView 的页面切换,通常是和 TabBar 来联动的,如果没有指定会默认在组件树上查找最近一个使用的 DefaultTabController
9.2 TabBar
TabBar 的许多属性都是用来配置 指示器 和 label 的,我们来看下其构造函数:
const TabBar(
Key? key,
// 具体的 Tab 数组,需要我们来创建
required this.tabs,
// TabController,用于和 TabBarView 联动
this.controller,
// 是否可以滑动
this.isScrollable = false,
this.padding,
// 指示器颜色
this.indicatorColor,
this.automaticIndicatorColorAdjustment = true,
// 指示器高度,默认为2
this.indicatorWeight = 2.0,
this.indicatorPadding = EdgeInsets.zero,
// 指示器 Decoration
this.indicator,
// 指示器长度,两个可选值,一个是 Tab 长度,一个是 label 长度
this.indicatorSize,
this.labelColor,
this.labelStyle,
this.labelPadding,
...
)
TabBar 和 TabBarView 是靠 TabController 进行联动的, 需要注意的是, TabBar 和 TabBarView 的孩子数量需要一致。
tab 是 TabBar 的孩子,可以是任意 Widget, 不过 Material 已经实现了默认的 Tab 组件给我们使用:
const Tab(
Key? key,
this.text, //文本
this.icon, // 图标
this.iconMargin = const EdgeInsets.only(bottom: 10.0),
this.height,
this.child, // 自定义 widget
)
其中 text 和 child 是互斥的
9.3 示例
代码如下:
class _TabBarViewRouteState extends State<TabBarViewRoute>
with SingleTickerProviderStateMixin
final ScrollController _controller = ScrollControllerFlutter学习 可滚动Widget 下