hanshengchiu / reorderables

Reorderable table, row, column, wrap, and sliver list that allow drag and drop of the children. https://pub.dartlang.org/packages/reorderables
MIT License
729 stars 167 forks source link

Drag Items from outside into the list #66

Open Levaru opened 4 years ago

Levaru commented 4 years ago

Hi, first of all great package. I'm looking for the functionality of adding new items to the list by dragging them from outside (for example a bottom bar filled with icons that represent the desired item) and then dropping them at the desired position inbetween other items.

I was thinking of using a DraggableWidget to initiate the drag outside the list and then add the corresponding item to the reordable item list. Then I would somehow have to replace the DraggableWidget with the new ReordableWidget and initiate the drag of that so that the list would register it.

But looking at the source code I'm kinda lost on how to implement such a thing. Similar to this I'm also looking for a way to drag items between lists like for example in the Nested ReorderableWrap where items could be moved inbetween the wraps.

olindenbaum commented 4 years ago

@Levaru I was trying something similar, here is a way to simply add it but only to the end of the list. I do want to end up being able to drag it to a certain position and also to drag out to remove.

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

import 'package:reorderables/reorderables.dart';

void main() => runApp(MyApp());

List<Widget> tiles = <Widget>[
  Icon(
    Icons.filter_1,
    size: 90,
  ),
  Icon(
    Icons.filter_2,
    size: 90,
  ),
  Icon(
    Icons.filter_3,
    size: 90,
  ),
  Icon(
    Icons.filter_4,
    size: 90,
  ),
  Icon(
    Icons.filter_5,
    size: 90,
  ),
  Icon(
    Icons.filter_6,
    size: 90,
  ),
  Icon(
    Icons.filter_7,
    size: 90,
  ),
  Icon(
    Icons.filter_8,
    size: 90,
  ),
  Icon(
    Icons.filter_9,
    size: 90,
  ),
];

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        fontFamily: 'PressStart',
      ),
      home: ColorGame(),
    );
  }
}

class ColorGame extends StatefulWidget {
  ColorGame({Key key}) : super(key: key);

  createState() => ColorGameState();
}

class ColorGameState extends State<ColorGame> {
  List<String> droppableNames = ["DROP_A", "DROP_B", "DROP_C", "DROP_D"];
  Map<String, dynamic> selected = {
    "DROP_A": false,
    "DROP_B": false,
    "DROP_C": false,
    "DROP_D": false
  };

  @override
  void initState() {
    super.initState();
    print("DROPABLE: $droppableNames \n DROPPABLEMAP: ${selected.toString()}");
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            DragTarget<String>(
              builder: (BuildContext context, List<String> candidateData,
                  List<dynamic> rejectedData) {
                return WrapExample();
              },

              onWillAccept: (data) => true,
              // data == expecting.runtimeType, //condition to accept data
              onAccept: (data) {
                var newTile = Icon(Icons.ac_unit);
                print("Wrap recieved data");
                setState(() {
                  tiles.add(newTile);
                });
              },
            ),
            // WrapExample(),
            Container(
              //dragables
              decoration:
                  BoxDecoration(border: Border.all(color: Colors.black)),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceAround,
                crossAxisAlignment: CrossAxisAlignment.start,
                children: droppableNames.map((name) {
                  return Draggable<String>(
                    data: name,
                    child: Text(name + "_DRAGGABLE"),
                    feedback: Text(name + "_IN_DRAG"),
                    childWhenDragging: Text(name + "_REMAINER"),
                  );
                }).toList(),
              ),
            )
          ],
        ),
      ),
    );
  }

  Widget _buildDragAndDropTarget(String key, {bool remove: true}) {
    return DragTarget<String>(
      builder: (BuildContext context, List<String> incoming, List rejected) {
        if ((!(this.selected[key] == false) && remove) ||
            ((this.selected[key] == false) && remove)) {
          return Container(
            color: Colors.white,
            child: Draggable<String>(
              data: key,
              child: Text(key + "_DRAGGABLE"),
              feedback: Text(key + "_IN_DRAG"),
              childWhenDragging: remove ? Container() : Text(key + "_REMAINER"),
            ),
            alignment: Alignment.center,
            height: 80,
            width: 80,
          );
        } else {
          return Container(
              color: Colors.grey,
              height: 80,
              width: 100,
              child: Text("$key: EMPTY"));
        }
      },
      onWillAccept: (data) => true,
      // data == expecting.runtimeType, //condition to accept data
      onAccept: (data) {
        setState(() {
          selected[key] = data.toString();
          print(selected.toString());
        });
      },
    );
  }
}

//reordable bit

class WrapExample extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _WrapExampleState();
}

class _WrapExampleState extends State<WrapExample> {
  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    void _onReorder(int oldIndex, int newIndex) {
      setState(() {
        Widget row = tiles.removeAt(oldIndex);
        tiles.insert(newIndex, row);
      });
    }

    var wrap = ReorderableWrap(
        spacing: 8.0,
        runSpacing: 4.0,
        padding: const EdgeInsets.all(8),
        children: tiles,
        onReorder: _onReorder,
        onNoReorder: (int index) {
          //this callback is optional
          debugPrint(
              '${DateTime.now().toString().substring(5, 22)} reorder cancelled. index:$index');
        },
        onReorderStarted: (int index) {
          //this callback is optional
          debugPrint(
              '${DateTime.now().toString().substring(5, 22)} reorder started: index:$index');
        });

    var column = Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        wrap,
        ButtonBar(
          alignment: MainAxisAlignment.start,
          children: <Widget>[
            IconButton(
              iconSize: 50,
              icon: Icon(Icons.add_circle),
              color: Colors.deepOrange,
              padding: const EdgeInsets.all(0.0),
              onPressed: () {
                var newTile = Icon(Icons.filter_9_plus);
                setState(() {
                  tiles.add(newTile);
                });
              },
            ),
            IconButton(
              iconSize: 50,
              icon: Icon(Icons.remove_circle),
              color: Colors.teal,
              padding: const EdgeInsets.all(0.0),
              onPressed: () {
                setState(() {
                  tiles.removeAt(0);
                });
              },
            ),
          ],
        ),
      ],
    );

    return SingleChildScrollView(
      child: column,
    );
  }
}
jimmyff commented 4 years ago

@Levaru I thinking having two lists (the first only containing a single item) would provide the behavior you are wanting.

I also am wanting to do this. There is an issue for it but it's not been updated in a long time: https://github.com/hanshengchiu/reorderables/issues/1

emvaized commented 4 years ago

@Levaru Did you managed to achieve 'drag out to remove' functionality?

Levaru commented 4 years ago

@emvaized Yes, I did. It involves some modifications to the package source code though.

  1. You need to remove this line or make it a comment. If you don't have LongPressDraggable functionality then remove/comment out the same line for the Draggable. This allows you to drag your items freely around the screen.

  2. I created a "delete area" which is a DragTarget that accepts Keys (just like the one used for every item). The Draggables all carry Keys as their data, so when you drop one into your Delete-DragTarget the onAccept function should trigger. There you just find the same Key inside your item list and remove it.

  3. (Optional) In my case, the "delete area" appears the moment I start dragging an item. To catch this event, you will need to modify the onDragStarted function and then somehow trigger your "delete area" to appear.

emvaized commented 4 years ago

@Levaru Well, I'm using ReordableWrap widget, and in this case it simply doesn't have the line you mentioned... And all the DragTarget widgets just don't react to the dragged element for some reason, even onHover callback with print()function is not called.

UPD: I figured it out -- just needed to change DragTarget to DragTarget<int> Now it accepts icons correctly! Thanks a lot.

The only question remaining is, how to achieve dragging items to list from the outside.

Levaru commented 4 years ago

@emvaized

Well, I'm using ReordableWrap widget, and in this case it simply doesn't have the line you mentioned...

In that case I believe you don't have the movement restriction along the axis so you can skip it.

And all the DragTarget widgets just don't react to the dragged element for some reason, even onHover callback with print() function is not called.

You need to create the DragTarget the same way as in this line of code. So DragTarget<Key> would be the correct way but it's interesting that DragTarget<int> works too.

The only question remaining is, how to achieve dragging items to list from the outside.

I managed to add this functionality, but it involves some HEAVY modifications to the source code of the package and it's quite a messy workaround.

To drag something into the list you need a Draggable. In my case, I created a Row filled with Draggable<Key> that have an Icon as their child. The Draggables need the same functionality as the ones found in the source code.

The onDragStarted function of your Draggable<Key> first needs to create a new ReorderableWidget with an UniqueKey and then add it to the reorderable widget list. Afterwards it needs to set some variables the same way as the normal onDragStarted function but this time with the Key and index (just use the last position in the list) of your newly created ReorderableWidget.

That way the reorderable list can recognize it and treat your new Draggable as part of the list that is just being reordered with all those reorder animations that come with it. In case that you change your mind while dragging and don't want to add it, you can just delete the Widget from the list inside the onDraggableCanceled function.

The tricky part is how you manage to access those variables that are being set in the onDragStarted function. If you create the Row with your Draggable<Key> inside the reorderable_flex.dart and just add it as toolbar (for example) to the bottom of the widget tree, then you can access and modify the reorderable_list variables, no problem.

In my case, I had my toolbar as a separate widget and had to create an extra Provider class, move some of the code from the reorderable_list to it and manage it there. That includes the AnimationControllers _ghostController and _entranceController which I then provided back to the reorderable_list. It's quite messy honestly and maybe there is a better way but I'm quite new to Flutter so this is the only one I found.

Hope that helps you!

emvaized commented 4 years ago

@Levaru Well, sounds really complicated 😅 Not sure if I can handle it, but thanks a lot for the idea!

the-thirteenth-fox commented 4 years ago

We need examples for it

adrianjagielak commented 2 years ago

@Levaru Can you share your fork of the package? Or even just the edited files/sample?

Would be very helpful