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: 喜欢这篇文章?记得收藏并在评论区留下你的想法或改进建议哈。


相关推荐
sunly_3 小时前
Flutter:图片在弹窗外部的UI布局
flutter·ui
getapi4 小时前
Flutter 强制横屏
前端·javascript·flutter
爱学习的大牛12310 小时前
flutter 获取通话记录和通讯录
开发语言·flutter
GeniuswongAir1 天前
Flutter BloC 架构入门指南
flutter·bloc
90后的晨仔1 天前
Flutter 报错 [☠] Network resources (the doctor check crashed)xxxx
前端·flutter
pengyu1 天前
【Flutter 状态管理 - 贰】 | 提升对界面与状态的认知
android·flutter·dart
HuWentao1 天前
你不需要那么多Provider——重新理解状态管理与业务逻辑
前端·flutter
好的佩奇1 天前
Dart 之异步模型
android·flutter·dart
louisgeek2 天前
Flutter StatelessWidget 和 StatefulWidget 的区别
flutter