Introduction
The FloatingActionButton (FAB) is a staple in Material Design apps, typically used for primary actions. But what if you want to make it draggable, allowing users to reposition it anywhere on the screen, preventing from covering other parts of the screen or even texts in other widgets.
In this article, we’ll implement a fully draggable FAB in Flutter as a solution using GestureDetector and Positioned inside a Stack. This approach ensures smooth dragging, proper boundary checks, and a polished user experience.
Why Use a Draggable FAB?
- Better UX: Lets users place the FAB where it’s most convenient.
- Avoids Obstructions: Useful when the FAB might block important content.
- Engagement: Adds a playful, interactive element to your app.
Implementation: Step-by-Step
1. Setting Up the Widget Structure
We’ll use:
-
Stack
→ To overlay the FAB on top of other content. -
Positioned
→ To control the FAB’s coordinates. -
GestureDetector
→ To handle drag gestures.
2. Initializing the FAB Position
We calculate the initial position after the first frame render to ensure correct placement.
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => _setInitialPosition());
}
void _setInitialPosition() {
final screenSize = MediaQuery.of(context).size;
final paddingBottom = MediaQuery.of(context).padding.bottom;
setState(() {
_fabPosition = Offset(
screenSize.width - _fabSize - 16, // Right margin
screenSize.height - paddingBottom - _fabSize - 16, // Bottom margin
);
});
}
3. Making the FAB Draggable
We use GestureDetector
’s onPanUpdate
to update the FAB’s position while dragging.
GestureDetector(
onPanUpdate: (details) {
final screenSize = MediaQuery.of(context).size;
final paddingTop = MediaQuery.of(context).padding.top;
final paddingBottom = MediaQuery.of(context).padding.bottom;
setState(() {
_fabPosition = Offset(
(_fabPosition.dx + details.delta.dx)
.clamp(0, screenSize.width - _fabSize),
(_fabPosition.dy + details.delta.dy)
.clamp(paddingTop, screenSize.height - paddingBottom - _fabSize),
);
_isDragging = true;
});
},
onPanEnd: (_) => setState(() => _isDragging = false),
child: FloatingActionButton(
onPressed: _isDragging ? null : () => print("FAB Pressed!"),
child: Icon(_isDragging ? Icons.drag_handle : Icons.add),
),
),
Key Features:
✔ ## Boundary-aware → Stays within screen limits.
✔ ## Visual feedback → Changes icon while dragging.
✔ ## Press handling → Disables click during drag.
4. Full Code Implementation
Here’s the complete widget:
import 'package:flutter/material.dart';
class DraggableFABScreen extends StatefulWidget {
@override
_DraggableFABScreenState createState() => _DraggableFABScreenState();
}
class _DraggableFABScreenState extends State<DraggableFABScreen> {
Offset _fabPosition = Offset(0, 0);
bool _isDragging = false;
final double _fabSize = 56.0;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => _setInitialPosition());
}
void _setInitialPosition() {
final screenSize = MediaQuery.of(context).size;
final paddingBottom = MediaQuery.of(context).padding.bottom;
setState(() {
_fabPosition = Offset(
screenSize.width - _fabSize - 16,
screenSize.height - paddingBottom - _fabSize - 16,
);
});
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
Scaffold(
appBar: AppBar(title: Text("Draggable FAB Demo")),
body: Center(child: Text("Main Content")),
),
if (_fabPosition != Offset.zero)
Positioned(
left: _fabPosition.dx,
top: _fabPosition.dy,
child: GestureDetector(
onPanUpdate: (details) {
final screenSize = MediaQuery.of(context).size;
final paddingTop = MediaQuery.of(context).padding.top;
final paddingBottom = MediaQuery.of(context).padding.bottom;
setState(() {
_fabPosition = Offset(
(_fabPosition.dx + details.delta.dx)
.clamp(0, screenSize.width - _fabSize),
(_fabPosition.dy + details.delta.dy)
.clamp(paddingTop, screenSize.height - paddingBottom - _fabSize),
);
_isDragging = true;
});
},
onPanEnd: (_) => setState(() => _isDragging = false),
child: FloatingActionButton(
onPressed: _isDragging ? null : () => print("FAB Pressed!"),
child: Icon(_isDragging ? Icons.drag_handle : Icons.add),
),
),
),
],
);
}
}
Possible Enhancements
Edge-Snapping → Make the FAB snap to screen edges when released.
Expandable → Creating expandable FAB widget with other buttons
Shape → Circular shaped FAB or even changing shapes while dragging
Conclusion
With just GestureDetector + Positioned, we’ve built a fully draggable FAB that enhances UX while staying performant. Try it in your next Flutter project!
Credits & References
I’ll push all the code on my GitHub repository and will put the link here later on.
Let me know in the comments how you’d improve this! 😊
Follow me for more Flutter tips!
Top comments (0)