需求
在 Flutter 开发中,常常需要实现自定义布局以满足不同的需求。本文将介绍如何通过自定义组件实现一个折叠流布局,该组件能够显示一系列标签,并且在内容超出一定行数时,可以展开和收起。
效果
该折叠流布局可以显示一组标签,并在标签数量超过指定行数时提供展开和收起功能。初始状态下只显示限定行数的标签,并在最后一个位置显示展开按钮。点击展开按钮可以显示全部标签,再次点击展开按钮可以收起标签。
效果图如下:
实现思路
实现一个自定义的折叠流布局组件需要以下几个步骤:
- 创建一个
TagFlowWidget
组件,接受标签数据、最大行数和样式等参数。 - 在
TagFlowWidget
组件中使用Flow
布局,并实现自定义的FlowDelegate
来处理标签布局和展开收起逻辑。 - 在
FlowDelegate
中计算当前显示的行数,根据行数决定是否显示展开或收起按钮。
具体实现包括以下关键部分:
TagFlowWidget
组件的构建逻辑- 自定义
FlowDelegate
的布局和绘制逻辑 - 展开和收起状态的管理
实现代码
下面是完整的实现代码,包括 TagFlowWidget
组件和 TagFlowDelegate
代理类。
import 'package:flutter/material.dart';
class TagFlowWidget extends StatefulWidget {
final List<String> items;
final int maxRows;
final double spaceHorizontal;
final double spaceVertical;
final double itemHeight;
final Color? itemBgColor;
final BorderRadiusGeometry? borderRadius;
final double? horizontalPadding;
final TextStyle? itemStyle;
const TagFlowWidget({
Key? key,
required this.items,
required this.maxRows,
required this.itemHeight,
this.borderRadius,
this.horizontalPadding,
this.spaceHorizontal = 0,
this.spaceVertical = 0,
this.itemBgColor,
this.itemStyle,
}) : super(key: key);
TagFlowWidgetState createState() => TagFlowWidgetState();
}
class TagFlowWidgetState extends State<TagFlowWidget> {
bool isExpanded = false;
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final maxWidth = constraints.maxWidth;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
Flow(
delegate: TagFlowDelegate(
maxRows: widget.maxRows,
isExpanded: isExpanded,
maxWidth: maxWidth,
itemHeight: widget.itemHeight,
spaceHorizontal: widget.spaceHorizontal,
spaceVertical: widget.spaceVertical,
itemCount: widget.items.length,
),
children: [
...widget.items.map((item) {
return Container(
padding: EdgeInsets.symmetric(
horizontal: widget.horizontalPadding ?? 0,
),
height: widget.itemHeight,
decoration: BoxDecoration(
color: widget.itemBgColor,
borderRadius: widget.borderRadius,
),
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
item,
style: widget.itemStyle,
),
],
));
}).toList(),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
setState(() {
isExpanded = !isExpanded;
});
},
child: Container(
padding: EdgeInsets.symmetric(
horizontal: widget.horizontalPadding ?? 0,
),
height: widget.itemHeight,
decoration: BoxDecoration(
color: widget.itemBgColor,
borderRadius: widget.borderRadius,
),
child: Icon(
isExpanded
? Icons.keyboard_arrow_up
: Icons.keyboard_arrow_down,
),
),
),
],
),
],
);
},
);
}
}
class TagFlowET {
static int maxRowCount = 0;
}
class TagFlowDelegate extends FlowDelegate {
final double maxWidth;
final double spaceHorizontal;
final double spaceVertical;
int maxRows;
final bool isExpanded;
double height = 0;
final double itemHeight;
final int itemCount;
TagFlowDelegate({
required this.maxWidth,
required this.itemCount,
required this.spaceHorizontal,
required this.spaceVertical,
required this.maxRows,
required this.isExpanded,
required this.itemHeight,
}) {
if (!isExpanded) {
TagFlowET.maxRowCount = maxRows;
}
}
void paintChildren(FlowPaintingContext context) {
TagFlowET.maxRowCount = _getMaxRowCount(context);
if (maxRows >= TagFlowET.maxRowCount) {
maxRows = TagFlowET.maxRowCount;
}
double x = 0;
double y = 0;
double rowHeight = 0;
int rowCount = 1;
for (int i = 0; i < context.childCount; i++) {
final childSize = context.getChildSize(i)!;
final arrowBtnSize = context.getChildSize(context.childCount - 1)!;
if (rowCount >= maxRows &&
!isExpanded &&
(x + childSize.width + arrowBtnSize.width) >= maxWidth) {
context.paintChild(
context.childCount - 1,
transform: Matrix4.translationValues(x, y, 0),
);
break;
}
if (x + childSize.width > maxWidth) {
x = 0;
y += rowHeight + spaceVertical;
rowHeight = 0;
rowCount++;
}
if (!(i == context.childCount - 1 && isExpanded && rowCount <= maxRows)) {
context.paintChild(
i,
transform: Matrix4.translationValues(x, y, 0),
);
}
x += childSize.width + spaceHorizontal;
rowHeight = childSize.height;
}
}
int _getMaxRowCount(FlowPaintingContext context) {
double x = 0;
var rowCount = 1;
for (int i = 0; i < context.childCount; i++) {
final childSize = context.getChildSize(i)!;
final arrowSize = context.getChildSize(context.childCount - 1)!;
if (x + childSize.width > maxWidth) {
x = 0;
rowCount++;
}
if (i == context.childCount - 1 &&
(x + childSize.width + arrowSize.width) > maxWidth) {
rowCount++;
}
x += childSize.width + spaceHorizontal;
}
return rowCount;
}
Size getSize(BoxConstraints constraints) {
final height = (itemHeight * TagFlowET.maxRowCount) +
(spaceVertical * (TagFlowET.maxRowCount - 1));
return Size(constraints.maxWidth, height);
}
bool shouldRelayout(covariant FlowDelegate oldDelegate) {
return oldDelegate != this ||
(oldDelegate as TagFlowDelegate).isExpanded != isExpanded;
}
bool shouldRepaint(covariant FlowDelegate oldDelegate) {
return oldDelegate != this ||
(oldDelegate as TagFlowDelegate).isExpanded != isExpanded;
}
}
具体使用
import 'package:flutter/material.dart';
import 'package:flutter_xy/widgets/xy_app_bar.dart';
import 'package:flutter_xy/xydemo/flow/tag_flow_view.dart';
class TagFlowPage extends StatelessWidget {
const TagFlowPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: XYAppBar(
title: '折叠标签',
onBack: () {
Navigator.pop(context);
}),
body: Container(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TagFlowWidget(
items: const [
'衣服',
'T恤宽松男',
'男鞋',
'香蕉苹果',
'休闲裤',
'牛仔裤',
'红薯',
'红薯',
'红薯',
'红薯',
'西红柿',
'更多商品',
'热销商品',
'最新商品',
'特价商品',
'限时特价商品',
'限时商品',
'热门商品',
],
maxRows: 3,
spaceHorizontal: 8,
spaceVertical: 8,
itemHeight: 30,
horizontalPadding: 8,
itemBgColor: Colors.lightBlue.withAlpha(30),
itemStyle: const TextStyle(height: 1.1),
borderRadius: const BorderRadius.all(Radius.circular(8)),
),
],
)),
),
);
}
}
通过上述代码,我们实现了一个自定义的折叠流布局组件 TagFlowWidget
,并通过 TagFlowDelegate
来控制标签的布局和展开收起的逻辑。希望这篇文章对你在 Flutter 开发中的自定义布局实现有所帮助。
详情见:github.com/yixiaolunhui/flutter_xy