Flutter开发模仿百度云盘创建文件夹功能Draggable和DragTarget的混合使用

使用LongPressDraggableDragTarget写了个类似于百度云盘管理文件和文件夹的功能(为了避免和列表的滑动手势冲突,所以采用LongPressDraggable而不是Draggable):

1、拖拽文件到文件夹中
2、拖拽两个文件可以合并成一个新的文件夹

效果如下:

实现效果

1、文件夹可以拖拽到另外一个文件夹中去
2、文件夹不可以拖拽到设备中去
3、设备可以拖拽到文件夹中去
4、两个设备可以合并成一个新的文件夹

使用到的三方 get: ^4.6.6

gif.gif

代码展示(代码注释都写的比较清楚,如果有不懂的可以在下方留言)

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

class DraggableListView extends StatefulWidget {
  const DraggableListView({super.key});

  @override
  State<DraggableListView> createState() => _DraggableListViewState();
}

class _DraggableListViewState extends State<DraggableListView> {
  final ScrollController _scrollController = ScrollController();
  final TextEditingController _nameController = TextEditingController();
  final List<Map<String, dynamic>> _gatherList = [
    {'label': '顺义区'},
    {'label': '朝阳区'},
    {'label': '通州区'},
    {'label': '密云区'},
    {'label': '海淀区'},
  ];
  final List<Map<String, dynamic>> _deviceList = [
    {'label': '设备---1'},
    {'label': '设备---2'},
    {'label': '设备---3'},
    {'label': '设备---4'},
    {'label': '设备---5'},
    {'label': '设备---6'},
    {'label': '设备---7'},
    {'label': '设备---8'},
    {'label': '设备---9'},
    {'label': '设备---10'},
    {'label': '设备---11'},
  ];

  ///当前拖拽的cell的index
  int dragIndex = 0;

  ///判断拖拽的是文件夹还是设备
  bool isDragFile = false;
  @override
  void initState() {
    super.initState();
  }

  @override
  void dispose() {
    super.dispose();
    _scrollController.dispose();
    _nameController.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('DraggableListView'),
      ),
      body: _buildBody(),
    );
  }

  Widget _buildBody() {
    Color bgColor = Colors.black38;
    return Column(
      children: [
        Expanded(
          child: ListView.builder(
            controller: _scrollController,
            itemCount: _deviceList.length + _gatherList.length,
            itemExtent: cellHeight,
            itemBuilder: (context, index) {
              ///文件夹列表
              if (index < _gatherList.length) {
                return Container(
                  padding: const EdgeInsets.symmetric(
                    horizontal: 10.0,
                    vertical: 5.0,
                  ),
                  child: LongPressDraggable(
                    data: index,
                    //拖拽的文件夹内容展示
                    feedback: _buildFeedbackContainer(
                      index: index,
                      isFile: true,
                    ),
                    onDragStarted: () {
                      dragIndex = index;
                      isDragFile = true;
                    },
                    //被拖拽的文件夹cell在列表中的展示
                    childWhenDragging: _buildContainerWhenDragging(),
                    onDragUpdate: (details) {
                      // 拖拽让列表上下滚动
                      _scrollListView(details);
                    },
                    child: DragTarget<int>(
                      onAccept: (int data) {
                        if (!isDragFile) {
                          ///
                          Get.snackbar("提示",
                              "${_deviceList[data]}被移动到${_gatherList[index]}中去了");

                          ///如果拖拽的是设备放到文件夹上,就移除设备
                          _deviceList.removeAt(data);
                        } else {
                          ///如果拖拽的是文件夹,当拖拽的文件夹和被拖拽的文件夹不是一个的时候,合并文件夹
                          if (dragIndex != index) {
                            ///
                            Get.snackbar("提示",
                                "${_gatherList[data]}被移动到${_gatherList[index]}中去了");

                            ///如果拖拽的是文件夹放到文件夹上,就移除文件夹
                            _gatherList.removeAt(data);
                          }
                        }
                        setState(() {});
                      },
                      onWillAccept: (data) {
                        if (isDragFile) {
                          ///当拖拽的是某个文件夹的时候,如果拖拽的文件夹放到被拖拽的文件夹上面的时候,不改变原来文件夹的状态(背景色)
                          if (dragIndex != index) {
                            bgColor = Colors.red;
                          }
                        } else {
                          bgColor = Colors.red;
                        }
                        return data != null;
                      },
                      onLeave: (data) {
                        bgColor = bgColor;
                        setState(() {});
                      },
                      builder: (context, candidateData, rejectedData) {
                        ///文件夹的cell展示
                        return Container(
                          alignment: Alignment.center,
                          decoration: BoxDecoration(
                            color: bgColor,
                            borderRadius: const BorderRadius.all(
                              Radius.circular(18.0),
                            ),
                          ),
                          child: _buildGatherCell(index),
                        );
                      },
                    ),
                  ),
                );
              }

              ///设备列表
              return Container(
                margin: const EdgeInsets.symmetric(
                  horizontal: 10.0,
                  vertical: 5.0,
                ),
                child: LongPressDraggable(
                  data: index - _gatherList.length,
                  //拖拽的设备内容展示
                  feedback: _buildFeedbackContainer(
                    index: index,
                    isFile: false,
                  ),
                  //被拖拽的设备cell在列表中的展示
                  childWhenDragging: _buildContainerWhenDragging(),
                  onDragStarted: () {
                    isDragFile = false;
                    dragIndex = index - _gatherList.length;
                  },
                  onDragUpdate: (details) {
                    // 拖拽让列表上下滚动
                    _scrollListView(details);
                  },
                  child: DragTarget<int>(
                    onAccept: (int data) {
                      ///拖拽设备放到设备上进行合并+创建新的文件夹
                      ///如果是把文件夹拖拽到设备上不做任何操作
                      if (!isDragFile) {
                        _mergeDevice(data: data, index: index);
                      }
                    },
                    onWillAccept: (data) {
                      if (!isDragFile) {
                        if (dragIndex != (index - _gatherList.length)) {
                          bgColor = Colors.red;
                        }
                      }
                      return data != null;
                    },
                    onLeave: (data) {
                      bgColor = bgColor;
                      setState(() {});
                    },
                    builder: (context, candidateData, rejectedData) {
                      return Container(
                        alignment: Alignment.center,
                        color: bgColor,
                        child: _buildDeviceCell(index),
                      );
                    },
                  ),
                ),
              );
            },
          ),
        ),
      ],
    );
  }

  ///创建文件夹的cell
  Widget _buildGatherCell(int index) {
    return Row(
      children: [
        const SizedBox(width: 50.0),
        Expanded(
          child: Align(
            alignment: Alignment.centerLeft,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(
                  "${_gatherList[index]["label"]}",
                  style: const TextStyle(
                    color: Colors.white,
                    fontSize: 16.0,
                  ),
                ),
              ],
            ),
          ),
        ),
        const Icon(
          Icons.arrow_forward_ios,
          color: Colors.white,
        ),
        const SizedBox(width: 10.0),
      ],
    );
  }

  ///创建设备的cell
  Widget _buildDeviceCell(int index) {
    return Row(
      children: [
        const SizedBox(width: 50.0),
        Expanded(
          child: Align(
            alignment: Alignment.centerLeft,
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(
                  "${_deviceList[index - _gatherList.length]["label"]}",
                  style: const TextStyle(
                    color: Colors.white,
                    fontSize: 16.0,
                    fontWeight: FontWeight.w500,
                  ),
                ),
              ],
            ),
          ),
        ),
      ],
    );
  }

  ///合并两个设备-创建新的文件夹
  _mergeDevice({
    required int data,
    required int index,
  }) {
    Get.defaultDialog(
      title: "新建集合",
      titlePadding: const EdgeInsets.symmetric(vertical: 16.0),
      titleStyle: const TextStyle(
        color: Colors.white,
        fontWeight: FontWeight.w400,
        fontSize: 16.0,
      ),
      backgroundColor: const Color.fromRGBO(25, 29, 39, 1),
      barrierDismissible: false,
      content: Column(
        children: [
          Container(
            height: 44.0,
            padding: const EdgeInsets.symmetric(horizontal: 10.0),
            margin: const EdgeInsets.symmetric(horizontal: 16.0),
            decoration: BoxDecoration(
              border: Border.all(
                color: const Color.fromRGBO(43, 82, 255, 1),
              ),
              borderRadius: BorderRadius.circular(8.0),
            ),
            alignment: Alignment.center,
            child: TextField(
              controller: _nameController,
              style: const TextStyle(color: Colors.white),
              decoration: const InputDecoration(
                border: InputBorder.none,
                enabledBorder: InputBorder.none,
                hintText: "新建集合",
                hintStyle: TextStyle(
                  color: Color.fromRGBO(255, 255, 255, 0.45),
                  fontSize: 16.0,
                ),
                isCollapsed: true,
                // 输入框内容上下居中
                contentPadding: EdgeInsets.only(top: 0, bottom: 0),
              ),
            ),
          ),
          const SizedBox(height: 20.0),
          Container(
            padding: const EdgeInsets.symmetric(horizontal: 16.0),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Container(
                  width: 105.0,
                  height: 44.0,
                  decoration: BoxDecoration(
                    color: const Color.fromRGBO(2, 3, 6, 1),
                    borderRadius: BorderRadius.circular(8.0),
                  ),
                  child: TextButton(
                    onPressed: () {
                      _nameController.clear();
                      setState(() {});
                      Get.back();
                    },
                    child: const Text(
                      "取消",
                      style: TextStyle(
                        color: Colors.white,
                        fontSize: 16.0,
                        fontWeight: FontWeight.w400,
                      ),
                    ),
                  ),
                ),
                Container(
                  width: 105.0,
                  height: 44.0,
                  decoration: BoxDecoration(
                    color: const Color.fromRGBO(43, 82, 255, 1),
                    borderRadius: BorderRadius.circular(8.0),
                  ),
                  child: TextButton(
                    onPressed: () {
                      if (_nameController.text.isEmpty) {
                        Get.snackbar("提示", "请输入名称!");
                        return;
                      }
                      for (var item in _gatherList) {
                        if (item["label"] == _nameController.text) {
                          Get.snackbar("提示", "该名称已存在,请重新输入!");
                          return;
                        }
                      }
                      var array = [
                        _deviceList[data],
                        _deviceList[index - _gatherList.length]
                      ];
                      _deviceList
                          .removeWhere((element) => array.contains(element));

                      ///删除设备之后再创建文件夹
                      _gatherList.add(
                        {'label': _nameController.text},
                      );
                      var fileName = _nameController.text;
                      _nameController.clear();
                      setState(() {});
                      Get.back();
                      Get.snackbar(
                          "提示", "${array[0]}和${array[1]}已合并到文件夹${fileName}中");
                    },
                    child: const Text(
                      "确定",
                      style: TextStyle(
                        color: Colors.white,
                        fontSize: 16.0,
                        fontWeight: FontWeight.w400,
                      ),
                    ),
                  ),
                ),
              ],
            ),
          )
        ],
      ),
    );
  }

  ///拖拽的时候上下滚动列表
  _scrollListView(DragUpdateDetails details) {
    if (details.globalPosition.dy < 200) {
      if (_scrollController.offset <= 0.0) return;
      // 执行向下滚动操作
      _scrollController.jumpTo(_scrollController.offset - 5);
    }

    if (details.globalPosition.dy > 700) {
      if (_scrollController.offset >=
          _scrollController.position.maxScrollExtent) return;
      // 执行向上滚动操作
      _scrollController.jumpTo(_scrollController.offset + 5);
    }
  }

  ///创建拖拽过程中的内容展示
  Widget _buildFeedbackContainer({
    required int index,
    required bool isFile, //是否是文件夹
  }) {
    return Container(
      alignment: Alignment.center,
      width: Get.width,
      height: cellHeight,
      decoration: BoxDecoration(
        borderRadius: const BorderRadius.all(
          Radius.circular(10.0),
        ),
        color: Colors.yellow.withOpacity(0.6),
      ),
      child: Text(
        isFile
            ? "拖拽的内容:${_gatherList[index]["label"]}"
            : "拖拽的设备:${_deviceList[index - _gatherList.length]["label"]}",
        style: const TextStyle(
          decoration: TextDecoration.none,
          fontSize: 20.0,
          color: Colors.red,
        ),
      ),
    );
  }

  ///创建占位容器
  Widget _buildContainerWhenDragging() {
    return Container(
      alignment: Alignment.center,
      color: Colors.red,
      child: const Text(
        "我是个占位容器,真实的我被拽走了",
        style: TextStyle(
          color: Colors.black,
        ),
      ),
    );
  }
}

const cellHeight = 88.0;

简书地址

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/378157.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

flask+pyinstaller实现mock接口,并打包到exe运行使用postman验证

flask代码 from flask import Flask, request, jsonifyapp Flask(__name__)app.route("/login", methods[POST]) def login():username request.json.get("username").strip() # 用户名password request.json.get("password").strip() # 密…

Git介绍和常用命令说明

目录 一、Git概述 1.1 Git是什么 1.2 Git有什么用 1.3 Git仓库介绍 二、Git下载与安装 三、Git代码托管服务&#xff08;远程仓库&#xff09; 四、Git常用命令 4.1 设置用户信息 4.2 获取Git仓库 4.2.1 本地初始化Git仓库 4.2.2 从远程仓库克隆 4.3 本地仓库操作 …

【Spring源码解读!底层原理进阶】【下】探寻Spring内部:BeanFactory和ApplicationContext实现原理揭秘✨

&#x1f389;&#x1f389;欢迎光临&#x1f389;&#x1f389; &#x1f3c5;我是苏泽&#xff0c;一位对技术充满热情的探索者和分享者。&#x1f680;&#x1f680; &#x1f31f;特别推荐给大家我的最新专栏《Spring 狂野之旅&#xff1a;底层原理高级进阶》 &#x1f680…

Mongodb启动为Windows服务开机自启动

注意&#xff1a;mongodb的安装目录不应有中文&#xff0c;如果有&#xff0c;服务启动的路径会出现乱码&#xff0c;导致找不到对应的文件 1.安装好mongoDB 2.创建data目录&#xff0c;并在其中创建db目录和log目录 3.在log目录中创建mongodb.log文件 4.打开cmd&#xff08;用…

创建本地yum源并安装tree命令(openEuler-20.03-LTS-SP3)

步骤 1&#xff1a;下载ISO镜像 首先&#xff0c;您需要从提供的URL下载ISO镜像文件&#xff1a; cd /opt wget https://mirrors.dotsrc.org/openeuler/openEuler-20.03-LTS-SP3/ISO/x86_64/openEuler-20.03-LTS-SP3-x86_64-dvd.iso步骤 2&#xff1a;挂载ISO镜像 接下来&am…

简易计算器的制作(函数指针数组的实践)

个人主页&#xff08;找往期文章包括但不限于本期文章中不懂的知识点&#xff09;&#xff1a; 我要学编程(ಥ_ಥ)-CSDN博客 前期思路&#xff08;菜单的制作等&#xff09;&#xff1a;利用C语言的分支循环少量的函数知识写一个猜数字的小游戏-CSDN博客 计算器的制作其实与游…

vuecli3 执行 npm run build 打包命令报错:TypeError: file.split is not a function

问题 今天有个项目在打包的时候遇到了一个问题&#xff0c;就是执行 npm run build 命令的时候报错了&#xff0c;如下&#xff1a; 解决 我排查了一下&#xff0c;模拟代码如下&#xff1a;在打包的时候用了 MinChunkSizePlugin const webpack require("webpack"…

2024阿里云GPU服务器租用价格表(包月/按小时/学生价)

阿里云GPU服务器租用价格表包括包年包月价格、一个小时收费以及学生GPU服务器租用费用&#xff0c;阿里云GPU计算卡包括NVIDIA V100计算卡、T4计算卡、A10计算卡和A100计算卡&#xff0c;GPU云服务器gn6i可享受3折优惠&#xff0c;阿里云百科aliyunbaike.com分享阿里云GPU服务器…

Docker容器监控-CIG

目录 一、CIG说明 1. CAdvisor 2. InfluxDB 3. Grafana 二、环境搭建 1. 创建目录 2. 编写 docker-compose.yml 3. 检查并运行容器 三、进行测试 1. 查看 influxdb 存储服务 是否能正常访问 2. 查看 cAdvisor 收集服务能否正常访问 3. 查看 grafana 展现服务&#…

波纹扩散效果

<!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-width, initial-scale1.0" /><title>波纹扩散</title><style>body {disp…

学习 Redis 基础数据结构,不讲虚的。

学习 Redis 基础数据结构&#xff0c;不讲虚的。 一个群友给我发消息&#xff0c;“该学的都学了&#xff0c;怎么就找不到心意的工作&#xff0c;太难了”。 很多在近期找过工作的同学一定都知道了&#xff0c;背诵八股文已经不是找工作的绝对王牌。企业最终要的是可以创造价…

DePIN+GameFi+顶级电竞团队,GAIMIN如何颠覆Web3游戏赛道

DePIN带动互联网进入去中心化时代 自从智能合约和去中心化应用DApp普及之后&#xff0c;越来越多的从业者开始将目光放在区块链对互联网的升级和改造之上&#xff0c;这里主要进行了三类&#xff0c;第一类是通过节点的去中心化来实现的&#xff0c;这样确保了基础设施的分散&a…

双指针专题2 数组元素的目标和

数组元素的目标和 C代码实现: #include<iostream> using namespace std; const int N1000000;int a[N],b[N]; int main(){int n,m,x;cin>>n>>m;cin>>x;for(int i0;i<n;i){cin>>a[i];}for(int j0;j<m;j){cin>>b[j];}for(int i0,jm-1…

Java利用阿里云OSS/本地存储实现文件上传功能

目录 1、简介 2、本地存储 3、阿里云OSS 3.1、准备 3.2、入门 3.3、项目实施 3.4、注意 4、图片删除 &#x1f343;作者介绍&#xff1a;双非本科大三网络工程专业在读&#xff0c;阿里云专家博主&#xff0c;专注于Java领域学习&#xff0c;擅长web应用开发、数据结构和…

零基础学编程系列,从入门到精通,中文编程开发语言工具下载,编程构件容器件之控制面板构件用法

零基础学编程系列&#xff0c;从入门到精通&#xff0c;中文编程开发语言工具下载&#xff0c;编程构件容器件之控制面板构件用法 一、前言 编程入门视频教程链接 https://edu.csdn.net/course/detail/39036 编程工具及实例源码文件下载可以点击最下方官网卡片——软件下载…

vite项目配置根据不同的打包环境使用不同的请求路径VITE_BASE_URL,包括报错解决

vite环境配置可以看官方文档&#xff1a;环境变量和模式 | Vite 官方中文文档 创建环境配置文件 在项目根目录下面创建.env和.env.production文件&#xff0c;.env是开发环境使用的&#xff0c;.env.production是生产环境使用的。 .env文件&#xff1a; # 基本环境 VITE_APP…

敏捷产品是双轨开发而非双轨制

长话短说&#xff1a; 如果你以前听说过“双轨开发”这个术语&#xff0c;本文将解释它的来源和含义。以下是要点 开发工作侧重于可预测的结果和可保证的质量 探索工作侧重于快速学习和快速验证 探索和发展被可视化为两条轨道&#xff0c;因为这是两种工作&#xff0c;两种思…

Leetcode3020. 子集中元素的最大数量

Every day a Leetcode 题目来源&#xff1a;3020. 子集中元素的最大数量 解法1&#xff1a;哈希 枚举 用一个哈希表统计数组 nums 中的元素及其出现次数。 暴力枚举数组中的数&#xff0c;作为 x&#xff0c;然后不断看 x2,x4,⋯ 在数组中的个数。直到个数不足 2 个为止&a…

微信小程序新手入门教程四:样式设计

WXSS (WeiXin Style Sheets)是一套样式语言&#xff0c;用于描述 WXML 的组件样式&#xff0c;决定了 WXML 的组件会怎么显示。 WXSS 具有 CSS 大部分特性&#xff0c;同时为了更适合开发微信小程序&#xff0c;WXSS 对 CSS 进行了扩充以及修改。与 CSS 相比&#xff0c;WXSS …

Unity AnimationRigging无法修改权重?

个人理解&#xff0c;已解决无法修改权重的问题: unity自带的动画系统是在FixUpdate和Update之后LateUpdate之前执行&#xff0c;如果在这FixedUpdate或Update函数内更新AnimationRigging内的权重后&#xff0c;内部动画系统会覆盖权重的修改&#xff0c;导致无法正确更新&…