展示分类快捷入口的分页效果
实现方式:
- 使用PageView组件实现分页效果
- 使用 flutter_staggered_grid_view 插件实现网格效果
- 使用AnimatedContainer组件实现高度变化的动画效果
- 自定义分页指示器
分页效果
@override
Widget build(BuildContext context) {
if (widget.categoryGrids != null) {
return Column(
children: [
// 分类网格
// PageView在使用时一定要限制大小
Container(
height: 200.0,
child: PageView(
children: [Text('第一页'), Text('第二页')]
),
),
// 指示器
Container()
],
);
} else {
// 将来补充加载中的骨架屏
return Container();
}
}
网格效果
- flutter_staggered_grid_view 插件:
- 一个实现网格布局的插件,可以实现多列网格大小不同的布局(瀑布流)
- 主轴方向上可以自适应高度(这是比GridView好的地方,不用设置宽高比,不用担心内容溢出)
- 安装 flutter_staggered_grid_view 插件:
- pubspec.yaml文件中,安装方式如下:
flutter_staggered_grid_view: ^0.4.0
- pubspec.yaml文件中,安装方式如下:
展示分类网格信息
核心逻辑:根据分页的总页数,构建分页网格视图
实现步骤:
- 计算分页的总页数
- 根据分页的总页数,循环构建分页网格视图
- 使用 flutter_staggered_grid_view 插件构建网格视图
- 禁用滚动:避免网格视图和CustomScrollView滚动冲突
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
分页展示分类网格信息
核心逻辑:将分类网格数据展示到对应的分页中
实现步骤:
- 计算当前页网格数据的起始位置
- 计算当前页网格数据的结束位置
- 将当前页的网格数据传入到网格视图
for (var i = 0; i < pages; i++) {
// 1. 计算当前页网格数据的起始位置
int start = i * 10;
// 2. 计算当前页网格数据的结束位置
int end = 0;
if (categoryGrids.sublist(start, categoryGrids.length).length > 10) {
// 剩下的分类个数大于10,结束位置继续取10个
end = start + 10;
} else {
end = categoryGrids.length;
}
// 3. 将当前页的网格数据传入到网格视图
items.add(_buildItem(categoryGrids.sublist(start, end)));
}
高度变化的动画效果
使用AnimatedContainer组件实现高度变化的动画效果
核心逻辑:以动画的形式展示高度变化效果
实现步骤:
- AnimatedContainer组件作为PageView组件的父组件
- 计算单行时和两行时的网格高度
- 监听PageView翻页事件,计算当前页是否是单行
AnimatedContainer(
duration: Duration(milliseconds: 400),
curve: Curves.ease, // 动画的样式:先快后慢
height: isSingle ? totalHeight1 : totalHeight2,
child: PageView(
children: _buildPages(widget.categoryGrids!),
// 3. 监听PageView翻页事件,计算当前页是否是单行
onPageChanged: (int index) {
int count = widget.categoryGrids!.sublist(index * 10, widget.categoryGrids!.length).length;
setState(() {
isSingle = count <= 5; // 如果是单行,isSingle=true
});
},
),
),
分页指示器
构建分类快捷入口的分页指示器
核心逻辑:使用Row组件根据总页数和当前展示的页码构建分页指示器
实现步骤:
- 使用Row组件排列分页指示器
- 根据总页数循环创建指示器元素
- 更新当前页的页码
for (var i = 0; i < _pages; i++) {
bool isActive = i == _activeIndex;
items.add(
Container(
width: 15.0,
height: 3.0,
color: isActive ? Color(0xFF3CCEAF) : Color(0xFFE2E2E2),
),
);
}
...
onPageChanged: (int index) {
int count = widget.categoryGrids!.sublist(index * 10, widget.categoryGrids!.length).length;
setState(() {
isSingle = count <= 5; // 如果是单行,isSingle=true
_activeIndex = index; // 当前页的页码
});
},
...
全部代码
import 'package:erabbit_app_flutter/models/home_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
class CategoryGridsWidget extends StatefulWidget {
CategoryGridsWidget({this.categoryGrids});
/// 分类网格数据
final List<CategoryGridsModel>? categoryGrids;
@override
_CategoryGridsWidgetState createState() => _CategoryGridsWidgetState();
}
class _CategoryGridsWidgetState extends State<CategoryGridsWidget> {
/// 图标的宽度
double _imageWidth = 0.0;
/// 是否是单行
bool isSingle = false;
/// 分页的总页数
int _pages = 0;
/// 当前页的页码
int _activeIndex = 0;
/// 根据总页数循环创建指示器元素
List<Widget> _buildIndicator() {
List<Widget> items = [];
for (var i = 0; i < _pages; i++) {
bool isActive = i == _activeIndex;
items.add(
Container(
width: 15.0,
height: 3.0,
color: isActive ? Color(0xFF3CCEAF) : Color(0xFFE2E2E2),
),
);
}
return items;
}
/// 构建分类网格:使用插件构建网格视图
Widget _buildItem(List<CategoryGridsModel> categoryGrids) {
return StaggeredGridView.countBuilder(
crossAxisCount: 5, // 每一行要展示的item的个数 = crossAxisCount / StaggeredTile.fit(1)
itemCount: categoryGrids.length,
mainAxisSpacing: 18.0, // 上下两行间距
// 为了解决StaggeredGridView和CustomScrollView的滚动冲突,需要禁用滚动效果
physics: NeverScrollableScrollPhysics(),
// shrinkWrap搭配禁用滚动,解决滚动视图间的滚动冲突
shrinkWrap: true,
itemBuilder: (BuildContext context, int index) {
CategoryGridsModel categoryGridsModel = categoryGrids[index];
return Column(
children: [
Image.network(
categoryGridsModel.picture!,
width: _imageWidth,
height: _imageWidth,
fit: BoxFit.cover,
),
Container(
height: 20.0,
child: Text(
categoryGridsModel.name!,
style: TextStyle(color: Color(0xFF131313), fontSize: 13.0),
),
),
],
);
},
staggeredTileBuilder: (int index) {
return StaggeredTile.fit(1);
});
}
/// 构建分页网格视图
List<Widget> _buildPages(List<CategoryGridsModel> categoryGrids) {
List<Widget> items = [];
// 计算分页的总页数
// categoryGrids.length * 0.1 : 分类总个数(14)除以每页最多展示的分类个数(10) ==> 1.4
// (1.4).ceil() ==> 2
// ceil() 取某个数值的上限的整数,会读取某个数值等于或者大于他的整数
_pages = (categoryGrids.length * 0.1).ceil();
// 根据分页的总页数,循环构建分页网格视图
for (var i = 0; i < _pages; i++) {
// 1. 计算当前页网格数据的起始位置
int start = i * 10;
// 2. 计算当前页网格数据的结束位置
int end = 0;
if (categoryGrids.sublist(start, categoryGrids.length).length > 10) {
// 剩下的分类个数大于10,结束位置继续取10个
end = start + 10;
} else {
end = categoryGrids.length;
}
// 3. 将当前页的网格数据传入到网格视图
items.add(_buildItem(categoryGrids.sublist(start, end)));
}
return items;
}
@override
Widget build(BuildContext context) {
// 获取屏幕宽度
double screenWidth = MediaQuery.of(context).size.width;
// 6 * 16.0 : 五个图标之间的6个间距
// 计算分类图标的宽度:(屏幕宽度 - 6 * 16.0) * 0.2
_imageWidth = (screenWidth - 6 * 16.0) * 0.2;
// 2. 计算单行时和两行时的网格高度
// 单行高度:图标高度 + 文字高度(20.0)
double totalHeight1 = _imageWidth + 20.0;
// 两行高度:2 * totalHeight1 + 上下两行的间距(18.0)
double totalHeight2 = 2 * totalHeight1 + 18.0;
debugPrint('${widget.categoryGrids}');
if (widget.categoryGrids != null) {
return Column(
children: [
// 分类网格
// PageView在使用时一定要限制大小
// 1. AnimatedContainer组件作为PageView组件的父组件
AnimatedContainer(
duration: Duration(milliseconds: 400),
curve: Curves.ease, // 动画的样式:先快后慢
height: isSingle ? totalHeight1 : totalHeight2,
child: PageView(
children: _buildPages(widget.categoryGrids!),
// 3. 监听PageView翻页事件,计算当前页是否是单行
onPageChanged: (int index) {
int count = widget.categoryGrids!.sublist(index * 10, widget.categoryGrids!.length).length;
setState(() {
isSingle = count <= 5; // 如果是单行,isSingle=true
_activeIndex = index; // 当前页的页码
});
},
),
),
// 指示器
Padding(
padding: EdgeInsets.only(top: 10.0, bottom: 14.0),
child: Row(
// 设置指示器居中
mainAxisAlignment: MainAxisAlignment.center,
children: _buildIndicator(),
),
),
],
);
} else {
// 将来补充加载中的骨架屏
return Container();
}
}
}