在 Flutter 的世界里,我们经常需要以网格、列表或者 Wrap 的形式呈现一组数据。遇到类似"标签"、"分类选项"或者"城市列表"时,如果再需要添加一些操作,比如删除、选择指示器,往往就需要自己费心手写逻辑。
今天给大家带来一款可以"自定义每个项目的操作按钮位置与形态"的网格组件 ------ CustomizableItemGrid。无论是要加个"删除"小圆圈、还是加个"打勾"的选择框,这个组件都能轻松帮你处理,还能快速切换到动画按钮、顶部/底部/左右角等多个位置。超灵活!
下面,就让我们一起来看看它的用法和实现原理吧。
一、功能概览
- 自定义项目列表 :传入一个
List<String>
就能快速展示每个元素。 - 可删除:点击或触发删除操作后,会更新父级状态,整个网格自动刷新。
- 可选中:提供选择模式开关,轻松切换是否可选中(打勾或其他自定义交互)。
- 可配置操作按钮:提供了四大"角落"位置供你自由摆放,还能用 builder 函数自定义操作按钮的外观与动画。
- 自定义布局:可设置项目间距、行间距、对齐方式,以及文字与边框样式。
二、示例界面:CustomWidgetDemo
我们先从 CustomWidgetDemo
入手,这是一个示例页面,用来展示如何使用 CustomizableItemGrid
这个组件。以下是它的主要功能:
- 显示一个地区列表(
areas
)并支持增删操作。 - 提供操作类型(四种:Simple Delete 、Animated Delete 、Icon Button 、Selection Indicator)的切换。
- 提供操作按钮在项目所处位置(四个角:Top Left 、Top Right 、Bottom Left 、Bottom Right)的切换。
- 允许开启或关闭选择模式,并在选择模式下查看哪些项目被选中。
核心代码如下(已省略 import 相关):
dart
import 'package:flutter/material.dart';
/// 定义操作小部件的可能位置。
enum ActionWidgetPosition {
/// 操作小部件位于项目左上角。
topLeft,
/// 操作小部件位于项目右上角。
topRight,
/// 操作小部件位于项目左下角。
bottomLeft,
/// 操作小部件位于项目右下角。
bottomRight,
}
/// 一个可重用的组件,用于显示带有可自定义操作小部件的响应式网格。
/// 每个项目可以在四个角之一放置一个自定义小部件。
class CustomizableItemGrid extends StatefulWidget {
/// 要显示的项目列表。
final List<String> items;
/// 当项目被删除时调用的回调函数。
final Function(List<String> updatedItems)? onItemsChanged;
/// 当项目被选中时调用的回调函数。
final Function(String item, bool isSelected)? onItemSelected;
/// 项目边框和文本的颜色。
final Color itemColor;
/// 项目的文本样式。
final TextStyle? textStyle;
/// 每个项目周围的内边距。
final EdgeInsetsGeometry itemPadding;
/// 项目之间的水平间距。
final double horizontalSpacing;
/// 行之间的垂直间距。
final double verticalSpacing;
/// 项目的圆角半径。
final double borderRadius;
/// 项目边框的宽度。
final double borderWidth;
/// 操作小部件的位置。
final ActionWidgetPosition actionWidgetPosition;
/// 用于为每个项目创建操作小部件的构建函数。
///
/// 该函数接收项目文本、删除回调、选中状态和选中状态变化回调。
/// 这允许创建自定义小部件,用作删除按钮、
/// 选择指示器或任何其他交互元素。
final Widget Function(
String item,
VoidCallback onDelete,
bool isSelected,
ValueChanged<bool> onSelectionChanged
) actionWidgetBuilder;
/// 操作小部件相对于角落的偏移量。
final Offset actionWidgetOffset;
/// 项目在 Wrap 组件中的对齐方式。
final WrapAlignment wrapAlignment;
/// 行在 Wrap 组件中的对齐方式。
final WrapAlignment runAlignment;
/// 项目在 Wrap 组件中的交叉轴对齐方式。
final WrapCrossAlignment crossAxisAlignment;
/// 是否允许项目被选中。
final bool selectable;
/// 最初选中的项目。
final List<String> initialSelectedItems;
/// 创建一个新的 [CustomizableItemGrid]。
const CustomizableItemGrid({
Key? key,
required this.items,
this.onItemsChanged,
this.onItemSelected,
this.itemColor = const Color(0xFF00CEC9),
this.textStyle,
this.itemPadding = const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),
this.horizontalSpacing = 12.0,
this.verticalSpacing = 16.0,
this.borderRadius = 16.0,
this.borderWidth = 2.0,
this.actionWidgetPosition = ActionWidgetPosition.topLeft,
required this.actionWidgetBuilder,
this.actionWidgetOffset = const Offset(-10, -10),
this.wrapAlignment = WrapAlignment.start,
this.runAlignment = WrapAlignment.start,
this.crossAxisAlignment = WrapCrossAlignment.start,
this.selectable = false,
this.initialSelectedItems = const [],
}) : super(key: key);
@override
State<CustomizableItemGrid> createState() => _CustomizableItemGridState();
}
class _CustomizableItemGridState extends State<CustomizableItemGrid> {
late List<String> _items;
late Set<String> _selectedItems;
@override
void initState() {
super.initState();
_items = List.from(widget.items);
_selectedItems = Set.from(widget.initialSelectedItems);
}
@override
void didUpdateWidget(CustomizableItemGrid oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.items != widget.items) {
_items = List.from(widget.items);
}
if (oldWidget.initialSelectedItems != widget.initialSelectedItems) {
_selectedItems = Set.from(widget.initialSelectedItems);
}
}
void _deleteItem(String item) {
setState(() {
_items.remove(item);
_selectedItems.remove(item);
});
if (widget.onItemsChanged != null) {
widget.onItemsChanged!(_items);
}
}
void _toggleItemSelection(String item, bool isSelected) {
setState(() {
if (isSelected) {
_selectedItems.add(item);
} else {
_selectedItems.remove(item);
}
});
if (widget.onItemSelected != null) {
widget.onItemSelected!(item, isSelected);
}
}
@override
Widget build(BuildContext context) {
return Wrap(
spacing: widget.horizontalSpacing,
runSpacing: widget.verticalSpacing,
alignment: widget.wrapAlignment,
runAlignment: widget.runAlignment,
crossAxisAlignment: widget.crossAxisAlignment,
children: _items.map((item) => _buildItem(item)).toList(),
);
}
Widget _buildItem(String item) {
final bool isSelected = _selectedItems.contains(item);
return CustomizableItem(
text: item,
onDelete: () => _deleteItem(item),
isSelected: isSelected,
onSelectionChanged: (value) => _toggleItemSelection(item, value),
itemColor: widget.itemColor,
textStyle: widget.textStyle,
padding: widget.itemPadding,
borderRadius: widget.borderRadius,
borderWidth: widget.borderWidth,
actionWidgetPosition: widget.actionWidgetPosition,
actionWidgetBuilder: widget.actionWidgetBuilder,
actionWidgetOffset: widget.actionWidgetOffset,
selectable: widget.selectable,
);
}
}
/// 一个带有可自定义操作小部件的单个项目。
class CustomizableItem extends StatelessWidget {
/// 项目中要显示的文本。
final String text;
/// 删除操作被触发时调用的回调函数。
final VoidCallback onDelete;
/// 项目当前是否被选中。
final bool isSelected;
/// 选中状态变化时调用的回调函数。
final ValueChanged<bool> onSelectionChanged;
/// 项目边框和文本的颜色。
final Color itemColor;
/// 项目的文本样式。
final TextStyle? textStyle;
/// 项目周围的内边距。
final EdgeInsetsGeometry padding;
/// 项目的圆角半径。
final double borderRadius;
/// 项目边框的宽度。
final double borderWidth;
/// 操作小部件的位置。
final ActionWidgetPosition actionWidgetPosition;
/// 用于创建操作小部件的构建函数。
final Widget Function(
String item,
VoidCallback onDelete,
bool isSelected,
ValueChanged<bool> onSelectionChanged
) actionWidgetBuilder;
/// 操作小部件相对于角落的偏移量。
final Offset actionWidgetOffset;
/// 项目是否可选。
final bool selectable;
/// 创建一个新的 [CustomizableItem]。
const CustomizableItem({
Key? key,
required this.text,
required this.onDelete,
required this.isSelected,
required this.onSelectionChanged,
this.itemColor = const Color(0xFF00CEC9),
this.textStyle,
this.padding = const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),
this.borderRadius = 16.0,
this.borderWidth = 2.0,
this.actionWidgetPosition = ActionWidgetPosition.topLeft,
required this.actionWidgetBuilder,
this.actionWidgetOffset = const Offset(-10, -10),
this.selectable = false,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Stack(
clipBehavior: Clip.none,
children: [
// 主容器,包含文本
GestureDetector(
onTap: selectable ? () => onSelectionChanged(!isSelected) : null,
child: Container(
padding: padding,
decoration: BoxDecoration(
border: Border.all(color: itemColor, width: borderWidth),
borderRadius: BorderRadius.circular(borderRadius),
color: isSelected ? itemColor.withOpacity(0.1) : Colors.transparent,
),
child: Text(
text,
style: textStyle ?? TextStyle(
color: itemColor,
fontSize: 20,
fontWeight: FontWeight.w500,
),
),
),
),
// 动态定位的自定义操作小部件
Positioned(
// 根据 actionWidgetPosition 动态定位
top: _getTopPosition(),
bottom: _getBottomPosition(),
left: _getLeftPosition(),
right: _getRightPosition(),
child: actionWidgetBuilder(
text,
onDelete,
isSelected,
onSelectionChanged
),
),
],
);
}
// 辅助方法,用于计算位置值
double? _getTopPosition() {
switch (actionWidgetPosition) {
case ActionWidgetPosition.topLeft:
case ActionWidgetPosition.topRight:
return actionWidgetOffset.dy;
case ActionWidgetPosition.bottomLeft:
case ActionWidgetPosition.bottomRight:
return null;
}
}
double? _getBottomPosition() {
switch (actionWidgetPosition) {
case ActionWidgetPosition.topLeft:
case ActionWidgetPosition.topRight:
return null;
case ActionWidgetPosition.bottomLeft:
case ActionWidgetPosition.bottomRight:
return actionWidgetOffset.dy;
}
}
double? _getLeftPosition() {
switch (actionWidgetPosition) {
case ActionWidgetPosition.topLeft:
case ActionWidgetPosition.bottomLeft:
return actionWidgetOffset.dx;
case ActionWidgetPosition.topRight:
case ActionWidgetPosition.bottomRight:
return null;
}
}
double? _getRightPosition() {
switch (actionWidgetPosition) {
case ActionWidgetPosition.topLeft:
case ActionWidgetPosition.bottomLeft:
return null;
case ActionWidgetPosition.topRight:
case ActionWidgetPosition.bottomRight:
return actionWidgetOffset.dx;
}
}
}
Demo
dart
class CustomWidgetDemo extends StatefulWidget {
const CustomWidgetDemo({Key? key}) : super(key: key);
@override
State<CustomWidgetDemo> createState() => _CustomWidgetDemoState();
}
class _CustomWidgetDemoState extends State<CustomWidgetDemo> {
// 初始的一组区域名称
List<String> areas = ['迴仔區', '青洲區', '台山區', '花地瑪堂區', '望德堂區', '大堂區', '風順堂區'];
// 当前选择的操作小部件类型
String currentWidgetType = 'Simple Delete';
// 当前操作小部件的位置
ActionWidgetPosition currentPosition = ActionWidgetPosition.topLeft;
// 是否开启选择模式
bool selectionMode = false;
// 已选中的项目
List<String> selectedItems = [];
// 更新列表回调
void _handleItemsChanged(List<String> updatedItems) {
setState(() {
areas = updatedItems;
});
}
// 处理项目选中回调
void _handleItemSelected(String item, bool isSelected) {
setState(() {
if (isSelected) {
selectedItems.add(item);
} else {
selectedItems.remove(item);
}
});
}
// 核心界面
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Custom Action Widgets'),
backgroundColor: Colors.white,
foregroundColor: Colors.black,
elevation: 0,
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 显示当前选中的区域
const Text(
'Selected Areas',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
// 使用我们自定义的 CustomizableItemGrid
Expanded(
child: CustomizableItemGrid(
items: areas,
onItemsChanged: _handleItemsChanged,
onItemSelected: _handleItemSelected,
actionWidgetPosition: currentPosition,
itemColor: const Color(0xFF00CEC9),
horizontalSpacing: 12.0,
verticalSpacing: 16.0,
wrapAlignment: WrapAlignment.start,
selectable: selectionMode,
initialSelectedItems: selectedItems,
actionWidgetBuilder: _buildActionWidget,
),
),
const SizedBox(height: 16),
// 切换操作小部件类型
Text(
'Action Widget Type:',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
_buildTypeButton('Simple Delete'),
_buildTypeButton('Animated Delete'),
_buildTypeButton('Icon Button'),
_buildTypeButton('Selection Indicator'),
],
),
const SizedBox(height: 16),
// 切换操作按钮位置
Text(
'Widget Position:',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
_buildPositionButton('Top Left', ActionWidgetPosition.topLeft),
_buildPositionButton('Top Right', ActionWidgetPosition.topRight),
_buildPositionButton('Bottom Left', ActionWidgetPosition.bottomLeft),
_buildPositionButton('Bottom Right', ActionWidgetPosition.bottomRight),
],
),
const SizedBox(height: 16),
// 切换选择模式
Row(
children: [
Text(
'Selection Mode:',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 8),
Switch(
value: selectionMode,
onChanged: (value) {
setState(() {
selectionMode = value;
if (!value) {
selectedItems.clear();
}
});
},
activeColor: const Color(0xFF00CEC9),
),
],
),
// 新增项目
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
setState(() {
areas.add('新區域 ${areas.length + 1}');
});
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00CEC9),
),
child: const Text('Add New Area'),
),
],
),
),
);
}
// 构建不同类型的小部件
Widget _buildActionWidget(
String item,
VoidCallback onDelete,
bool isSelected,
ValueChanged<bool> onSelectionChanged,
) {
switch (currentWidgetType) {
case 'Simple Delete':
return GestureDetector(
onTap: onDelete,
child: Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: const Color(0xFF00CEC9),
shape: BoxShape.circle,
),
child: Icon(
Icons.close,
color: Colors.white,
size: 20,
),
),
);
case 'Animated Delete':
return GestureDetector(
onTap: onDelete,
child: TweenAnimationBuilder<double>(
tween: Tween<double>(begin: 0, end: 1),
duration: const Duration(milliseconds: 300),
builder: (context, value, child) {
return Transform.scale(
scale: 0.8 + (value * 0.2),
child: Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: const Color(0xFF00CEC9),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2 * value),
blurRadius: 4 * value,
spreadRadius: 2 * value,
),
],
),
child: Icon(
Icons.close,
color: Colors.white,
size: 20,
),
),
);
},
),
);
case 'Icon Button':
return Material(
color: Colors.transparent,
child: InkWell(
onTap: onDelete,
borderRadius: BorderRadius.circular(20),
child: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: const Color(0xFF00CEC9),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.delete_outline,
color: Colors.white,
size: 20,
),
),
),
);
case 'Selection Indicator':
return GestureDetector(
onTap: () => onSelectionChanged(!isSelected),
child: Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: isSelected ? const Color(0xFF00CEC9) : Colors.white,
shape: BoxShape.circle,
border: Border.all(
color: const Color(0xFF00CEC9),
width: 2,
),
),
child: isSelected
? Icon(
Icons.check,
color: Colors.white,
size: 20,
)
: null,
),
);
default:
return GestureDetector(
onTap: onDelete,
child: Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: const Color(0xFF00CEC9),
shape: BoxShape.circle,
),
child: Icon(
Icons.close,
color: Colors.white,
size: 20,
),
),
);
}
}
// 构建"操作类型"按钮
Widget _buildTypeButton(String type) {
return ElevatedButton(
onPressed: () {
setState(() {
currentWidgetType = type;
});
},
style: ElevatedButton.styleFrom(
backgroundColor: currentWidgetType == type ? Color(0xFF00CEC9) : null,
),
child: Text(type),
);
}
// 构建"位置"按钮
Widget _buildPositionButton(String label, ActionWidgetPosition position) {
return ElevatedButton(
onPressed: () {
setState(() {
currentPosition = position;
});
},
style: ElevatedButton.styleFrom(
backgroundColor: currentPosition == position ? Color(0xFF00CEC9) : null,
),
child: Text(label),
);
}
}
如何运行示例
-
将上述代码(
CustomWidgetDemo
+CustomizableItemGrid
)拷贝到你的项目中,保证引用正常。 -
在
main.dart
中,将home
替换为CustomWidgetDemo()
,例如:dartvoid main() { runApp( MaterialApp( home: CustomWidgetDemo(), ), ); }
-
运行项目,就能看到演示界面,切换各种操作类型、改变位置、开启选择模式,实时看到效果。
三、核心组件:CustomizableItemGrid
下面重点介绍一下最核心的 CustomizableItemGrid
。它是一个 StatefulWidget ,内部使用 Wrap
布局来展示一系列可点击、可删除、可选的项目标签,并通过"自定义操作小部件"来实现各种交互。
1. 构造参数
以下是其关键属性与用途:
参数 | 类型 | 解释 |
---|---|---|
items |
List<String> |
必传。 要显示的一组字符串。 |
onItemsChanged |
Function(List<String> updatedItems)? |
传入一个回调函数,当内部有项目被删除时,回调会携带最新的列表数据(让父 widget 及时更新 state )。 |
onItemSelected |
Function(String item, bool isSelected)? |
当项目被选中或取消选中时触发的回调,携带项目本身以及选中状态。 |
itemColor |
Color |
每个项目的边框和文字颜色。默认为 Color(0xFF00CEC9) 。 |
textStyle |
TextStyle? |
设置每个项目文字样式,如果为空则使用默认样式。 |
itemPadding |
EdgeInsetsGeometry |
项目内部的 padding,默认为 EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0) 。 |
horizontalSpacing |
double |
item 与 item 间的水平间距,用于 Wrap 布局。默认为 12.0 。 |
verticalSpacing |
double |
item 与 item 间的垂直间距,用于 Wrap 布局。默认为 16.0 。 |
borderRadius |
double |
项目外边框的圆角半径,默认为 16.0 。 |
borderWidth |
double |
项目的边框宽度,默认为 2.0 。 |
actionWidgetPosition |
ActionWidgetPosition |
操作小部件在项目四个角的位置,可选 topLeft 、topRight 、bottomLeft 、bottomRight 。 |
actionWidgetBuilder |
Widget Function(String item, VoidCallback onDelete, bool isSelected, ValueChanged<bool> onSelectionChanged) |
必传。 用来为每个项目构建自定义操作小部件的函数,比如删除按钮、选择指示器等。 |
actionWidgetOffset |
Offset |
操作小部件相对于所属角落的偏移,默认为 Offset(-10, -10) ,即让操作按钮稍微溢出一点边界,看起来像挂在角落上。 |
wrapAlignment |
WrapAlignment |
控制主轴对齐方式,默认为 WrapAlignment.start 。 |
runAlignment |
WrapAlignment |
控制换行对齐方式,默认为 WrapAlignment.start 。 |
crossAxisAlignment |
WrapCrossAlignment |
控制交叉轴对齐方式,默认为 WrapCrossAlignment.start 。 |
selectable |
bool |
是否允许选中。默认为 false 。 |
initialSelectedItems |
List<String> |
初始选中的项目列表,默认为空列表。 |
2. 基本用法
简而言之,想要使用它,你只需要:
dart
CustomizableItemGrid(
items: myItemsList, // 显示的一组字符串
onItemsChanged: (updatedList) {
// 当有项目被删除时,这里会回调最新列表
setState(() {
myItemsList = updatedList;
});
},
onItemSelected: (item, isSelected) {
// 当有项目被选中或取消选中时触发
// 你可以在这里更新外部选中状态
},
actionWidgetPosition: ActionWidgetPosition.topRight, // 设置操作按钮放在右上角
wrapAlignment: WrapAlignment.start, // Wrap 排列方式
selectable: true, // 是否允许选中
initialSelectedItems: ['预先选中的内容'], // 如果需要预选
actionWidgetBuilder: (
String item,
VoidCallback onDelete,
bool isSelected,
ValueChanged<bool> onSelectionChanged,
) {
// 在这里根据需要返回各种自定义按钮
// 比如一个简单删除:
return GestureDetector(
onTap: onDelete,
child: Icon(Icons.close, color: Colors.red),
);
},
)
小贴士 :
onDelete
回调会删除当前项目并触发onItemsChanged
,如果你的业务逻辑不需要完全删除项目,只是想做别的事情,也可以不调用它,而是做其他交互效果。
四、实现原理简述
这个组件的核心思路是,先封装一个 CustomizableItemGrid
,内部用 Wrap
生成每个 item
的小方块,再在每个小方块的右上角(或其他角)叠加一个操作小部件。这样就不用每次去写重复的删除 / 选择代码,大大减少模板代码量。
1. Wrap 布局
dart
return Wrap(
spacing: widget.horizontalSpacing,
runSpacing: widget.verticalSpacing,
alignment: widget.wrapAlignment,
runAlignment: widget.runAlignment,
crossAxisAlignment: widget.crossAxisAlignment,
children: _items.map((item) => _buildItem(item)).toList(),
);
Wrap
可以很好地在多行中自动换行,并指定行间距和列间距。- 我们通过
map
将每个item
转换为一个CustomizableItem
。
2. CustomizableItem 内部结构
每个"项目"其实是一个 Stack
,里面放两层:
- Container + Text:显示文字,并带边框、背景色等。
- Positioned :放一个自定义的按钮(或图标),通过四个位置参数来控制它在
Stack
中是贴在左上角还是右下角。
最后,通过 actionWidgetBuilder
,你可以非常灵活地自行创造不同按钮形式或动画。
五、拓展与定制
1. 自定义外观
- 想定制项目外观?可以修改
itemColor
、textStyle
、itemPadding
、borderRadius
、borderWidth
等参数,让网格的样式完全符合你的 UI 设计需求。 - 想改操作按钮显示位置或偏移?可以在
actionWidgetPosition
和actionWidgetOffset
上做文章,轻松做到各种角落悬浮。
2. 新增动画或更复杂逻辑
如果你想要更炫酷的动画,除了示例中的 TweenAnimationBuilder
,完全可以用 AnimatedContainer
、ScaleTransition
或者你喜欢的动画组件进行包裹,然后在 actionWidgetBuilder
里面随意发挥。
六、常见问题
-
如何更换删除逻辑?
- 默认调用了
_deleteItem(item)
来从内部列表中移除项目。如果你不想真正删数据,也可以在自定义按钮的onTap
里改成别的操作,比如标记为"已禁用",然后自行处理setState()
或onItemsChanged
。
- 默认调用了
-
能否只显示而不带删除或选中?
- 可以!只要在
actionWidgetBuilder
里返回一个空SizedBox
或者返回Container()
不做交互,这样就相当于没有按钮。 - 如果你想把"删除按钮"从界面中去掉,
onDelete
也可以不调用。
- 可以!只要在
-
选择与删除能否并存?
- 可以。默认情况下,你在
onSelectionChanged
里处理选中状态,在onDelete
里处理删除操作,这俩是独立的逻辑。
- 可以。默认情况下,你在
七、总结
CustomizableItemGrid
和它的示例 CustomWidgetDemo
为我们提供了一种快速且灵活的网格展示 + 操作按钮组合的解决方案。通过可配置的参数和自定义的操作小部件,你可以轻松地打造出带有删除、选择、打标、动画等多种功能的标签式UI。
如果你正好在做 "标签"、"分类选择"、"动态可增删项目列表"等功能场景,这个组件能让你的开发效率直线飙升。希望本文能给你启发,并帮你节省一部分重复造轮子的时间。
感谢阅读,祝你在 Flutter 的探索之路上越走越远,代码越写越美!
Tip: 喜欢这篇文章?记得收藏并在评论区留下你的想法或改进建议哈。