2

Stack of big image and small image, and I want to be able to move the small image around the bounds of the big image, but not outside

Hi,

As the title says, I want to be able to move an image inside another one, but restrict its position to be inside of the "big" image.

I am ussing Draggable, and DragTarget, but I don't know if that's the best approach.

@override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(defaultPadding),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Expanded(
            flex: 5,
            child: Stack(
              children: <Widget>[
                DragTarget<int>(
                  builder: (context, candidateData, rejectedData) => Image.asset(
                    'images/big_image.png',
                    semanticLabel: 'Big Image',
                    fit: BoxFit.fill,
                  ),
                ),
                Draggable<int>(
                  data: 1,
                  feedback: Container(
                    color: Colors.deepOrange,
                    height: 40,
                    width: 40,
                    child: const Icon(Icons.directions_run),
                  ),
                  childWhenDragging: Container(
                    height: 40.0,
                    width: 40.0,
                    color: Colors.pinkAccent,
                    child: const Center(child: Text('Child When Dragging')),
                  ),
                  child: Image.asset(
                    'images/small_image.png',
                    semanticLabel: 'SmallImage',
                    fit: BoxFit.fill,
                  ), 
                ),
              ],
            ),
          ),
          const Text('Text'),
        ],
      ),
    );
  }

any idea how to do it? is draggable the best widget for this?

1

1 Answer 1

2

It's kinda tricky, but I managed to do it with a CustomPainter and a GestureDetector:

So the CustomPainter could look something like this, since I used a circular image, I make the calculations of the click based on a Circular equation to determine if the point is inside the circle.

class Painter extends CustomPainter {
  const Painter({
    required this.image,
    required this.xPosition,
    required this.yPosition,
  });

  final ui.Image image;
  final double xPosition;
  final double yPosition;

  final desiredDimension = 80.0;

  bool isRegion(double checkX, double checkY) {
    //Check inside Square Region
    // if (checkX >= xPosition - desiredDimension / 2 &&
    //     checkX <= xPosition + desiredDimension / 2 &&
    //     checkY >= yPosition - desiredDimension / 2 &&
    //     checkY <= yPosition + desiredDimension / 2)

    //Check inside Circle Region
    if ((pow(xPosition - checkX, 2) + pow(yPosition - checkY, 2)) <=
        pow(desiredDimension / 2, 2)) {
      return true;
    }
    return false;
  }

  @override
  void paint(Canvas canvas, Size size) {
    final originalWidth = image.width.toDouble();
    final originalHeight = image.height.toDouble();
    final scale = desiredDimension / originalHeight;
    final scaledWidth = originalWidth * scale;

    final rect = Rect.fromLTWH(
      xPosition - scaledWidth / 2,
      yPosition - desiredDimension / 2,
      scaledWidth,
      desiredDimension,
    );

    paintImage(
      canvas: canvas,
      image: image,
      rect: rect,
      fit: BoxFit.fill,
    );
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => true;
}

And I modify your Widget code for the screen, using a StatefulWidget, of course:

import 'dart:math';
import 'dart:ui' as ui show Image;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';


class Demo extends StatefulWidget {
  const Demo({super.key});

  @override
  State<Demo> createState() => _DemoState();
}

class _DemoState extends State<Demo> {
  late Painter _painter;
  bool _isClick = false;
  bool _isLoading = true;
  double xPos = 100;
  double yPos = 100;
  ui.Image? myImage;

  final bigImageHeight = 600.0;
  final bigImageWidth = 400.0;
  final smallImageDimension = 80.0;

  @override
  void initState() {
    super.initState();
    initialize();
  }

  void initialize() async {
    const keyName = 'assets/images/circle.png';
    final data = (await rootBundle.load(keyName));
    final bytes = data.buffer.asUint8List();
    myImage = await decodeImageFromList(bytes);

    _painter = Painter(
      image: myImage!,
      xPosition: xPos,
      yPosition: yPos,
    );
    _isLoading = false;
 }


  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(12.0),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Expanded(
            flex: 5,
            child: SizedBox(
              height: bigImageHeight,
              width: bigImageWidth,
              child: _isLoading
                  ? const Center(
                      child: SizedBox.square(
                        dimension: 12.0,
                        child: CircularProgressIndicator(),
                      ),
                    )
                  : GestureDetector(
                      onHorizontalDragDown: (details) {
                        setState(
                          () {
                            if (_painter.isRegion(
                              details.localPosition.dx,
                              details.localPosition.dy,
                            )) {
                              _isClick = true;
                            }
                          },
                        );
                      },
                      onHorizontalDragEnd: (details) {
                        setState(() {
                          _isClick = false;
                        });
                      },
                      onHorizontalDragUpdate: (details) {
                        if (_isClick) {
                          if (details.localPosition.dx <
                                  (bigImageWidth - smallImageDimension / 2) &&
                              details.localPosition.dx >
                                  smallImageDimension / 2) {
                            xPos = details.localPosition.dx;
                          }
                          if (details.localPosition.dy <
                                  (bigImageHeight - smallImageDimension / 2) &&
                              details.localPosition.dy >
                                  smallImageDimension / 2) {
                            yPos = details.localPosition.dy;
                          }
                          _painter = Painter(
                            image: myImage!,
                            xPosition: xPos,
                            yPosition: yPos,
                          );

                          if (mounted) {
                            setState(() {});
                          }
                        }
                      },
                      child: MouseRegion(
                        cursor: _isClick
                            ? SystemMouseCursors.grabbing
                            : SystemMouseCursors.grab,
                        child: RepaintBoundary(
                          key: GlobalKey(),
                          child: Stack(
                            children: [
                              Container(
                                height: bigImageHeight,
                                width: bigImageWidth,
                                decoration: _isClick
                                    ? BoxDecoration(
                                        color: Colors.grey.shade200,
                                        borderRadius:
                                            BorderRadius.circular(12.0),
                                      )
                                    : null,
                                child: Padding(
                                  padding: const EdgeInsets.all(12.0),
                                  child: Image.asset(
                                    'assets/images/book_1.jpeg',
                                    semanticLabel: 'Big Image',
                                    fit: BoxFit.fill,
                                  ),
                                ),
                              ),
                              SizedBox(
                                width: smallImageDimension,
                                height: smallImageDimension,
                                child: CustomPaint(
                                  painter: _painter,
                                ),
                              ),
                            ],
                          ),
                        ),
                      ),
                    ),
            ),
          ),
          const Text('Text'),
        ],
      ),
    );
  }
}

Result:

enter image description here
https://raw.githubusercontent.com/AlejoCumpa/flutter-varios/ab1744473c30168544d1fa772ed73d2d90efe4fe/assets/drag-demo.gif

Sign up to request clarification or add additional context in comments.

2 Comments

thanks for your answer. I am new to Flutter, so I still need to digest your answer, I was really hopping it would be easier. Thanks anyway!
It's because of the behavior you want, draggable widgets are using to drag items from one place to other, like dragging a file to upload or something like that, not to place images freely above others. If you share the desired feature, maybe we can help you also with the UX so it could be less complicated.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.