需求
在 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); @override TagFlowWidgetState createState() => TagFlowWidgetState(); } class TagFlowWidgetState extends State<TagFlowWidget> { bool isExpanded = false; @override 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; } } @override 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; } @override Size getSize(BoxConstraints constraints) { final height = (itemHeight * TagFlowET.maxRowCount) + (spaceVertical * (TagFlowET.maxRowCount - 1)); return Size(constraints.maxWidth, height); } @override bool shouldRelayout(covariant FlowDelegate oldDelegate) { return oldDelegate != this || (oldDelegate as TagFlowDelegate).isExpanded != isExpanded; } @override 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