Flutter自定义带有Badger组件组


在 Flutter 的世界里,我们经常需要以网格、列表或者 Wrap 的形式呈现一组数据。遇到类似"标签"、"分类选项"或者"城市列表"时,如果再需要添加一些操作,比如删除、选择指示器,往往就需要自己费心手写逻辑。

今天给大家带来一款可以"自定义每个项目的操作按钮位置与形态"的网格组件 ------ CustomizableItemGrid。无论是要加个"删除"小圆圈、还是加个"打勾"的选择框,这个组件都能轻松帮你处理,还能快速切换到动画按钮、顶部/底部/左右角等多个位置。超灵活!

下面,就让我们一起来看看它的用法和实现原理吧。


一、功能概览

  1. 自定义项目列表 :传入一个 List<String> 就能快速展示每个元素。
  2. 可删除:点击或触发删除操作后,会更新父级状态,整个网格自动刷新。
  3. 可选中:提供选择模式开关,轻松切换是否可选中(打勾或其他自定义交互)。
  4. 可配置操作按钮:提供了四大"角落"位置供你自由摆放,还能用 builder 函数自定义操作按钮的外观与动画。
  5. 自定义布局:可设置项目间距、行间距、对齐方式,以及文字与边框样式。

二、示例界面:CustomWidgetDemo

我们先从 CustomWidgetDemo 入手,这是一个示例页面,用来展示如何使用 CustomizableItemGrid 这个组件。以下是它的主要功能:

  • 显示一个地区列表(areas)并支持增删操作。
  • 提供操作类型(四种:Simple DeleteAnimated DeleteIcon ButtonSelection Indicator)的切换。
  • 提供操作按钮在项目所处位置(四个角:Top LeftTop RightBottom LeftBottom 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),
    );
  }
}

如何运行示例

  1. 将上述代码(CustomWidgetDemo + CustomizableItemGrid)拷贝到你的项目中,保证引用正常。

  2. main.dart 中,将 home 替换为 CustomWidgetDemo(),例如:

    dart 复制代码
    void main() {
      runApp(
        MaterialApp(
          home: CustomWidgetDemo(),
        ),
      );
    }
  3. 运行项目,就能看到演示界面,切换各种操作类型、改变位置、开启选择模式,实时看到效果。


三、核心组件: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 操作小部件在项目四个角的位置,可选 topLefttopRightbottomLeftbottomRight
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,里面放两层:

  1. Container + Text:显示文字,并带边框、背景色等。
  2. Positioned :放一个自定义的按钮(或图标),通过四个位置参数来控制它在 Stack 中是贴在左上角还是右下角。

最后,通过 actionWidgetBuilder,你可以非常灵活地自行创造不同按钮形式或动画。


五、拓展与定制

1. 自定义外观

  • 想定制项目外观?可以修改 itemColortextStyleitemPaddingborderRadiusborderWidth 等参数,让网格的样式完全符合你的 UI 设计需求。
  • 想改操作按钮显示位置或偏移?可以在 actionWidgetPositionactionWidgetOffset 上做文章,轻松做到各种角落悬浮。

2. 新增动画或更复杂逻辑

如果你想要更炫酷的动画,除了示例中的 TweenAnimationBuilder,完全可以用 AnimatedContainerScaleTransition 或者你喜欢的动画组件进行包裹,然后在 actionWidgetBuilder 里面随意发挥。


六、常见问题

  1. 如何更换删除逻辑?

    • 默认调用了 _deleteItem(item) 来从内部列表中移除项目。如果你不想真正删数据,也可以在自定义按钮的 onTap 里改成别的操作,比如标记为"已禁用",然后自行处理 setState()onItemsChanged
  2. 能否只显示而不带删除或选中?

    • 可以!只要在 actionWidgetBuilder 里返回一个空 SizedBox 或者返回 Container() 不做交互,这样就相当于没有按钮。
    • 如果你想把"删除按钮"从界面中去掉, onDelete 也可以不调用。
  3. 选择与删除能否并存?

    • 可以。默认情况下,你在 onSelectionChanged 里处理选中状态,在 onDelete 里处理删除操作,这俩是独立的逻辑。

七、总结

CustomizableItemGrid 和它的示例 CustomWidgetDemo 为我们提供了一种快速且灵活的网格展示 + 操作按钮组合的解决方案。通过可配置的参数和自定义的操作小部件,你可以轻松地打造出带有删除、选择、打标、动画等多种功能的标签式UI。

如果你正好在做 "标签"、"分类选择"、"动态可增删项目列表"等功能场景,这个组件能让你的开发效率直线飙升。希望本文能给你启发,并帮你节省一部分重复造轮子的时间。

感谢阅读,祝你在 Flutter 的探索之路上越走越远,代码越写越美!

Tip: 喜欢这篇文章?记得收藏并在评论区留下你的想法或改进建议哈。


相关推荐
LawrenceLan1 小时前
Flutter 零基础入门(九):构造函数、命名构造函数与 this 关键字
开发语言·flutter·dart
一豆羹2 小时前
macOS 环境下 ADB 无线调试连接失败、Protocol Fault 及端口占用的深度排查
flutter
行者962 小时前
OpenHarmony上Flutter粒子效果组件的深度适配与实践
flutter·交互·harmonyos·鸿蒙
行者964 小时前
Flutter与OpenHarmony深度集成:数据导出组件的实战优化与性能提升
flutter·harmonyos·鸿蒙
小雨下雨的雨5 小时前
Flutter 框架跨平台鸿蒙开发 —— Row & Column 布局之轴线控制艺术
flutter·华为·交互·harmonyos·鸿蒙系统
小雨下雨的雨5 小时前
Flutter 框架跨平台鸿蒙开发 —— Center 控件之完美居中之道
flutter·ui·华为·harmonyos·鸿蒙
小雨下雨的雨6 小时前
Flutter 框架跨平台鸿蒙开发 —— Icon 控件之图标交互美学
flutter·华为·交互·harmonyos·鸿蒙系统
小雨下雨的雨6 小时前
Flutter 框架跨平台鸿蒙开发 —— Placeholder 控件之布局雏形美学
flutter·ui·华为·harmonyos·鸿蒙系统
行者966 小时前
OpenHarmony Flutter弹出菜单组件深度实践:从基础到高级的完整指南
flutter·harmonyos·鸿蒙
前端不太难7 小时前
Flutter / RN / iOS,在长期维护下的性能差异本质
flutter·ios