DEV Community

Cover image for Creating a Draggable FloatingActionButton in Flutter: A Complete Guide
Ali Khoshsafa
Ali Khoshsafa

Posted on

Creating a Draggable FloatingActionButton in Flutter: A Complete Guide

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
    );
  });
}
Enter fullscreen mode Exit fullscreen mode

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),
  ),
),
Enter fullscreen mode Exit fullscreen mode

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),
              ),
            ),
          ),
      ],
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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)