Search code examples
flutterdartdraggable

Flutter - Dragging a Draggable from drawer to body


Given a list of items in a drawer, is it somehow possible to drag one of these items from the drawer onto the body of the scaffold and into the DragTarget?

Below is a simplified example that shows what I would generally like to happen. Except when I Pop the Navigator, the Draggable context also dissapears as this is obviously part of the Drawer. I'm not sure how to solve this problem. Or if this is even possible...

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  String droppedItem = "";

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Drag and Drop Example'),
      ),
      body: Center(
        child: Container(
          width: 200,
          height: 200,
          color: Colors.grey,
          child: DragTarget<String>(
            onWillAcceptWithDetails: (details) {
              setState(() {
                droppedItem = details.data;
              });
              return true;
            },
            builder: (context, candidateData, rejectedData) {
              return Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  const Icon(
                    Icons.cloud_upload,
                    size: 50,
                    color: Colors.blue,
                  ),
                  Text(
                    'Drop Here : \n $droppedItem',
                    style: const TextStyle(fontSize: 20),
                  ),
                ],
              );
            },
          ),
        ),
      ),
      drawer: Drawer(
        child: ListView(
          children: <Widget>[
            Draggable<String>(
              data: 'Item 1',
              feedback: Container(
                color: Colors.deepOrange,
                height: 50,
                width: 50,
                child: const Icon(Icons.directions_run),
              ),
              childWhenDragging: const ListTile(
                title: Text('Item 1'),
                leading: Icon(Icons.drag_handle), // Placeholder icon
              ),
              child: const ListTile(
                title: Text('Item 1'),
                leading: Icon(Icons.drag_handle), // Placeholder icon
              ),
              // onDragStarted: () {
              //   Navigator.pop(context);
              // },
            ),
            Draggable<String>(
              data: 'Item 2',
              feedback: Container(
                color: Colors.deepOrange,
                height: 50,
                width: 50,
                child: const Icon(Icons.directions_run),
              ),
              childWhenDragging: const ListTile(
                title: Text('Item 2'),
                leading: Icon(Icons.drag_handle), // Placeholder icon
              ),
              child: const ListTile(
                title: Text('Item 2'),
                leading: Icon(Icons.drag_handle), // Placeholder icon
              ),
              // onDragStarted: () {
              //   Navigator.pop(context);
              // },
            ),
          ],
        ),
      ),
    );
  }
}

Solution

  • This may not suit your usecase if it's a requirement to use a drawer, BUT: you could map the button on the scaffold to bring on a custom widget that masquerades as a drawer as an Overlay. That gives you a ton of power in that situation:

    import 'package:flutter/material.dart';
    
    class MyHomePageRefactor extends StatefulWidget {
      const MyHomePageRefactor({super.key});
    
      @override
      State<MyHomePageRefactor> createState() => _MyHomePageRefactorState();
    }
    
    class _MyHomePageRefactorState extends State<MyHomePageRefactor>
        with SingleTickerProviderStateMixin {
      late AnimationController pseudoDrawerAnimationController;
      late Animation<Offset> pseudoDrawerSlideAnimation;
    
      OverlayEntry? pseudoDrawerOverlay;
    
      String droppedItem = "";
    
      @override
      void initState() {
        super.initState();
        pseudoDrawerAnimationController = AnimationController(
          vsync: this,
          // This is usually the default for page animations
          duration: const Duration(milliseconds: 300),
        );
    
        pseudoDrawerSlideAnimation = Tween<Offset>(
          begin: const Offset(-1.0, 0.0),
          end: const Offset(0.0, 0.0),
        ).animate(pseudoDrawerAnimationController);
      }
    
      @override
      void dispose() {
        pseudoDrawerAnimationController.dispose();
        super.dispose();
      }
    
      void _onDrawerSummoned() {
        if (!mounted || pseudoDrawerOverlay != null) return;
    
        pseudoDrawerOverlay = OverlayEntry(
          builder: (BuildContext context) => Positioned(
            left: 0.0,
            top: 0.0,
            child: SlideTransition(
              position: pseudoDrawerSlideAnimation,
              child: PsudeoDrawer(
                pageConstraints: BoxConstraints(
                  maxWidth: MediaQuery.of(context).size.width,
                  maxHeight: MediaQuery.of(context).size.height,
                ),
                // NOTE: you have access to context in here, so you could adjust the
                //  constraints based on context
                drawerConstraints: const BoxConstraints(maxWidth: 300.0),
                // You could also adjsut the parameters of your PsudeoDrawer to be
                //  able to pass in your options in here, so that both
                //  [_MyHomePageState] and [PsudeoDrawer] are both aware of your
                //  options. I am not doing that in this example for simplicity's
                //  sake
                popDrawer: _onDrawerDismissed,
              ),
            ),
          ),
        );
    
        // Insert the overlay and drive it forward!
        Overlay.of(context).insert(pseudoDrawerOverlay!);
        pseudoDrawerAnimationController.forward();
      }
    
      // NOTE: You could decide to leave the pseudoDrawer open by adjusting the
      //  pseudoDrawer code/removing it's drag target, and then instead just
      //  calling this function when you accept a widget dragged from the side
      Future<void> _onDrawerDismissed() async {
        if (!mounted) return;
        await pseudoDrawerAnimationController.reverse();
        if (!mounted) return;
        pseudoDrawerOverlay?.remove();
        pseudoDrawerOverlay = null;
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            leading: IconButton(
              icon: const Icon(Icons.menu),
              onPressed: _onDrawerSummoned,
            ),
            title: const Text('Drag and Drop Example'),
          ),
          body: Center(
            child: Container(
              width: 200,
              height: 200,
              color: Colors.grey,
              child: DragTarget<String>(
                onWillAcceptWithDetails: (details) {
                  setState(() {
                    droppedItem = details.data;
                  });
                  return true;
                },
                builder: (context, candidateData, rejectedData) {
                  return Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[
                      const Icon(
                        Icons.cloud_upload,
                        size: 50,
                        color: Colors.blue,
                      ),
                      Text(
                        'Drop Here : \n $droppedItem',
                        style: const TextStyle(fontSize: 20),
                      ),
                    ],
                  );
                },
              ),
            ),
          ),
        );
      }
    }
    
    class PsudeoDrawer extends StatelessWidget {
      final BoxConstraints pageConstraints;
      final BoxConstraints drawerConstraints;
      final void Function() popDrawer;
    
      const PsudeoDrawer({
        super.key,
        required this.pageConstraints,
        required this.drawerConstraints,
        required this.popDrawer,
      });
    
      @override
      Widget build(BuildContext context) => ConstrainedBox(
            constraints: pageConstraints,
            child: Row(
              children: [
                ConstrainedBox(
                  constraints: drawerConstraints,
                  child: Material(
                    elevation: 10.0,
                    shadowColor: Colors.black87,
                    child: ListView(
                      children: <Widget>[
                        Draggable<String>(
                          data: 'Item 1',
                          feedback: Container(
                            color: Colors.deepOrange,
                            height: 50,
                            width: 50,
                            child: const Icon(Icons.directions_run),
                          ),
                          childWhenDragging: const ListTile(
                            title: Text('Item 1'),
                            leading: Icon(Icons.drag_handle), // Placeholder icon
                          ),
                          child: const ListTile(
                            title: Text('Item 1'),
                            leading: Icon(Icons.drag_handle), // Placeholder icon
                          ),
                          // onDragStarted: () {
                          //   Navigator.pop(context);
                          // },
                        ),
                        Draggable<String>(
                          data: 'Item 2',
                          feedback: Container(
                            color: Colors.deepOrange,
                            height: 50,
                            width: 50,
                            child: const Icon(Icons.directions_run),
                          ),
                          childWhenDragging: const ListTile(
                            title: Text('Item 2'),
                            leading: Icon(Icons.drag_handle), // Placeholder icon
                          ),
                          child: const ListTile(
                            title: Text('Item 2'),
                            leading: Icon(Icons.drag_handle), // Placeholder icon
                          ),
                          // onDragStarted: () {
                          //   Navigator.pop(context);
                          // },
                        ),
                      ],
                    ),
                  ),
                ),
                Expanded(
                  child: GestureDetector(
                    // The gesture detector allows you to tap on the area to the
                    //  right to dismiss the pseudoDrawer. You could also experiment
                    //  with adding addition dismissal interactions
                    behavior: HitTestBehavior.opaque,
                    onTap: popDrawer,
                    child: DragTarget(
                      builder: (
                        BuildContext context,
                        List<Object?> candidateData,
                        List<Object?> rejectedData,
                      ) =>
                          const SizedBox.expand(),
                      onWillAcceptWithDetails: (details) {
                        // Pop the pseudoDrawer, and do NOT accept the dragged widget.
                        popDrawer();
                        return false;
                      },
                    ),
                  ),
                ),
              ],
            ),
          );
    }
    

    Here's my refactor. Note that if you want shadows, complex gestures, or a number of other drawer-like scenarios you would need to further adjust that starting code.

    The main focus of this example is just to demonstrate that you could achieve something similar to (though in fairness: not exactly) what you asked for by using Overlays and animation controllers.

    EDIT

    I accidentally uploaded your baseline code, not my changed code. Whoops. That wouldn't have been helpful... Fixed now.