Position widgets arbitrarily (based on x-y coordinates relative to parent) in card

2,873

Solution 1

you can use Transform like below

  class Marker extends StatelessWidget {
    final double x;
    final double y;

    Marker({this.x: 0.0, this.y: 0.0});

    @override
    Widget build(BuildContext context) {
      print("x: $x, y: $y");

      return Transform(
          transform: Matrix4.translationValues(x, y, 0.0), child: CircleAvatar());
    }
  }

you need to check your x,y constraints to bound the transform to a certain area

Edit:

this is a complete working code for how to constrain your marker for the bottom edge of the card

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

void main() {
  runApp(new MaterialApp(
    home: new Scaffold(
      body: RoomCard(room: Room()),
    ),
  ));
}

class Room {}

class RoomCard extends StatefulWidget {
  final Room room;

  RoomCard({
    @required this.room,
  }) : assert(room != null);

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

class _RoomCardState extends State<RoomCard> {
  double x = 0.0;
  double y = 0.0;

  @override
  Widget build(BuildContext context) {

    //This hight should be known or calculated for the Widget need to be moved
    const double markerHight = 50.0;

    double ymax = context.findRenderObject()?.paintBounds?.bottom ?? markerHight ;


    return SizedBox(
      height: 300.0,
      width: 400.0,
      child: GestureDetector(
        onPanUpdate: (p) {
          setState(() {
            x += p.delta.dx;
            y = (y+p.delta.dy) >ymax - markerHight ? ymax -markerHight : y+p.delta.dy;

          });
        },
        child: Card(
          child: Stack(
            children: <Widget>[

              Marker(
                x: x,
                y: y,
              ),
            ],
          ),
        ),
      ),
    );
  }
}

      class Marker extends StatelessWidget {
        final double x;
        final double y;

        Marker({this.x: 0.0, this.y: 0.0});

        @override
        Widget build(BuildContext context) {
          print("x: $x, y: $y");
          return Transform(
              transform: Matrix4.translationValues(x, y, 0.0), 
              child: CircleAvatar());
        }
      }

Solution 2

What you're looking for is probably either CustomSingleChildLayout or CustomMultiChildLayout.

Using CustomSingleChildLayout would look something like this:

class RoomCard extends StatefulWidget {
  @override
  _RoomCardState createState() => _RoomCardState();
}

class _RoomCardState extends State<RoomCard> {
  Offset position = Offset.zero;

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: 400.0,
      width: 400.0,
      child: GestureDetector(
        onPanUpdate: (p) {
          setState(() => position += p.delta);
        },
        child: CustomSingleChildLayout(
          delegate: MarkerLayoutDelegate(position),
          child: Marker(),
        ),
      ),
    );
  }
}

class CallableNotifier extends ChangeNotifier {
  void notify() {
    this.notifyListeners();
  }
}

class MarkerLayoutDelegate extends SingleChildLayoutDelegate with ChangeNotifier {
  Offset position;

  MarkerLayoutDelegate(this.position);

  @override
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
    return constraints.loosen();
  }

  @override
  Offset getPositionForChild(Size size, Size childSize) {
    return Offset(min(position.dx, size.width - childSize.width), min(position.dy, size.height - childSize.height));
  }

  @override
  bool shouldRelayout(MarkerLayoutDelegate oldDelegate) {
    return position != oldDelegate.position;
  }
}

class Marker extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      width: 30,
      height: 30,
      child: CircleAvatar(),
    );
  }
}

Or you could use a listener to do it in such a way that the main widget doesn't need to rebuild every time the position of the dot is changed:

class RoomCard extends StatefulWidget {
  @override
  _RoomCardState createState() => _RoomCardState();
}

class _RoomCardState extends State<RoomCard> {
  double x = 0.0;
  double y = 0.0;

  MarkerLayoutDelegate delegate = MarkerLayoutDelegate(relayout: CallableNotifier());

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: 400.0,
      width: 400.0,
      child: GestureDetector(
        onPanUpdate: (p) {
          delegate.position += p.delta;
        },
        child: CustomSingleChildLayout(
          delegate: delegate,
          child: Marker(),
        ),
      ),
    );
  }
}

class CallableNotifier extends ChangeNotifier {
  void notify() {
    this.notifyListeners();
  }
}

class MarkerLayoutDelegate extends SingleChildLayoutDelegate with ChangeNotifier {
  Offset _position;

  CallableNotifier _notifier;

  MarkerLayoutDelegate({CallableNotifier relayout, Offset initialPosition = Offset.zero})
      : _position = initialPosition,
        _notifier = relayout,
        super(relayout: relayout);

  set position(Offset position) {
    _position = position;
    _notifier.notifyListeners();
  }

  Offset get position => _position;

  @override
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
    return constraints.loosen();
  }

  @override
  Offset getPositionForChild(Size size, Size childSize) {
    return Offset(min(_position.dx, size.width - childSize.width), min(_position.dy, size.height - childSize.height));
  }

  @override
  bool shouldRelayout(MarkerLayoutDelegate oldDelegate) {
    return _position != oldDelegate._position;
  }
}

class Marker extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      width: 30,
      height: 30,
      child: CircleAvatar(),
    );
  }
}
Share:
2,873
Harsh Bhikadia
Author by

Harsh Bhikadia

curious-mind.

Updated on December 07, 2022

Comments

  • Harsh Bhikadia
    Harsh Bhikadia over 1 year

    I want a to place a dot on a card, that can be moved arbitrarily inside the card.

    This is my solution so far.

    class RoomCard extends StatefulWidget {
      final Room room;
    
      RoomCard({
        @required this.room,
      }) : assert(room != null);
    
      @override
      _RoomCardState createState() => _RoomCardState();
    }
    
    class _RoomCardState extends State<RoomCard> {
      double x = 0.0;
      double y = 0.0;
      @override
      Widget build(BuildContext context) {
        return SizedBox(
          height: 400.0,
          width: 400.0,
          child: GestureDetector(
            onPanUpdate: (p) {
              setState(() {
                x += p.delta.dx;
                y += p.delta.dy;
              });
            },
            child: Card(
              child: Stack(
                children: <Widget>[
                  Marker(
                    x: x,
                    y: y,
                  ),
                ],
              ),
            ),
          ),
        );
      }
    }
    
    class Marker extends StatelessWidget {
      final double x;
      final double y;
    
      Marker({this.x: 0.0, this.y: 0.0});
    
      @override
      Widget build(BuildContext context) {
        print("x: $x, y: $y");
        return Padding(
          padding: EdgeInsets.only(left: x, top: y),
          child: CircleAvatar(),
        );
      }
    }
    

    I couldn't find any other way to place the marker in card based on x,y location except for using Padding widget to do that. Let me know if there is some another better way to do it.

    Secondly, this works for first time (moving it for the first time). Having issue while moving it afterwards. Am I missing any logic here?

    I want to further extend this to have multiple of such dots in the card that can be irritably placed and moved.

    I am happy if you can suggest any 3rd party packages that do this.