Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

flutter

dart

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.

like image 550
Harsh Bhikadia Avatar asked Mar 05 '23 00:03

Harsh Bhikadia


2 Answers

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());
        }
      }
like image 92
Saed Nabil Avatar answered Mar 07 '23 20:03

Saed Nabil


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(),
    );
  }
}
like image 41
rmtmckenzie Avatar answered Mar 07 '23 19:03

rmtmckenzie