Reputation: 10875
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.
Upvotes: 5
Views: 4998
Reputation: 6861
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());
}
}
Upvotes: 5
Reputation: 40433
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(),
);
}
}
Upvotes: 4