file_drag_and_drop一个Flutter桌面版拖动复制文件插件开源啦

623 阅读3分钟

前言

我的上一篇文章2天摸鱼时间教你实战Flutter 桌面版-Tinypng(熊猫图片压缩)GUI工具基于Flutter Deskstop 实现初版的图片压缩功能,可以支持macOS、以及windows。但是美中不足的是,macOS下依然要点击选择文件去压缩,而不是像Finder一样随意拖动文件。在文末我也是立了Flag要支持,经过一周时间的调研,顺利实现并且开源了此插件file_drag_and_drop。目前仅支持macOS,由于此功能非常依赖原生桌面,我对Windows Visual Studio编程是在是不熟,Flutter接口已经写好,期待有缘人可以贡献。话不多说,基于此插件,我也对我的图片压缩工具macOS版本做了版本更新,效果如下。

20220114_001948.GIF

插件实现的代码过程解析

第一步等待初始化window

由于macOS桌面不像iOS原生可以使用PlatforView. 实际拖动接受文件和iOS差不多,要实现NSView的一个drag协议。 这里用了个取巧的方法,先在flutter端main函数 await一个 initializedMainView初始化方法。我们直接盖一个drop view到 NSWindow上即可。由于用户可能放大缩小窗口,布局就不用frame了,直接用原生约束,也不要SnapKit了,还要导入库,很简单的约束而已。

Flutter代码


void main() async {

WidgetsFlutterBinding.ensureInitialized();

await dragAndDropChannel.initializedMainView();

runApp(GetMaterialApp(

navigatorKey: Get.key,

home: OKToast(

child: MyApp(),

),

));

}

macOS 原生 Swift代码


private var mainWindow: NSWindow {

get {

return (self.registrar.view?.window)!;

}

}

private var mainView: NSView {

get {

return self.registrar.view!

}

}

private func _initializedMainView() {

if (!_initialized) {

_initialized = true

mainView.addSubview(mainDropView)

mainDropView.frame = mainView.bounds

mainDropView.translatesAutoresizingMaskIntoConstraints = false

mainView.addConstraints(

[

NSLayoutConstraint(item: mainDropView, attribute: .leading, relatedBy: .equal, toItem: mainView, attribute: .leading, multiplier: 1, constant: 0),

NSLayoutConstraint(item: mainDropView, attribute: .trailing, relatedBy: .equal, toItem: mainView, attribute: .trailing, multiplier: 1, constant: 0),

NSLayoutConstraint(item: mainDropView, attribute: .top, relatedBy: .equal, toItem: mainView, attribute: .top, multiplier: 1, constant: 0),

NSLayoutConstraint(item: mainDropView, attribute: .bottom, relatedBy: .equal, toItem: mainView, attribute: .bottom, multiplier: 1, constant: 0)

]

)

}

}

第二步实现协议


Swift

protocol FlutterDragContainerDelegate {

func draggingFileEntered()

func draggingFileExit()

func prepareForDragFileOperation()

func performDragFileOperation(_ results : [FileResult])

}

Flutter 添加监听

abstract class DragContainerListener {

void draggingFileEntered() {}

void draggingFileExit() {}

void prepareForDragFileOperation() {}

void performDragFileOperation(List<DragFileResult> fileResults) {}

}

原生几个重要协议方法,通过Channel 转为Flutter的监听

Swift


override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {

if let delegate = self.delegate {

delegate.draggingFileEntered();

}

return NSDragOperation.generic

}

override func draggingExited(_ sender: NSDraggingInfo?) {

if let delegate = self.delegate {

delegate.draggingFileExit();

}

}

override func prepareForDragOperation(_ sender: NSDraggingInfo) -> Bool {

if self.delegate != nil {

self.delegate?.prepareForDragFileOperation()

}

return true

}

override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {

var files = Array<FileResult>()

if let board = sender.draggingPasteboard.propertyList(forType: NSFilenamesPboardType) as? NSArray {

for path in board {

print(path)

if let p = path as? String {

let isDirectory = FlutterFileUtil.isDirectory(p)

let fileExtension = FlutterFileUtil.fileExtension(p)

files.append((path: p,isDirectory: isDirectory, fileExtension: fileExtension))

}

}

}

if self.delegate != nil {

self.delegate?.performDragFileOperation(files)

}

return true

}

Flutter端


ObserverList<DragContainerListener>? _listeners =

ObserverList<DragContainerListener>();

Future<void> _methodCallHandler(MethodCall call) async {

if (_listeners == null) return;

for (final DragContainerListener listener in listeners) {

if (!_listeners!.contains(listener)) {

return;

}

if (call.method != 'onEvent') throw UnimplementedError();

String eventName = call.arguments['eventName'];

Map<String, Function> funcMap = {

kFileDragAndDropEventEntered: listener.draggingFileEntered,

kFileDragAndDropEventExit: listener.draggingFileExit,

kFileDragAndDropEventPrepareDragTask:

listener.prepareForDragFileOperation,

kFileDragAndDropEventPerformDragTask: listener.performDragFileOperation,

};

if (eventName == kFileDragAndDropEventPerformDragTask) {

List fileResult = call.arguments['fileResult'];

var resultList = <DragFileResult>[];

fileResult.forEach((element) { 

var result = DragFileResult.fromJson(element);

resultList.add(result);

});

funcMap[eventName]!(resultList);

} else {

funcMap[eventName]!();

}

}

}

第三步Window Home Page添加监听及处理


@override

void initState() {

super.initState();

dragAndDropChannel.addListener(this);

}

@override

void dispose() {

dragAndDropChannel.removeListener(this);

super.dispose();

}

flutter监听的处理(相当于触发了原生的协议),这里简单做了个遮罩,拖进去显示,退出隐藏。


@override

void draggingFileEntered() {

print("flutter: draggingFileEntered");

setState(() {

visibilityTips = true;

});

}

@override

void draggingFileExit() {

print("flutter: draggingFileExit");

setState(() {

visibilityTips = false;

});

}

@override

void prepareForDragFileOperation() {

print("flutter: prepareForDragFileOperation");

setState(() {

visibilityTips = false;

});

}

@override

void performDragFileOperation(List<DragFileResult> fileResults) {

print("flutter: performDragFileOperation");

checkCanPicker().then((canPicker) {

if (canPicker) {

var collectionFiles = <File>[];

fileResults.forEach((element) {

if (element.isDirectory == false) {

collectionFiles.add(File(element.path));

}

//TODO Also can collect the image file in Directory

});

var chooseFiles = chooseImageFiles(collectionFiles);

if (chooseFiles.isNotEmpty) {

controller.refreshWithFileList(chooseFiles);

}

}

});

}

源码地址

未来研究

此次插件仅实现了macOS从外部拖文件到应用内部,如何从应用内部拖文件去其他地方?由于deskstop版不支持Platform View。这感觉像是变成了一个死循环,还有待研究。另外写作不易,每次写作都耗费了不少时间,如果此文对你有帮助,希望点赞三连,Github也是Star顶起来,感谢🙏。