<h1 id="create-a-draggable-widget-in-flutter">Create a Draggable Widget in Flutter</h1> <p>Adding draggable elements to your Flutter application can greatly enhance the user experience by providing intuitive interactions. In this tutorial, we'll explore different ways to create draggable widgets in Flutter, from simple draggable objects to more complex use cases like drag-and-drop lists and custom draggable panels.</p> <h2 id="basic-draggable-widget">Basic Draggable Widget</h2> <p>The simplest way to create a draggable widget in Flutter is to use the built-in <code>Draggable</code> widget. This widget lets you create an element that users can drag around the screen.</p> <pre>import 'package:flutter/material.dart';
void main() { runApp(MyApp()); }
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Draggable Widget Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: DraggableDemo(), ); } }
class DraggableDemo extends StatefulWidget { @override _DraggableDemoState createState() => _DraggableDemoState(); }
class _DraggableDemoState extends State<DraggableDemo> { Offset _position = Offset(100, 100);
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Draggable Widget Demo'), ), body: Stack( children: [ Positioned( left: _position.dx, top: _position.dy, child: Draggable( feedback: Container( width: 100, height: 100, color: Colors.blue.withOpacity(0.5), child: Center( child: Text( 'Dragging', style: TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold, ), ), ), ), childWhenDragging: Container( width: 100, height: 100, color: Colors.grey.withOpacity(0.5), ), child: Container( width: 100, height: 100, color: Colors.blue, child: Center( child: Text( 'Drag Me', style: TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold, ), ), ), ), onDragEnd: (details) { setState(() ); }, ), ), ], ), ); } } </pre> <p><img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAwIiBoZWlnaHQ9IjQwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8IS0tIEJhY2tncm91bmQgLS0+CiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjQwMCIgaGVpZ2h0PSI0MDAiIGZpbGw9IiNmNWY1ZjUiLz4KICAKICA8IS0tIEFwcEJhciAtLT4KICA8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iNDAwIiBoZWlnaHQ9IjU2IiBmaWxsPSIjMjE5NkYzIi8+CiAgPHRleHQgeD0iMjAiIHk9IjMzIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTgiIGZpbGw9IndoaXRlIj5EcmFnZ2FibGUgV2lkZ2V0IERlbW88L3RleHQ+CiAgCiAgPCEtLSBTY3JlZW4gQ29udGVudCAtLT4KICA8IS0tIFN0YXRpYyBCbHVlIEJveCAtLT4KICA8cmVjdCB4PSI1MCIgeT0iMTAwIiB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iIzIxOTZGMyIgc3Ryb2tlPSIjMTk3NkQyIiBzdHJva2Utd2lkdGg9IjIiLz4KICA8dGV4dCB4PSI3MCIgeT0iMTUwIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTQiIGZpbGw9IndoaXRlIj5EcmFnIE1lPC90ZXh0PgogIAogIDwhLS0gRHJhZ2dpbmcgQm94IC0tPgogIDxyZWN0IHg9IjIwMCIgeT0iMTUwIiB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0icmdiYSgzMywgMTUwLCAyNDMsIDAuNSkiIHN0cm9rZT0icmdiYSgyNSwgMTE4LCAyMTAsIDAuNSkiIHN0cm9rZS13aWR0aD0iMiIvPgogIDx0ZXh0IHg9IjIxMCIgeT0iMjAwIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTQiIGZpbGw9IndoaXRlIj5EcmFnZ2luZzwvdGV4dD4KICAKICA8IS0tIEZpbmdlciBEcmFnZ2luZyAtLT4KICA8cGF0aCBkPSJNIDEwMCAxNTAgUSAxNTAgMTUwIDE4MCAxNzAgVCAyMjAgMTg1IiBzdHJva2U9IiM4ODg4ODgiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWRhc2hhcnJheT0iNSwyIiBmaWxsPSJub25lIi8+CiAgPGNpcmNsZSBjeD0iMjIwIiBjeT0iMTg1IiByPSI4IiBmaWxsPSIjZmZhNTAwIi8+CiAgCiAgPCEtLSBQcmV2aW91cyBQb3NpdGlvbiAoZ3JheWVkIG91dCkgLS0+CiAgPHJlY3QgeD0iNTAiIHk9IjEwMCIgd2lkdGg9IjEwMCIgaGVpZ2h0PSIxMDAiIGZpbGw9InJnYmEoMjAwLCAyMDAsIDIwMCwgMC41KSIvPgogIAogIDwhLS0gRHJhZ0JveCBUYXJnZXQgWm9uZSAtLT4KICA8cmVjdCB4PSIyNTAiIHk9IjI1MCIgd2lkdGg9IjEyMCIgaGVpZ2h0PSIxMjAiIGZpbGw9InJnYmEoNTAsIDIwMCwgNTAsIDAuMikiIHN0cm9rZT0icmdiYSgwLCAxNTAsIDAsIDAuNCkiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWRhc2hhcnJheT0iNSw1Ii8+CiAgPHRleHQgeD0iMjY1IiB5PSIzMTUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzMzMzMzMyI+RHJvcCBUYXJnZXQ8L3RleHQ+CiAgCiAgPCEtLSBFeHBsYW5hdGlvbiBUZXh0IC0tPgogIDx0ZXh0IHg9IjIwIiB5PSIzNzAiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMCIgZmlsbD0iIzY2NjY2NiI+RHJhZyB0aGUgYmx1ZSBib3ggYXJvdW5kIHRoZSBzY3JlZW4uIFRoZSB0cmFuc3BhcmVudCBib3ggc2hvd3MgdGhlIGRyYWdnaW5nIHN0YXRlLjwvdGV4dD4KICA8dGV4dCB4PSIyMCIgeT0iMzg1IiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTAiIGZpbGw9IiM2NjY2NjYiPlRoZSBncmVlbiBhcmVhIHJlcHJlc2VudHMgYSBwb3RlbnRpYWwgZHJvcCB0YXJnZXQgZm9yIGEgbW9yZSBjb21wbGV4IGRyYWctYW5kLWRyb3AuPC90ZXh0PgogIAogIDwhLS0gQXJyb3cgSW5kaWNhdG9ycyAtLT4KICA8Y2lyY2xlIGN4PSIxNzAiIGN5PSIxNzAiIHI9IjMiIGZpbGw9IiM4ODg4ODgiLz4KICA8cG9seWdvbiBwb2ludHM9IjE4MCwxNzAgMTkwLDE2MCAxOTAsMTgwIiBmaWxsPSIjODg4ODg4Ii8+CiAgPHRleHQgeD0iMTQwIiB5PSIxNDAiIHRyYW5zZm9ybT0icm90YXRlKDE1IDE0MCwxNDApIiBmb250LWZhbWlseT0iQXJpYWwiIGZvbnQtc2l6ZT0iMTAiIGZpbGw9IiM4ODg4ODgiPkRyYWcgZGlyZWN0aW9uPC90ZXh0PgogIAogIDwhLS0gRmx1dHRlciBMb2dvIC0tPgogIDxyZWN0IHg9IjEwIiB5PSIxMCIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiBmaWxsPSIjNDJBNUY1IiByeD0iNSIgcnk9IjUiLz4KICA8cGF0aCBkPSJNIDE4IDEzIEwgMjYgMjEgTCAxOCAyOSBMIDEwIDIxIFoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPg==" alt="Draggable Widget Visualization" /></p> <p>In this example:</p> <ul> <li><code>Draggable</code> is the core widget that enables dragging</li> <li><code>feedback</code> is what appears under the user's finger during the drag</li> <li><code>childWhenDragging</code> is shown in the original position while dragging</li> <li><code>child</code> is the widget displayed normally</li> <li><code>onDragEnd</code> updates the position when dragging ends</li> </ul> <h2 id="drag-and-drop-functionality">Drag and Drop Functionality</h2> <p>To implement a complete drag and drop system, we need to use both <code>Draggable</code> and <code>DragTarget</code> widgets:</p> <pre>class DragAndDropDemo extends StatefulWidget { @override _DragAndDropDemoState createState() => _DragAndDropDemoState(); }
class _DragAndDropDemoState extends State<DragAndDropDemo> { Color _draggableColor = Colors.blue; Color _targetColor = Colors.grey; bool _isDropped = false;
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Drag and Drop Demo'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Draggable<Color>( data: _draggableColor, feedback: Container( width: 120, height: 120, color: _draggableColor.withOpacity(0.5), child: Center( child: Text( 'Dragging', style: TextStyle( color: Colors.white, fontSize: 18, ), ), ), ), childWhenDragging: Container( width: 120, height: 120, color: Colors.grey.withOpacity(0.5), ), child: Container( width: 120, height: 120, color: _draggableColor, child: Center( child: Text( 'Drag Me', style: TextStyle( color: Colors.white, fontSize: 18, ), ), ), ), ), DragTarget<Color>( onAccept: (color) { setState(() ); }, onWillAccept: (color) => true, builder: (context, candidateData, rejectedData) { return Container( width: 200, height: 200, color: _targetColor, child: Center( child: Text( _isDropped ? 'Dropped!' : 'Drop Here', style: TextStyle( color: Colors.white, fontSize: 18, ), ), ), ); }, ), ElevatedButton( onPressed: () { setState(() ); }, child: Text('Reset'), ), ], ), ), ); } } </pre> <h2 id="draggable-list-items">Draggable List Items</h2> <p>A common use case is making list items rearrangeable through drag and drop:</p> <pre>class DraggableListDemo extends StatefulWidget { @override _DraggableListDemoState createState() => _DraggableListDemoState(); }
class _DraggableListDemoState extends State<DraggableListDemo> { List<String> _items = [ 'Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5', ];
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Draggable List Items'), ), body: ReorderableListView( onReorder: (oldIndex, newIndex) { setState(() { if (newIndex > oldIndex) { newIndex -= 1; } final item = _items.removeAt(oldIndex); _items.insert(newIndex, item); }); }, children: _items.map((item) { return ListTile( key: ValueKey(item), title: Text(item), leading: Icon(Icons.drag_handle), tileColor: Colors.white, ); }).toList(), ), ); } } </pre> <p>Flutter provides the convenient <code>ReorderableListView</code> widget that handles most of the drag-and-drop complexity for you.</p> <h2 id="creating-a-custom-draggable-panel">Creating a Custom Draggable Panel</h2> <p>For more complex use cases, we can create a custom draggable panel widget:</p> <pre>class DraggablePanel extends StatefulWidget { final Widget child; final double initialTop; final double minTop; final double maxTop;
DraggablePanel({ required this.child, this.initialTop = 200, this.minTop = 100, this.maxTop = 400, });
@override _DraggablePanelState createState() => _DraggablePanelState(); }
class _DraggablePanelState extends State<DraggablePanel> { late double _top;
@override void initState() { super.initState(); _top = widget.initialTop; }
@override Widget build(BuildContext context) { return Stack( children: [ Positioned( top: _top, left: 0, right: 0, bottom: 0, child: GestureDetector( onVerticalDragUpdate: (details) { setState(() { _top += details.delta.dy; _top = _top.clamp(widget.minTop, widget.maxTop); }); }, child: Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.only( topLeft: Radius.circular(20), topRight: Radius.circular(20), ), boxShadow: [ BoxShadow( color: Colors.black26, blurRadius: 10, spreadRadius: 0, offset: Offset(0, -3), ), ], ), child: Column( children: [ Center( child: Container( margin: EdgeInsets.only(top: 8, bottom: 8), width: 40, height: 5, decoration: BoxDecoration( color: Colors.grey[300], borderRadius: BorderRadius.circular(2.5), ), ), ), Expanded(child: widget.child), ], ), ), ), ), ], ); } } </pre> <p>Usage:</p> <pre>class DraggablePanelDemo extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Draggable Panel Demo'), ), body: Stack( children: [ Center( child: Text('Content Behind Panel'), ), DraggablePanel( initialTop: 300, minTop: 100, maxTop: 500, child: ListView.builder( itemCount: 20, itemBuilder: (context, index) { return ListTile( title: Text('Item ${index + 1}'), ); }, ), ), ], ), ); } } </pre> <h2 id="using-gesturedetector-for-simplified-dragging">Using GestureDetector for Simplified Dragging</h2> <p>For simple dragging needs without the full drag-and-drop capabilities, you can use <code>GestureDetector</code>:</p> <pre>class GestureDraggableDemo extends StatefulWidget { @override _GestureDraggableDemoState createState() => _GestureDraggableDemoState(); }
class _GestureDraggableDemoState extends State<GestureDraggableDemo> { double _left = 100; double _top = 100;
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('GestureDetector Draggable Demo'), ), body: Stack( children: [ Positioned( left: _left, top: _top, child: GestureDetector( onPanUpdate: (details) { setState(() { _left += details.delta.dx; _top += details.delta.dy; }); }, child: Container( width: 100, height: 100, decoration: BoxDecoration( color: Colors.red, borderRadius: BorderRadius.circular(10), ), child: Center( child: Text( 'Drag Me', style: TextStyle(color: Colors.white), ), ), ), ), ), ], ), ); } } </pre> <h2 id="using-longpressdraggable">Using LongPressDraggable</h2> <p>If you want to initiate dragging only after a long press, you can use <code>LongPressDraggable</code>:</p> <pre>LongPressDraggable<String>( data: 'long_press_data', feedback: Container( width: 100, height: 100, color: Colors.orange.withOpacity(0.5), child: Center( child: Text( 'Dragging', style: TextStyle(color: Colors.white), ), ), ), child: Container( width: 100, height: 100, color: Colors.orange, child: Center( child: Text( 'Long Press to Drag', textAlign: TextAlign.center, style: TextStyle(color: Colors.white), ), ), ), childWhenDragging: Container( width: 100, height: 100, color: Colors.grey.withOpacity(0.5), ), onDragStarted: () { print('Drag started'); }, onDragEnd: (details) { print('Drag ended'); }, ) </pre> <h2 id="best-practices-for-draggable-widgets">Best Practices for Draggable Widgets</h2> <ol> <li><p><strong>Provide Visual Feedback</strong>: Always make it clear when an item is being dragged by showing a different appearance.</p> </li> <li><p><strong>Consider Performance</strong>: When dragging large or complex widgets, consider using a simpler representation for the <code>feedback</code> widget to maintain smooth animations.</p> </li> <li><p><strong>Handle Edge Cases</strong>: Make sure to handle cases where the user drags items out of bounds or to invalid locations.</p> </li> <li><p><strong>Implement Proper Animations</strong>: Smooth transitions make dragging feel natural.</p> </li> <li><p><strong>Use Platform-Appropriate Gestures</strong>: On mobile, dragging should feel natural and match platform conventions.</p> </li> <li><p><strong>Add Haptic Feedback</strong>: Consider adding haptic feedback when an item is picked up or dropped to improve user experience.</p> </li> </ol> <h2 id="conclusion">Conclusion</h2> <p>Draggable widgets add a level of interactivity to your Flutter applications that can greatly enhance the user experience. Whether you're implementing simple draggable elements, creating drag-and-drop functionality, or building complex draggable panels, Flutter provides the tools you need to create smooth and intuitive drag interactions.</p> <p>With the techniques described in this tutorial, you can implement draggable functionality in your Flutter applications for a wide range of use cases, from rearrangeable lists to custom panels and drag-and-drop interfaces.</p> <p>Happy coding!</p>