Flutter拖拽容器插件draggable_container的使用

发布于 1周前 作者 eggper 来自 Flutter

Flutter拖拽容器插件draggable_container的使用

可拖动子部件,可删除子部件,可以固定子部件位置

每个子部件都可以拖动、删除和固定位置。

截图 / Screenshots

拖拽容器插件示例

模式 / Mode

正常模式 / Normal Mode:

  • 不拦截子部件的手势事件。
  • 不能拖动和删除子部件。

编辑模式 / Edit mode:

  • 长按子部件进入编辑模式。
  • 进入编辑模式后,不再需要长按来拖动子部件,直接拖动就可以了。
  • 在可删除子部件上显示删除按钮。
  • 拦截所有子部件的手势事件。
  • 可以拖动和删除子部件。

DraggableContainer的构造函数参数

  • required List<T extends DraggableItem> items: 必须传入子部件列表。
  • required Widget? NullableItemBuilder<T extends DraggableItem>(BuildContext context, T? item) itemBuilder: 子项widget的构建器。
  • required SliverGridDelegate gridDelegate: 布局依赖于gridDelegate。
  • bool? shrinkWrap: 紧缩水平宽度大小,方便水平居中,默认为false。
  • Widget? NullableItemBuilder<T extends DraggableItem>(BuildContext context, T? item) deleteButtonBuilder: 子项删除按钮的构建器。
  • Widget? NullableItemBuilder<T extends DraggableItem>(BuildContext context, T? item) slotBuilder: 槽位组件的构建器。
  • BoxDecoration? draggingDecoration, default is a shadow style.: 当拖动子项时,包裹在子项外部的样式,默认为阴影效果。
  • Duration? animationDuration, default 200ms.: 子项widget位移的动画时间。
  • bool? tapOutSideExitEditMode, default true.: 当点击了DraggableContainer外部后,退出编辑模式。
  • onChanged(List<T extends DraggableItem> items): 当子项目改变时触发(拖动过后,删除后)。
  • onEditModeChanged(bool mode): mode为true则进入了编辑模式,为false则退出了编辑模式.
  • Future<bool> beforeRemove(T? item, int slotIndex): 删除item的确认事件,返回true删除,返回false不删除。
  • Future<bool> beforeDrop({T? fromItem, int fromSlotIndex, T? toItem, int toSlotIndex}): 将一个item从A点移到B点后的确认事件,返回true为允许放下,返回false不允许放下,会覆盖toItem.fixed属性。

DraggableContainerState的方法

  • getter / setter bool editMode: 读取或设置编辑模式。
  • List<T extends DraggableItem> items: 项目列表。
  • Future<void> addSlot(<T extends DraggableItem>? item): 添加一个新的槽。
  • Future<T extends DraggableItem> removeSlot(int index): 删除一个槽位,返回item。
  • removeItem(<T extends DraggableItem> item): 删除item。
  • removeItemAt(int index): 删除item。
  • replaceItem(int index, <T extends DraggableItem>? item): 替换item。

完整示例代码

import 'package:draggable_container/draggable_container.dart';
import 'package:flutter/material.dart';

import 'data.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'DraggableContainer In ListView',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyItem extends DraggableItem {
  final Color color;
  final int index;
  bool deletable;
  bool fixed;

  MyItem({
    required this.index,
    this.deletable = true,
    this.fixed = false,
    Color color,
  }) : color = color ?? randomColor();

  [@override](/user/override)
  String toString() {
    return '<MyItem> {index:$index, fixed:$fixed, deletable:$deletable}';
  }
}

class AddItem extends DraggableItem {
  [@override](/user/override)
  bool get deletable => false;

  [@override](/user/override)
  bool get fixed => true;

  [@override](/user/override)
  String toString() {
    return '<AddItem> {fixed:$fixed, deletable:$deletable}';
  }
}

class MyHomePage extends StatefulWidget {
  [@override](/user/override)
  _MyHomePage createState() => _MyHomePage();
}

class _MyHomePage extends State<MyHomePage> {
  final data = <DraggableItem>[
    MyItem(index: 0),
    MyItem(index: 1),
    MyItem(index: 2),
    MyItem(index: 3),
    AddItem(),
  ];

  final key = GlobalKey<DraggableContainerState<DraggableItem>>();

  String items = '';
  String delegate = '';
  String settings = '';

  updateText() {
    if (key.currentState != null) {
      Map<String, dynamic> text = {};
      var widget = key.currentWidget as DraggableContainer;
      var _delegate = widget.gridDelegate;
      if (_delegate is SliverGridDelegateWithFixedCrossAxisCount) {
        text['SliverGridDelegateWithFixedCrossAxisCount'] = '';
        text['crossAxisCount'] = _delegate.crossAxisCount;
        text['crossAxisSpacing'] = _delegate.crossAxisSpacing;
        text['mainAxisSpacing'] = _delegate.mainAxisSpacing;
      } else if (_delegate is SliverGridDelegateWithMaxCrossAxisExtent) {
        text['SliverGridDelegateWithMaxCrossAxisExtent'] = '';
        text['maxCrossAxisExtent'] = _delegate.maxCrossAxisExtent;
        text['crossAxisSpacing'] = _delegate.crossAxisSpacing;
        text['mainAxisSpacing'] = _delegate.mainAxisSpacing;
      }
      delegate = mapToString(text);

      text.clear();
      text['tapOutSideExitEditMode'] = key.currentState.tapOutSideExitEditMode;
      text['onChange listener'] = widget.onChanged == null ? 'unset' : 'set';
      text['beforeRemove listener'] =
          widget.beforeRemove == null ? 'unset' : 'set';
      text['beforeDrop listener'] = widget.beforeDrop == null ? 'unset' : 'set';
      settings = mapToString(text);

      items = key.currentState?.items?.join('\n');
    }
  }

  Future<bool> beforeRemove(item, int slotIndex) async {
    item = item as MyItem;
    final res = await showDialog<bool>(
        context: context,
        builder: (_) =>
            AlertDialog(
              title: Text('Remove item ${item.index}?'),
              actions: [
                TextButton(
                    onPressed: () => Navigator.pop(context, false),
                    child: Text('No')),
                ElevatedButton(
                    onPressed: () => Navigator.pop(context, true),
                    child: Text('Yes')),
              ],
            ));
    if (res == true) {
      key.currentState.removeSlot(slotIndex);
    }
    return false;
  }

  String mapToString(Map map) {
    return map.keys.map((key) => '$key: ${map[key]}').join('\n');
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    updateText();
    return Scaffold(
      appBar: AppBar(
        title: Text('DraggableContainer In ListView'),
      ),
      body: ListView(
        children: [
          ListTile(
            title: Text('DraggableContainer settings'),
            subtitle: Text(settings),
          ),
          Divider(height: 1),
          ListTile(
            title: Text('Current SliverGridDelegate'),
            subtitle: Text(delegate),
          ),
          Divider(height: 1),
          Align(
            alignment: Alignment.center,
            child: DraggableContainer<DraggableItem>(
              key: key,
              items: data,
              shrinkWrap: true,
              beforeRemove: beforeRemove,
              draggingDecoration: BoxDecoration(boxShadow: [
                BoxShadow(
                  color: Colors.black.withOpacity(0.3),
                  blurRadius: 5,
                  offset: Offset(0, 5),
                ),
              ]),
              // gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
              //   crossAxisCount: 3,
              //   crossAxisSpacing: 10,
              //   mainAxisSpacing: 10,
              // ),
              gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
                maxCrossAxisExtent: 150,
                crossAxisSpacing: 10,
                mainAxisSpacing: 10,
              ),
              padding: EdgeInsets.all(10),
              onChanged: (List<DraggableItem> items) {
                final addItem = items.firstWhere(
                  (item) => item is AddItem,
                  orElse: () => null,
                );
                // print('has AddItem ${addItem != null}');
                if (items.length < 9 && addItem == null)
                  key.currentState.addSlot(AddItem());
                setState(() {});
              },
              beforeDrop: ({fromItem, fromSlotIndex, toItem, toSlotIndex}) {
                // print('beforeDrop from $fromSlotIndex to $toSlotIndex');
                /// will override the toItem.fixed property
                return Future.value(true);
              },
              itemBuilder: (_, DraggableItem item) {
                if (item is AddItem) {
                  return ElevatedButton(
                    child: Column(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        Text(
                          'Add',
                          style: TextStyle(color: Colors.white),
                        ),
                        Icon(
                          Icons.add,
                          color: Colors.white,
                        ),
                      ],
                    ),
                    onPressed: () {
                      if (key.currentState.slots.length < 9) {
                        key.currentState.insertSlot(
                            0,
                            MyItem(
                              index: key.currentState.slots.length,
                            ));
                      } else {
                        print('replace 8');
                        key.currentState.replaceItem(8, MyItem(index: 99));
                      }
                    },
                  );
                } else if (item is MyItem) {
                  return Material(
                    elevation: 0,
                    borderOnForeground: false,
                    child: Container(
                      color: item.color,
                      child: Column(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          Text(
                            item.index.toString(),
                            style: TextStyle(
                              fontSize: 22,
                              color: Colors.white,
                              shadows: [
                                BoxShadow(color: Colors.black, blurRadius: 5),
                              ],
                            ),
                          ),
                          SizedBox(height: 5),
                          ElevatedButton.icon(
                            icon: Icon(item.fixed
                                ? Icons.lock_outline
                                : Icons.lock_open),
                            label: Text(item.fixed ? 'Unlock' : 'Lock'),
                            onPressed: () {
                              item.fixed = !item.fixed;
                              setState(() {});
                            },
                          ),
                        ],
                      ),
                    ),
                  );
                }
                return null;
              },
            ),
          ),
          Divider(height: 1),
          ListTile(
            title: Text('Current Items'),
            subtitle: Text(items),
          ),
        ],
      ),
    );
  }
}

更多关于Flutter拖拽容器插件draggable_container的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter拖拽容器插件draggable_container的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,下面是一个关于如何在Flutter中使用draggable_container插件来实现拖拽功能的代码案例。draggable_container插件允许你创建一个可以拖拽的容器。

首先,确保你的pubspec.yaml文件中已经添加了draggable_container依赖:

dependencies:
  flutter:
    sdk: flutter
  draggable_container: ^x.y.z  # 替换为最新的版本号

然后,运行flutter pub get来安装依赖。

以下是一个简单的示例代码,展示如何使用draggable_container插件:

import 'package:flutter/material.dart';
import 'package:draggable_container/draggable_container.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Draggable Container Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: DraggableContainerDemo(),
    );
  }
}

class DraggableContainerDemo extends StatefulWidget {
  @override
  _DraggableContainerDemoState createState() => _DraggableContainerDemoState();
}

class _DraggableContainerDemoState extends State<DraggableContainerDemo> {
  Offset? _initialOffset;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Draggable Container Demo'),
      ),
      body: Center(
        child: DraggableContainer(
          onDragStarted: (details) {
            // 当拖拽开始时记录初始位置
            _initialOffset = details.globalPosition;
          },
          onDragEnded: (details) {
            // 当拖拽结束时,可以在这里处理逻辑,比如更新状态
            print('Drag ended at: ${details.globalPosition}');
          },
          child: Container(
            width: 200,
            height: 200,
            color: Colors.amber,
            child: Center(
              child: Text(
                'Drag me!',
                style: TextStyle(fontSize: 24, color: Colors.black),
              ),
            ),
          ),
          // 可选参数,设置拖拽的边界
          constraints: BoxConstraints(
            minWidth: 100,
            minHeight: 100,
            maxWidth: 300,
            maxHeight: 300,
          ),
          // 可选参数,设置拖拽的反馈效果
          feedback: Container(
            width: 210,
            height: 210,
            color: Colors.amber.withOpacity(0.8),
            child: Center(
              child: Text(
                'Dragging...',
                style: TextStyle(fontSize: 24, color: Colors.black),
              ),
            ),
          ),
          // 可选参数,设置拖拽时的子widget的阴影
          childDecoration: BoxDecoration(
            boxShadow: [
              BoxShadow(
                color: Colors.black.withOpacity(0.3),
                spreadRadius: 5,
                blurRadius: 7,
                offset: Offset(0, 3), // changes position of shadow
              ),
            ],
          ),
        ),
      ),
    );
  }
}

在这个示例中:

  1. DraggableContainer是主要的拖拽容器。
  2. onDragStarted回调在拖拽开始时触发,可以用来记录初始位置。
  3. onDragEnded回调在拖拽结束时触发,可以用来处理逻辑。
  4. constraints参数用于设置拖拽容器的边界。
  5. feedback参数用于定义拖拽时的反馈效果,比如放大或改变颜色。
  6. childDecoration参数用于设置拖拽时子widget的装饰,比如阴影效果。

这个代码示例展示了如何使用draggable_container插件来实现基本的拖拽功能,你可以根据需求进一步自定义和扩展。

回到顶部