Flutter拖拽与放置功能插件super_drag_and_drop的使用

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

Flutter拖拽与放置功能插件super_drag_and_drop的使用

Native Drag and Drop for Flutter

super_drag_and_drop 是一个用于Flutter应用的拖放功能插件,它允许开发者轻松地在不同平台上实现拖放操作。以下是该插件的主要特性:

Features

  • Native Drag and Drop functionality:提供原生的拖放功能。
  • 跨平台支持:支持macOS, iOS, Android, Windows, Linux和Web(*)。
  • 平台无关代码:可以编写与平台无关的代码来处理常见的格式。
  • 自定义数据格式支持:支持自定义的数据格式。
  • 多指拖动(iOS):可以在iOS上添加项目到现有的拖动会话中。
  • 虚拟文件拖放:支持在macOS, iOS和Windows上拖放虚拟文件。

(*) Web平台支持从其他应用程序拖放到当前页面,但仅限于在同一浏览器标签页内进行拖动。

Drag Drop Example

Getting started

super_drag_and_drop 使用Rust内部实现低级别的平台特定功能。如果未安装Rust,插件将自动下载预编译的二进制文件以适应目标平台。如果您希望从源代码编译Rust代码,可以通过rustup 安装Rust。存在rustup时,构建过程会自动检测并使用它。

对于macOS或Linux,在终端执行以下命令:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

对于Windows,您可以使用Rust Installer

如果您已经安装了Rust,请确保更新至最新版本:

rustup update

这之后,构建集成将自动安装所需的Rust目标和其他依赖项(如NDK)。首次构建可能会花费一些时间。

Android support

在Android上使用super_drag_and_drop需要NDK。如果不存在,将在首次构建时自动安装。NDK版本在Flutter项目的android/app/build.gradle中指定。

android {
    // 默认情况下,项目使用来自flutter插件的NDK版本。
    ndkVersion flutter.ndkVersion
}

如果您有较旧的Flutter Android项目,则需要在android/app/build.gradle中指定合理的最小SDK版本:

android {
    defaultConfig {
        minSdkVersion 23
    }
}

要能够从您的应用程序中拖动图像和其他自定义数据,您需要在AndroidManifest.xml中声明一个内容提供程序:

<manifest>
    <application>
        ...
        <provider
            android:name="com.superlist.super_native_extensions.DataProvider"
            android:authorities="<your-package-name>.SuperClipboardDataProvider"
            android:exported="true"
            android:grantUriPermissions="true" >
        </provider>
        ...
    </application>
</manifest>

请用实际包名替换<your-package-name>。注意,这是super_clipboard使用的相同内容提供程序。如果同时使用这两个包,只需这样做一次。

Usage

拖动来自应用程序的内容

下面是一个简单的例子,演示如何创建一个可拖动的小部件:

class MyDraggableWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return DragItemWidget(
      dragItemProvider: (request) async {
        final item = DragItem(
          localData: {'x': 3, 'y': 4},
        );
        item.add(Formats.plainText('Plain Text Data'));
        item.add(
            Formats.htmlText.lazy(() => '<b>HTML generated on demand</b>'));
        return item;
      },
      allowedOperations: () => [DropOperation.copy],
      child: const DraggableWidget(
        child: Text('This widget is draggable'),
      ),
    );
  }
}

接收被拖动的项目

接收拖动项目的代码如下所示:

class MyDropRegion extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return DropRegion(
      formats: Formats.standardFormats,
      hitTestBehavior: HitTestBehavior.opaque,
      onDropOver: (event) {
        final item = event.session.items.first;
        if (item.localData is Map) {
          // 这是在应用程序内部拖动,并设置了自定义本地数据。
        }
        if (item.canProvide(Formats.plainText)) {
          // 此项目包含纯文本。
        }
        if (event.session.allowedOperations.contains(DropOperation.copy)) {
          return DropOperation.copy;
        } else {
          return DropOperation.none;
        }
      },
      onDropEnter: (event) {
        // 当区域首次接受拖动时调用此方法。
      },
      onDropLeave: (event) {
        // 当拖动离开区域时调用此方法。
      },
      onPerformDrop: (event) async {
        final item = event.session.items.first;
        final reader = item.dataReader!;
        if (reader.canProvide(Formats.plainText)) {
          reader.getValue<String>(Formats.plainText, (value) {
            if (value != null) {
              print('Dropped text: ${value}');
            }
          }, onError: (error) {
            print('Error reading value $error');
          });
        }

        if (reader.canProvide(Formats.png)) {
          reader.getFile(Formats.png, (file) {
            final stream = file.getStream();
            // 可以在这里使用流读取文件内容。
          }, onError: (error) {
            print('Error reading value $error');
          });
        }
      },
      child: const Padding(
        padding: EdgeInsets.all(15.0),
        child: Text('Drop items here'),
      ),
    );
  }
}

数据格式

有关提供的和接收的拖动数据格式,super_drag_and_drop基于super_clipboard构建。更多关于数据格式的信息,请参阅super_clipboard文档

Advanced usage

拖动虚拟文件

虚拟文件是那些在拖动时刻尚未物理存在的文件。当放下时,应用程序会收到通知并开始生成文件内容。这对于拖动显示在应用程序中的内容但实际上存在于远程位置(例如云)上的内容很有用。

final item = DragItem();
item.addVirtualFile(
  format: Formats.plainTextFile,
  provider: (sinkProvider, progress) {
    final line = utf8.encode('Line in virtual file\n');
    const lines = 10;
    final sink = sinkProvider(fileSize: line.length * lines);
    for (var i = 0; i < lines; ++i) {
      sink.add(line);
    }
    sink.close();
  },
);

在iOS上拖动多个项目

要在iOS上启用拖动多个项目,创建DragItemWidget时设置canAddItemToExistingSessiontrue

return DragItemWidget(
  allowedOperations: () => [DropOperation.copy],
  canAddItemToExistingSession: true,
  dragItemProvider: (request) async {
    if (await request.session.hasLocalData('image-item')) {
      return null;
    }
    final item = DragItem(
      localData: 'image-item',
      suggestedName: 'Green.png',
    );
    item.add(Formats.png(await createImageData(Colors.green)));
    return item;
  }
);

接收虚拟文件

接收虚拟文件不需要特殊处理。您可以像使用任何其他流一样消费虚拟文件的内容:

reader.getFile(Formats.png, (file) {
  final Stream<Uint8List> stream = file.getStream();
  // 现在可以使用流读取文件内容。
});

自定义拖动图像

默认的拖动图像是DragItemWidget子元素的快照。要自定义拖动图像,使用liftBuilderdragBuilder属性。

DragItemWidget(
  liftBuilder: (context, child) {
    return Container(color: Colors.blue, child: child);
  },
  dragBuilder: (context, child) {
    return Container(color: Colors.red, child: child);
  },
  ...
);

示例Demo

下面是一个完整的示例,展示了如何在Flutter应用程序中使用super_drag_and_drop插件:

import 'dart:convert';
import 'dart:ui' as ui;

import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:super_drag_and_drop/super_drag_and_drop.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        snackBarTheme: const SnackBarThemeData(
          behavior: SnackBarBehavior.floating,
        ),
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Super Drag and Drop Example'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class DragableWidget extends StatefulWidget {
  const DragableWidget({
    super.key,
    required this.name,
    required this.color,
    required this.dragItemProvider,
  });

  final String name;
  final Color color;
  final DragItemProvider dragItemProvider;

  @override
  State<DragableWidget> createState() => _DragableWidgetState();
}

class _DragableWidgetState extends State<DragableWidget> {
  bool _dragging = false;

  Future<DragItem?> provideDragItem(DragItemRequest request) async {
    final item = await widget.dragItemProvider(request);
    if (item != null) {
      void updateDraggingState() {
        setState(() {
          _dragging = request.session.dragging.value;
        });
      }

      request.session.dragging.addListener(updateDraggingState);
      updateDraggingState();
    }
    return item;
  }

  @override
  Widget build(BuildContext context) {
    return DragItemWidget(
      allowedOperations: () => [DropOperation.copy],
      canAddItemToExistingSession: true,
      dragItemProvider: provideDragItem,
      child: DraggableWidget(
        child: AnimatedOpacity(
          opacity: _dragging ? 0.5 : 1,
          duration: const Duration(milliseconds: 200),
          child: Container(
            decoration: BoxDecoration(
              color: widget.color,
              borderRadius: BorderRadius.circular(14),
            ),
            padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
            child: Text(
              widget.name,
              style: const TextStyle(fontSize: 20, color: Colors.white),
              textAlign: TextAlign.center,
            ),
          ),
        ),
      ),
    );
  }
}

Future<Uint8List> createImageData(Color color) async {
  final recorder = ui.PictureRecorder();
  final canvas = Canvas(recorder);
  final paint = Paint()..color = color;
  canvas.drawOval(const Rect.fromLTWH(0, 0, 200, 200), paint);
  final picture = recorder.endRecording();
  final image = await picture.toImage(200, 200);
  final data = await image.toByteData(format: ui.ImageByteFormat.png);
  return data!.buffer.asUint8List();
}

class HomeLayout extends StatelessWidget {
  const HomeLayout({
    super.key,
    required this.draggable,
    required this.dropZone,
  });

  final List<Widget> draggable;
  final Widget dropZone;

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: LayoutBuilder(builder: (context, constraints) {
        if (constraints.maxWidth < 500) {
          return Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              Container(
                padding: const EdgeInsets.all(16),
                child: Wrap(
                  direction: Axis.horizontal,
                  runSpacing: 8,
                  spacing: 10,
                  children: draggable,
                ),
              ),
              Expanded(
                child: Padding(
                  padding: const EdgeInsets.all(16.0).copyWith(top: 0),
                  child: dropZone,
                ),
              ),
            ],
          );
        } else {
          return Row(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            textDirection: TextDirection.rtl,
            children: [
              SingleChildScrollView(
                padding: const EdgeInsets.all(16),
                child: IntrinsicWidth(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.stretch,
                    children: draggable
                        .intersperse(
                          const SizedBox(height: 10),
                        )
                        .toList(growable: false),
                  ),
                ),
              ),
              Expanded(
                child: Padding(
                  padding: const EdgeInsets.all(16.0).copyWith(right: 0),
                  child: dropZone,
                ),
              ),
            ],
          );
        }
      }),
    );
  }
}

extension on DragSession {
  Future<bool> hasLocalData(Object data) async {
    final localData = await getLocalData() ?? [];
    return localData.contains(data);
  }
}

class _MyHomePageState extends State<MyHomePage> {
  void showMessage(String message) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(message),
        duration: const Duration(milliseconds: 1500),
      ),
    );
  }

  Future<DragItem?> textDragItem(DragItemRequest request) async {
    if (await request.session.hasLocalData('text-item')) {
      return null;
    }
    final item = DragItem(
      localData: 'text-item',
      suggestedName: 'PlainText.txt',
    );
    item.add(Formats.plainText('Plain Text Value'));
    return item;
  }

  Future<DragItem?> imageDragItem(DragItemRequest request) async {
    if (await request.session.hasLocalData('image-item')) {
      return null;
    }
    final item = DragItem(
      localData: 'image-item',
      suggestedName: 'Green.png',
    );
    item.add(Formats.png(await createImageData(Colors.green)));
    return item;
  }

  Future<DragItem?> lazyImageDragItem(DragItemRequest request) async {
    if (await request.session.hasLocalData('lazy-image-item')) {
      return null;
    }
    final item = DragItem(
      localData: 'lazy-image-item',
      suggestedName: 'LazyBlue.png',
    );
    item.add(Formats.png.lazy(() async {
      showMessage('Requested lazy image.');
      return await createImageData(Colors.blue);
    }));
    return item;
  }

  Future<DragItem?> virtualFileDragItem(DragItemRequest request) async {
    if (await request.session.hasLocalData('virtual-file-item')) {
      return null;
    }
    final item = DragItem(
      localData: 'virtual-file-item',
      suggestedName: 'VirtualFile.txt',
    );
    if (!item.virtualFileSupported) {
      return null;
    }
    item.addVirtualFile(
      format: Formats.plainTextFile,
      provider: (sinkProvider, progress) {
        showMessage('Requesting virtual file content.');
        final line = utf8.encode('Line in virtual file\n');
        const lines = 10;
        final sink = sinkProvider(fileSize: line.length * lines);
        for (var i = 0; i < lines; ++i) {
          sink.add(line);
        }
        sink.close();
      },
    );
    return item;
  }

  Future<DragItem?> multipleRepresentationsDragItem(
      DragItemRequest request) async {
    if (await request.session.hasLocalData('multiple-representations-item')) {
      return null;
    }
    final item = DragItem(
      localData: 'multiple-representations-item',
    );
    item.add(Formats.png(await createImageData(Colors.pink)));
    item.add(Formats.plainText("Hello World"));
    item.add(Formats.uri(
        NamedUri(Uri.parse('https://flutter.dev'), name: 'Flutter')));
    return item;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: HomeLayout(
        draggable: [
          DragableWidget(
            name: 'Text',
            color: Colors.red,
            dragItemProvider: textDragItem,
          ),
          DragableWidget(
            name: 'Image',
            color: Colors.green,
            dragItemProvider: imageDragItem,
          ),
          DragableWidget(
            name: 'Image 2',
            color: Colors.blue,
            dragItemProvider: lazyImageDragItem,
          ),
          DragableWidget(
            name: 'Virtual',
            color: Colors.amber.shade700,
            dragItemProvider: virtualFileDragItem,
          ),
          DragableWidget(
            name: 'Multiple',
            color: Colors.pink,
            dragItemProvider: multipleRepresentationsDragItem,
          ),
        ],
        dropZone: Container(
          decoration: BoxDecoration(
            border: Border.all(color: Colors.blueGrey.shade200),
            borderRadius: BorderRadius.circular(14),
          ),
          child: _DropZone(),
        ),
      ),
    );
  }
}

class _DropZone extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _DropZoneState();
}

class _DropZoneState extends State<_DropZone> {
  @override
  Widget build(BuildContext context) {
    return DropRegion(
      formats: const [
        ...Formats.standardFormats,
        formatCustom,
      ],
      hitTestBehavior: HitTestBehavior.opaque,
      onDropOver: _onDropOver,
      onPerformDrop: _onPerformDrop,
      onDropLeave: _onDropLeave,
      child: Stack(
        children: [
          Positioned.fill(child: _content),
          Positioned.fill(
            child: IgnorePointer(
              child: AnimatedOpacity(
                opacity: _isDragOver ? 1.0 : 0.0,
                duration: const Duration(milliseconds: 200),
                child: _preview,
              ),
            ),
          ),
        ],
      ),
    );
  }

  DropOperation _onDropOver(DropOverEvent event) {
    setState(() {
      _isDragOver = true;
      _preview = Container(
        decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(13),
          color: Colors.black.withOpacity(0.2),
        ),
        child: Padding(
          padding: const EdgeInsets.all(50),
          child: Center(
            child: ConstrainedBox(
              constraints: const BoxConstraints(maxWidth: 400),
              child: ClipRRect(
                borderRadius: BorderRadius.circular(10),
                child: ListView(
                  shrinkWrap: true,
                  children: event.session.items
                      .map<Widget>((e) => _DropItemInfo(dropItem: e))
                      .intersperse(Container(
                        height: 2,
                        color: Colors.white.withOpacity(0.7),
                      ))
                      .toList(growable: false),
                ),
              ),
            ),
          ),
        ),
      );
    });
    return event.session.allowedOperations.firstOrNull ?? DropOperation.none;
  }

  Future<void> _onPerformDrop(PerformDropEvent event) async {
    final readers = await Future.wait(
      event.session.items.map(
        (e) => ReaderInfo.fromReader(
          e.dataReader!,
          localData: e.localData,
        ),
      ),
    );

    if (!mounted) {
      return;
    }

    buildWidgetsForReaders(context, readers, (value) {
      setState(() {
        final delegate = SliverChildListDelegate(value
            .intersperse(const SizedBox(height: 10))
            .toList(growable: false));
        _content = CustomScrollView(
          slivers: [
            SliverPadding(
              padding: const EdgeInsets.all(10),
              sliver: SuperSliverList(delegate: delegate),
            )
          ],
        );
      });
    });
  }

  void _onDropLeave(DropEvent event) {
    setState(() {
      _isDragOver = false;
    });
  }

  bool _isDragOver = false;

  Widget _preview = const SizedBox();
  Widget _content = const Center(
    child: Text(
      'Drop here',
      style: TextStyle(
        color: Colors.grey,
        fontSize: 16,
      ),
    ),
  );
}

class _DropItemInfo extends StatelessWidget {
  const _DropItemInfo({
    required this.dropItem,
  });

  final DropItem dropItem;

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.white,
      padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 10),
      child: DefaultTextStyle.merge(
        style: const TextStyle(fontSize: 11.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            if (dropItem.localData != null)
              Text.rich(TextSpan(children: [
                const TextSpan(
                  text: 'Local data: ',
                  style: TextStyle(fontWeight: FontWeight.bold),
                ),
                TextSpan(text: '${dropItem.localData}'),
              ])),
            const SizedBox(
              height: 4,
            ),
            Text.rich(TextSpan(children: [
              const TextSpan(
                text: 'Native formats: ',
                style: TextStyle(fontWeight: FontWeight.bold),
              ),
              TextSpan(text: dropItem.platformFormats.join(', ')),
            ])),
          ],
        ),
      ),
    );
  }
}

通过以上内容,您可以了解如何在Flutter应用程序中使用super_drag_and_drop插件实现拖放功能。希望这些信息对您有所帮助!


更多关于Flutter拖拽与放置功能插件super_drag_and_drop的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter拖拽与放置功能插件super_drag_and_drop的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,下面是一个关于如何使用Flutter中的super_drag_and_drop插件来实现拖拽与放置功能的代码示例。这个示例将展示如何设置一个简单的拖拽与放置界面。

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

dependencies:
  flutter:
    sdk: flutter
  super_drag_and_drop: ^最新版本号  # 请替换为实际的最新版本号

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

接下来,编写一个简单的Flutter应用,展示如何使用super_drag_and_drop插件:

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

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Drag and Drop Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: DragDropDemo(),
    );
  }
}

class DragDropDemo extends StatefulWidget {
  @override
  _DragDropDemoState createState() => _DragDropDemoState();
}

class _DragDropDemoState extends State<DragDropDemo> {
  final List<String> items = ['Item 1', 'Item 2', 'Item 3'];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Drag and Drop Demo'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: DragAndDropBuilder(
          items: items,
          itemBuilder: (context, index, data, dragState) {
            return Container(
              margin: EdgeInsets.all(8.0),
              decoration: BoxDecoration(
                border: Border.all(color: Colors.grey),
                borderRadius: BorderRadius.circular(8),
                color: dragState == DragState.dragging
                    ? Colors.lightBlueAccent
                    : Colors.white,
              ),
              padding: EdgeInsets.all(16.0),
              child: DraggableItem(
                data: data,
                onDragStarted: () {
                  print('$data is being dragged');
                },
                onDragEnded: (details) {
                  print('$data was dropped at ${details.offset}');
                },
                child: Text(data),
              ),
            );
          },
          onDrop: (context, draggedData, targetData, details) {
            print('$draggedData was dropped on $targetData');
            // 在这里处理放置逻辑,例如更新items列表的顺序
          },
        ),
      ),
    );
  }
}

在这个示例中,我们做了以下几件事:

  1. 设置依赖:在pubspec.yaml文件中添加了super_drag_and_drop依赖。
  2. 创建主应用:使用MaterialAppScaffold创建了一个简单的Flutter应用。
  3. 构建拖拽与放置界面:使用DragAndDropBuilder来构建拖拽项和放置区域。
    • items:一个包含要拖拽的项的列表。
    • itemBuilder:用于构建每个拖拽项的Widget。在这个例子中,每个项都是一个Container,里面包含了一个DraggableItem
    • onDrop:处理放置事件的回调函数。在这个例子中,我们只是打印了一些信息,但你可以在这里添加逻辑来处理放置后的数据更新。

请注意,super_drag_and_drop插件的具体API可能会有所不同,因此请参考该插件的官方文档和示例代码以获取最新和最准确的信息。如果插件提供了更多的配置选项或回调,你可以根据需要进行调整。

回到顶部