Flutter開發(fā)模仿百度云盤創(chuàng)建文件夾功能Draggable和DragTarget的混合使用

使用LongPressDraggableDragTarget寫了個類似于百度云盤管理文件和文件夾的功能(為了避免和列表的滑動手勢沖突,所以采用LongPressDraggable而不是Draggable):

1、拖拽文件到文件夾中
2、拖拽兩個文件可以合并成一個新的文件夾

效果如下:

實現(xiàn)效果

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': '順義區(qū)'},
    {'label': '朝陽區(qū)'},
    {'label': '通州區(qū)'},
    {'label': '密云區(qū)'},
    {'label': '海淀區(qū)'},
  ];
  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,
                    //拖拽的文件夾內(nèi)容展示
                    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) {
                          ///當拖拽的是某個文件夾的時候,如果拖拽的文件夾放到被拖拽的文件夾上面的時候,不改變原來文件夾的狀態(tài)(背景色)
                          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,
                  //拖拽的設備內(nèi)容展示
                  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) {
                      ///拖拽設備放到設備上進行合并+創(chuàng)建新的文件夾
                      ///如果是把文件夾拖拽到設備上不做任何操作
                      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),
                      );
                    },
                  ),
                ),
              );
            },
          ),
        ),
      ],
    );
  }

  ///創(chuàng)建文件夾的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),
      ],
    );
  }

  ///創(chuàng)建設備的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,
                  ),
                ),
              ],
            ),
          ),
        ),
      ],
    );
  }

  ///合并兩個設備-創(chuàng)建新的文件夾
  _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,
                // 輸入框內(nèi)容上下居中
                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));

                      ///刪除設備之后再創(chuàng)建文件夾
                      _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;
      // 執(zhí)行向下滾動操作
      _scrollController.jumpTo(_scrollController.offset - 5);
    }

    if (details.globalPosition.dy > 700) {
      if (_scrollController.offset >=
          _scrollController.position.maxScrollExtent) return;
      // 執(zhí)行向上滾動操作
      _scrollController.jumpTo(_scrollController.offset + 5);
    }
  }

  ///創(chuàng)建拖拽過程中的內(nèi)容展示
  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
            ? "拖拽的內(nèi)容:${_gatherList[index]["label"]}"
            : "拖拽的設備:${_deviceList[index - _gatherList.length]["label"]}",
        style: const TextStyle(
          decoration: TextDecoration.none,
          fontSize: 20.0,
          color: Colors.red,
        ),
      ),
    );
  }

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

const cellHeight = 88.0;

CSDN地址

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容