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

前言

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

在这里插入图片描述

插件实现的代码过程解析

第一步等待初始化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顶起来,感谢🙏。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值