前言
我的上一篇文章手把手教你实战Flutter 桌面版-Tinypng(熊猫图片压缩)GUI工具基于Flutter Deskstop 实现初版的图片压缩功能,可以支持macOS、以及windows。但是美中不足的是,macOS下依然要点击选择文件去压缩,而不是像Finder一样随意拖动文件。在文末我也是立了Flag要支持,经过一周时间的调研,顺利实现并且开源了此插件file_drag_and_drop。目前仅支持macOS,由于此功能非常依赖原生桌面,我对Windows Visual Studio编程是在是不熟,Flutter接口已经写好,期待有缘人可以贡献。话不多说,基于此插件,我也对我的图片压缩工具macOS版本做了版本更新,效果如下。
c126edd73491463a81c8fa8c941c94e4~tplv-k3u1fbpfcp-watermark.image.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监听的处理(相当于触发了原生的协议),这里简单做了个遮罩,拖进去显示,退出隐藏。
[图片上传失败...(image-b3e241-1659196308997)]
@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顶起来,感谢🙏。
网友评论