Flutter拖拽与放置功能插件super_drag_and_drop的使用
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平台支持从其他应用程序拖放到当前页面,但仅限于在同一浏览器标签页内进行拖动。
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
时设置canAddItemToExistingSession
为true
。
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
子元素的快照。要自定义拖动图像,使用liftBuilder
和dragBuilder
属性。
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
更多关于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列表的顺序
},
),
),
);
}
}
在这个示例中,我们做了以下几件事:
- 设置依赖:在
pubspec.yaml
文件中添加了super_drag_and_drop
依赖。 - 创建主应用:使用
MaterialApp
和Scaffold
创建了一个简单的Flutter应用。 - 构建拖拽与放置界面:使用
DragAndDropBuilder
来构建拖拽项和放置区域。items
:一个包含要拖拽的项的列表。itemBuilder
:用于构建每个拖拽项的Widget。在这个例子中,每个项都是一个Container
,里面包含了一个DraggableItem
。onDrop
:处理放置事件的回调函数。在这个例子中,我们只是打印了一些信息,但你可以在这里添加逻辑来处理放置后的数据更新。
请注意,super_drag_and_drop
插件的具体API可能会有所不同,因此请参考该插件的官方文档和示例代码以获取最新和最准确的信息。如果插件提供了更多的配置选项或回调,你可以根据需要进行调整。