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,
                    //拖拽的文件夾內容展示
                    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,
                  //拖拽的設備內容展示
                  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,
                // 輸入框內容上下居中
                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)建拖拽過程中的內容展示
  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,
        ),
      ),
    );
  }

  ///創(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地址

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末境蜕,一起剝皮案震驚了整個濱河市蝙场,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌粱年,老刑警劉巖售滤,帶你破解...
    沈念sama閱讀 221,548評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異台诗,居然都是意外死亡完箩,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評論 3 399
  • 文/潘曉璐 我一進店門拉队,熙熙樓的掌柜王于貴愁眉苦臉地迎上來弊知,“玉大人,你說我怎么就攤上這事粱快≈韧” “怎么了?”我有些...
    開封第一講書人閱讀 167,990評論 0 360
  • 文/不壞的土叔 我叫張陵事哭,是天一觀的道長漫雷。 經常有香客問我,道長鳍咱,這世上最難降的妖魔是什么降盹? 我笑而不...
    開封第一講書人閱讀 59,618評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮谤辜,結果婚禮上澎现,老公的妹妹穿的比我還像新娘仅胞。我一直安慰自己,他們只是感情好剑辫,可當我...
    茶點故事閱讀 68,618評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著渠欺,像睡著了一般妹蔽。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上挠将,一...
    開封第一講書人閱讀 52,246評論 1 308
  • 那天胳岂,我揣著相機與錄音,去河邊找鬼舔稀。 笑死乳丰,一個胖子當著我的面吹牛,可吹牛的內容都是我干的内贮。 我是一名探鬼主播产园,決...
    沈念sama閱讀 40,819評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼夜郁!你這毒婦竟也來了什燕?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,725評論 0 276
  • 序言:老撾萬榮一對情侶失蹤竞端,失蹤者是張志新(化名)和其女友劉穎屎即,沒想到半個月后,有當地人在樹林里發(fā)現(xiàn)了一具尸體事富,經...
    沈念sama閱讀 46,268評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡技俐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,356評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了统台。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片雕擂。...
    茶點故事閱讀 40,488評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖饺谬,靈堂內的尸體忽然破棺而出捂刺,到底是詐尸還是另有隱情,我是刑警寧澤募寨,帶...
    沈念sama閱讀 36,181評論 5 350
  • 正文 年R本政府宣布族展,位于F島的核電站,受9級特大地震影響拔鹰,放射性物質發(fā)生泄漏仪缸。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,862評論 3 333
  • 文/蒙蒙 一列肢、第九天 我趴在偏房一處隱蔽的房頂上張望恰画。 院中可真熱鬧宾茂,春花似錦、人聲如沸拴还。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽片林。三九已至端盆,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間费封,已是汗流浹背焕妙。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留弓摘,地道東北人焚鹊。 一個月前我還...
    沈念sama閱讀 48,897評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像韧献,于是被迫代替她去往敵國和親嗡综。 傳聞我的和親對象是個殘疾皇子堂鲤,可洞房花燭夜當晚...
    茶點故事閱讀 45,500評論 2 359

推薦閱讀更多精彩內容