How to create a movable widget in flutter such that is stays at the position it is dragged to

Viewed 5304
2 Answers

On top of dragging the object around, you can also make it zoomable with the help of a GestureDetector. I applied the GestureDetector to the main Stack so that you can pinch to zoom in/out anywhere on the screen. It makes it somewhat easier to see what you are doing.

enter image description here


HookWidget version

class DragArea extends HookWidget {
  final Widget child;

  const DragArea({Key key, this.child}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final position = useState(Offset(100, 100));
    final prevScale = useState(1.0);
    final scale = useState(1.0);
    return GestureDetector(
      onScaleUpdate: (details) => scale.value = prevScale.value * details.scale,
      onScaleEnd: (_) => prevScale.value = scale.value,
      child: Stack(
        children: [
          Positioned.fill(
              child: Container(color: Colors.amber.withOpacity(.4))),
          Positioned(
            left: position.value.dx,
            top: position.value.dy,
            child: Draggable(
              maxSimultaneousDrags: 1,
              feedback: Transform.scale(
                scale: scale.value,
                child: child,
              ),
              childWhenDragging: Opacity(
                opacity: .3,
                child: Transform.scale(
                  scale: scale.value,
                  child: child,
                ),
              ),
              onDragEnd: (details) => position.value = details.offset,
              child: Transform.scale(
                scale: scale.value,
                child: child,
              ),
            ),
          )
        ],
      ),
    );
  }
}

StatefulWidget version

class StatefulDragArea extends StatefulWidget {
  final Widget child;

  const StatefulDragArea({Key key, this.child}) : super(key: key);

  @override
  _DragAreaStateStateful createState() => _DragAreaStateStateful();
}

class _DragAreaStateStateful extends State<StatefulDragArea> {
  Offset position = Offset(100, 100);
  double prevScale = 1;
  double scale = 1;

  void updateScale(double zoom) => setState(() => scale = prevScale * zoom);
  void commitScale() => setState(() => prevScale = scale);
  void updatePosition(Offset newPosition) =>
      setState(() => position = newPosition);

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onScaleUpdate: (details) => updateScale(details.scale),
      onScaleEnd: (_) => commitScale(),
      child: Stack(
        children: [
          Positioned.fill(
              child: Container(color: Colors.amber.withOpacity(.4))),
          Positioned(
            left: position.dx,
            top: position.dy,
            child: Draggable(
              maxSimultaneousDrags: 1,
              feedback: widget.child,
              childWhenDragging: Opacity(
                opacity: .3,
                child: widget.child,
              ),
              onDragEnd: (details) => updatePosition(details.offset),
              child: Transform.scale(
                scale: scale,
                child: widget.child,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

You need to manage the position of the dragged item(s) with some kind of State Management. In the following code sample, I use Flutter Hooks useState.

enter image description here

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

const imgData =
    'data:image/jpeg;base64,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';

void main() {
  runApp(
    MaterialApp(
      title: 'Flutter Demo',
      home: Scaffold(
        body: DragArea(
          child: Image.network(imgData),
        ),
      ),
    ),
  );
}

class DragArea extends HookWidget {
  final Widget child;

  const DragArea({Key key, this.child}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final position = useState(Offset(100, 100));
    return Stack(
      children: [
        Positioned(
          left: position.value.dx,
          top: position.value.dy,
          child: Draggable(
            feedback: child,
            childWhenDragging: Opacity(
              opacity: .3,
              child: child,
            ),
            onDragEnd: (details) => position.value = details.offset,
            child: child,
          ),
        )
      ],
    );
  }
}

Stateful version

as requested by anikait

class StatefulDragArea extends StatefulWidget {
  final Widget child;

  const StatefulDragArea({Key key, this.child}) : super(key: key);

  @override
  _DragAreaStateStateful createState() => _DragAreaStateStateful();
}

class _DragAreaStateStateful extends State<StatefulDragArea> {
  Offset position = Offset(100, 100);

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        Positioned(
          left: position.dx,
          top: position.dy,
          child: Draggable(
            feedback: widget.child,
            childWhenDragging: Opacity(
              opacity: .3,
              child: widget.child,
            ),
            onDragEnd: (details) => setState(() => position = details.offset),
            child: widget.child,
          ),
        )
      ],
    );
  }
}
Related