🎯 Flutter 拖拽选择组件:flutter_drag_selector —— 像选文件一样选择列表项

在 Flutter 开发桌面端应用中,我们经常需要在一个滚动列表(如 ListViewGridViewWrap)中让用户批量选择多个项目。传统的做法是点击每个项目逐一选中,或使用"全选"按钮,但这在项目数量多、布局密集的场景下体验不佳。

flutter_drag_selector 正是为解决这一痛点而生的轻量级 Flutter 插件。它让你像在桌面系统中用鼠标框选文件一样,通过拖拽鼠标(或手指)划出一个区域,自动选中该区域内的所有子组件,大幅提升交互效率。

✨ 核心功能概览

  • 拖拽框选:按住鼠标左键(或触摸屏长按)并拖动,实时绘制一个半透明选择框。
  • 自动识别 :自动检测拖拽区域内所有被 SelectableItem 包裹的子组件,并动态选中/取消选中。
  • 可定制:支持自定义选择区域的样式(颜色、圆角、边框)、选中状态回调、滚动容器控制等。

🛠️ 如何使用?

1. 添加依赖

在你的 pubspec.yaml 文件中添加:

yaml 复制代码
dependencies:
  flutter_drag_selector: ^latest

2. 包装你的列表

将你的滚动容器(如 SingleChildScrollView + Wrap)用 CursorSelectorWidget 包裹,并用 SelectableItem 包裹每一个可选项目。

dart 复制代码
import 'package:flutter/material.dart';
import 'package:flutter_drag_selector/flutter_drag_selector.dart';

class MyApp extends StatefulWidget {
  const MyApp({super.key});
  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final _list = List.generate(50, (i) => i);
  final _controller = StreamController<(Key?, bool)>.broadcast(); // 用于更新 UI 状态
  final scrollController = ScrollController();

  @override
  void initState() {
    super.initState();
  }

  @override
  void dispose() {
    _controller.close();
    scrollController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    Widget buildBox(int index) {
      final id = ValueKey<int>(index);
      return StreamBuilder<(Key?, bool)>(
        stream: _controller.stream.where((e) => e.$1 == id),
        builder: (ctx, snapshot) {
          return SelectableItem(
            key: id,
            child: GestureDetector(
              onTap: () {
                debugPrint('tap -> $index');
              },
              child: Container(
                width: 200,
                height: 200,
                decoration: BoxDecoration(
                  color: index % 2 == 0 ? Colors.yellow : Colors.lightBlueAccent,
                ),
                alignment: Alignment.center,
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Text('$index', style: const TextStyle(fontSize: 20)),
                    const SizedBox(width: 20),
                    Icon(
                      (snapshot.data?.$2 ?? false)
                          ? Icons.check_box
                          : Icons.check_box_outline_blank,
                      size: 40,
                      color: Colors.red,
                    )
                  ],
                ),
              ),
            ),
          );
        },
      );
    }

    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('拖拽选择示例')),
        body: CursorSelectorTheme(
          data: CursorSelectorThemeData(
            selectedAreaDecoration: BoxDecoration(
              color: Colors.blue.withOpacity(0.4),
              borderRadius: BorderRadius.circular(10),
            ),
          ),
          child: CursorSelectorWidget(
            scrollController: scrollController, // 控制滚动
            selectedChangedCallback: (selection) {
              // 选中状态变更回调,用于更新 UI
              _controller.add(selection);
            },
            child: SingleChildScrollView(
              controller: scrollController,
              child: Wrap(
                spacing: 10,
                runSpacing: 10,
                children: _list.map<Widget>(buildBox).toList(),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

3. 效果说明

  • 用户按住鼠标左键(或在移动端长按)并拖动,会看到一个蓝色半透明矩形框跟随鼠标移动。
  • 每次SelectableItem的选中状态变化,都会通过 selectedChangedCallback 回调返回 (Key, isSelected),开发者可据此更新数据模型或 UI。

🎨 自定义主题

你可以通过 CursorSelectorThemeCursorSelectorThemeData 自定义选择框的视觉样式:

dart 复制代码
CursorSelectorTheme(
  data: CursorSelectorThemeData(
    selectedAreaDecoration: BoxDecoration(
      color: Colors.green.withOpacity(0.3),
      border: Border.all(color: Colors.green, width: 2),
      borderRadius: BorderRadius.circular(8),
    )
  ),
  child: CursorSelectorWidget(...),
)

结语

如有设计不佳的点,欢迎指出。

👉 GitHub 地址github.com/bladeofgod/...

👉 Pub 地址pub.dev/packages/fl...

相关推荐
Cobyte1 分钟前
1.基于依赖追踪和触发的响应式系统的本质
前端·javascript·vue.js
主宰者18 分钟前
C# CommunityToolkit.Mvvm全局事件
java·前端·c#
前端小咸鱼一条1 小时前
16.迭代器 和 生成器
开发语言·前端·javascript
小江的记录本1 小时前
【注解】常见 Java 注解系统性知识体系总结(附《全方位对比表》+ 思维导图)
java·前端·spring boot·后端·spring·mybatis·web
web守墓人1 小时前
【前端】记一次将ruoyi vue3 element-plus迁移到arco design vue的经历
前端·vue.js·arco design
伊步沁心1 小时前
Webpack & Vite 深度解析
前端
libokaifa1 小时前
OpenSpec + TDD:让 AI 写代码,用测试兜底
前端·ai编程
用户15815963743701 小时前
搭 AI Agent 团队踩了 18 个坑,总结出这 5 个关键步骤
前端
Kellen1 小时前
Fumadocs 基础概念:从内容源到页面渲染
前端
Lee川1 小时前
前端进阶之路:从性能优化到响应式布局的实战指南(Tailwindcss)
前端·面试