Open SheenaJacob opened 2 years ago
Thanks for this! Good spot and surprised I missed this. I haven't got a lot of time for the next week, but I will try and sort this when I get chance.
This may fix it for version 4 (readying for flutter_map v3), so I'll try and get this type of solution added there where I get chance. If you need a quick hack, look at the stuff with marker.point and markerPoint which is now saved by initState and amend anything with those in it.
It may be a few days before I get time to test this a bit more and push to Git and publish to pub.dev.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:latlong2/latlong.dart';
import 'package:flutter_map/plugin_api.dart';
class DragMarkers extends StatefulWidget {
final List<DragMarker> markers;
DragMarkers({Key? key, this.markers = const []});
@override
State<DragMarkers> createState() => _DragMarkersState();
}
class _DragMarkersState extends State<DragMarkers> {
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
var dragMarkers = <Widget>[];
FlutterMapState? mapState = FlutterMapState.maybeOf(context);
for (var marker in widget.markers) {
if (!_boundsContainsMarker(mapState, marker)) continue;
dragMarkers.add(DragMarkerWidget(
mapState: mapState,
marker: marker));
}
return Stack(children: dragMarkers);
}
static bool _boundsContainsMarker(FlutterMapState? map, DragMarker marker) {
var pixelPoint = map!.project(marker.point);
final width = marker.width - marker.anchor.left;
final height = marker.height - marker.anchor.top;
var sw = CustomPoint(pixelPoint.x + width, pixelPoint.y - height);
var ne = CustomPoint(pixelPoint.x - width, pixelPoint.y + height);
return map.pixelBounds.containsPartialBounds(Bounds(sw, ne));
}
}
class DragMarkerWidget extends StatefulWidget {
const DragMarkerWidget(
{Key? key,
this.mapState,
required this.marker,
AnchorPos? anchorPos})
//: anchor = Anchor.forPos(anchorPos, marker.width, marker.height);
: super(key: key);
final FlutterMapState? mapState;
//final Anchor anchor;
final DragMarker marker;
@override
State<DragMarkerWidget> createState() => _DragMarkerWidgetState();
}
class _DragMarkerWidgetState extends State<DragMarkerWidget> {
CustomPoint pixelPosition = const CustomPoint(0.0, 0.0);
late LatLng dragPosStart;
late LatLng markerPointStart;
late LatLng oldDragPosition;
bool isDragging = false;
late LatLng markerPoint;
static Timer? autoDragTimer;
@override
void initState() {
markerPoint = widget.marker.point;
super.initState();
}
@override
Widget build(BuildContext context) {
DragMarker marker = widget.marker;
updatePixelPos(markerPoint);
bool feedBackEnabled = isDragging && marker.feedbackBuilder != null;
Widget displayMarker = feedBackEnabled
? marker.feedbackBuilder!(context)
: marker.builder!(context);
return GestureDetector(
onPanStart: marker.useLongPress ? null : onPanStart,
onPanUpdate: marker.useLongPress ? null : onPanUpdate,
onPanEnd: marker.useLongPress ? null : onPanEnd,
onLongPressStart: marker.useLongPress ? onLongPanStart : null,
onLongPressMoveUpdate: marker.useLongPress ? onLongPanUpdate : null,
onLongPressEnd: marker.useLongPress ? onLongPanEnd : null,
onTap: () {
if (marker.onTap != null) {
marker.onTap!(markerPoint);
}
},
onLongPress: () {
if (marker.onLongPress != null) {
marker.onLongPress!(markerPoint);
}
},
child: Stack(children: [
Positioned(
width: marker.width,
height: marker.height,
left: pixelPosition.x +
((isDragging) ? marker.feedbackOffset.dx : marker.offset.dx),
top: pixelPosition.y +
((isDragging) ? marker.feedbackOffset.dy : marker.offset.dy),
child: widget.marker.rotateMarker
? Transform.rotate(
angle: -widget.mapState!.rotationRad, child: displayMarker)
: displayMarker)
]),
);
}
void updatePixelPos(point) {
DragMarker marker = widget.marker;
FlutterMapState? mapState = widget.mapState;
CustomPoint pos;
if (mapState != null) {
pos = mapState.project(point);
pos =
pos.multiplyBy(mapState.getZoomScale(mapState.zoom, mapState.zoom)) -
mapState.pixelOrigin;
pixelPosition = CustomPoint(
(pos.x - (marker.width - widget.marker.anchor.left)).toDouble(),
(pos.y - (marker.height - widget.marker.anchor.top)).toDouble());
}
}
void _start(Offset localPosition) {
isDragging = true;
dragPosStart = _offsetToCrs(localPosition);
markerPointStart =
LatLng(markerPoint.latitude, markerPoint.longitude);
}
void onPanStart(DragStartDetails details) {
_start(details.localPosition);
DragMarker marker = widget.marker;
if (marker.onDragStart != null) marker.onDragStart!(details, markerPoint);
}
void onLongPanStart(LongPressStartDetails details) {
_start(details.localPosition);
DragMarker marker = widget.marker;
if (marker.onLongDragStart != null) {
marker.onLongDragStart!(details, markerPoint);
}
}
void _pan(Offset localPosition) {
bool isDragging = true;
DragMarker marker = widget.marker;
FlutterMapState? mapState = widget.mapState;
var dragPos = _offsetToCrs(localPosition);
var deltaLat = dragPos.latitude - dragPosStart.latitude;
var deltaLon = dragPos.longitude - dragPosStart.longitude;
var pixelB = mapState?.getPixelBounds(mapState.zoom); //getLastPixelBounds();
var pixelPoint = mapState?.project(markerPoint);
/// If we're near an edge, move the map to compensate.
if (marker.updateMapNearEdge) {
/// How much we'll move the map by to compensate
var autoOffsetX = 0.0;
var autoOffsetY = 0.0;
if (pixelB != null && pixelPoint != null) {
if (pixelPoint.x + marker.width * marker.nearEdgeRatio >=
pixelB.topRight.x) autoOffsetX = marker.nearEdgeSpeed;
if (pixelPoint.x - marker.width * marker.nearEdgeRatio <=
pixelB.bottomLeft.x) autoOffsetX = -marker.nearEdgeSpeed;
if (pixelPoint.y - marker.height * marker.nearEdgeRatio <=
pixelB.topRight.y) autoOffsetY = -marker.nearEdgeSpeed;
if (pixelPoint.y + marker.height * marker.nearEdgeRatio >=
pixelB.bottomLeft.y) autoOffsetY = marker.nearEdgeSpeed;
}
/// Sometimes when dragging the onDragEnd doesn't fire, so just stops dead.
/// Here we allow a bit of time to keep dragging whilst user may move
/// around a bit to keep it going.
var lastTick = 0;
if (autoDragTimer != null) lastTick = autoDragTimer!.tick;
if ((autoOffsetY != 0.0) || (autoOffsetX != 0.0)) {
adjustMapToMarker(widget, autoOffsetX, autoOffsetY);
if ((autoDragTimer == null || autoDragTimer?.isActive == false) &&
(isDragging == true)) {
autoDragTimer =
Timer.periodic(const Duration(milliseconds: 10), (Timer t) {
var tick = autoDragTimer?.tick;
bool tickCheck = false;
if (tick != null) {
if (tick > lastTick + 15) {
tickCheck = true;
}
}
if (isDragging == false || tickCheck) {
autoDragTimer?.cancel();
} else {
/// Note, we may have adjusted a few lines up in same drag,
/// so could test for whether we've just done that
/// this, but in reality it seems to work ok as is.
adjustMapToMarker(widget, autoOffsetX, autoOffsetY);
}
});
}
}
}
setState(() {
markerPoint = LatLng(markerPointStart.latitude + deltaLat,
markerPointStart.longitude + deltaLon);
updatePixelPos(markerPoint);
});
}
void onPanUpdate(DragUpdateDetails details) {
_pan(details.localPosition);
DragMarker marker = widget.marker;
if (marker.onDragUpdate != null) {
marker.onDragUpdate!(details, markerPoint);
}
}
void onLongPanUpdate(LongPressMoveUpdateDetails details) {
_pan(details.localPosition);
DragMarker marker = widget.marker;
if (marker.onLongDragUpdate != null) {
marker.onLongDragUpdate!(details, markerPoint);
}
}
/// If dragging near edge of the screen, adjust the map so we keep dragging
void adjustMapToMarker(DragMarkerWidget widget, autoOffsetX, autoOffsetY) {
DragMarker marker = widget.marker;
FlutterMapState? mapState = widget.mapState;
var oldMapPos = mapState?.project(mapState.center);
LatLng? newMapLatLng;
CustomPoint<num>? oldMarkerPoint;
if (oldMapPos != null) {
newMapLatLng = mapState?.unproject(
CustomPoint(oldMapPos.x + autoOffsetX, oldMapPos.y + autoOffsetY));
oldMarkerPoint = mapState?.project(markerPoint);
}
if (mapState != null && newMapLatLng != null && oldMarkerPoint != null) {
markerPoint = mapState.unproject(CustomPoint(
oldMarkerPoint.x + autoOffsetX, oldMarkerPoint.y + autoOffsetY));
mapState.move(newMapLatLng, mapState.zoom, source: MapEventSource.onDrag);
}
}
void _end() {
isDragging = false;
if (autoDragTimer != null) autoDragTimer?.cancel();
}
void onPanEnd(details) {
_end();
if (widget.marker.onDragEnd != null) {
widget.marker.onDragEnd!(details, markerPoint);
}
setState(() {}); // Needed if using a feedback widget
}
void onLongPanEnd(details) {
_end();
if (widget.marker.onLongDragEnd != null) {
widget.marker.onLongDragEnd!(details, markerPoint);
}
setState(() {}); // Needed if using a feedback widget
}
static CustomPoint _offsetToPoint(Offset offset) {
return CustomPoint(offset.dx, offset.dy);
}
LatLng _offsetToCrs(Offset offset) {
// Get the widget's offset
var renderObject = context.findRenderObject() as RenderBox;
var width = renderObject.size.width;
var height = renderObject.size.height;
var mapState = widget.mapState;
// convert the point to global coordinates
var localPoint = _offsetToPoint(offset);
var localPointCenterDistance =
CustomPoint((width / 2) - localPoint.x, (height / 2) - localPoint.y);
if (mapState != null) {
var mapCenter = mapState.project(mapState.center);
var point = mapCenter - localPointCenterDistance;
return mapState.unproject(point);
}
return LatLng(0, 0);
}
}
class DragMarker {
LatLng point;
final WidgetBuilder? builder;
final WidgetBuilder? feedbackBuilder;
final double width;
final double height;
final Offset offset;
final Offset feedbackOffset;
final bool useLongPress;
final Function(DragStartDetails, LatLng)? onDragStart;
final Function(DragUpdateDetails, LatLng)? onDragUpdate;
final Function(DragEndDetails, LatLng)? onDragEnd;
final Function(LongPressStartDetails, LatLng)? onLongDragStart;
final Function(LongPressMoveUpdateDetails, LatLng)? onLongDragUpdate;
final Function(LongPressEndDetails, LatLng)? onLongDragEnd;
final Function(LatLng)? onTap;
final Function(LatLng)? onLongPress;
final bool updateMapNearEdge;
final double nearEdgeRatio;
final double nearEdgeSpeed;
final bool rotateMarker;
late Anchor anchor;
DragMarker({
required this.point,
this.builder,
this.feedbackBuilder,
this.width = 30.0,
this.height = 30.0,
this.offset = const Offset(0.0, 0.0),
this.feedbackOffset = const Offset(0.0, 0.0),
this.useLongPress = false,
this.onDragStart,
this.onDragUpdate,
this.onDragEnd,
this.onLongDragStart,
this.onLongDragUpdate,
this.onLongDragEnd,
this.onTap,
this.onLongPress,
this.updateMapNearEdge = false, // experimental
this.nearEdgeRatio = 1.5,
this.nearEdgeSpeed = 1.0,
this.rotateMarker = true,
AnchorPos? anchorPos,
}) {
anchor = Anchor.forPos(anchorPos, width, height);
}
}
Thanks for the quick response @ibrierley
@SheenaJacob can you confirm if the problem is solved to close this issue?
So the first part of the issue is now fixed, which means that a rebuild no longer triggers a change in the position of a marker while dragging. I'm still facing an issue when dragging a marker whose position is constantly updated on onDragUpdate() outside the border though. At some point, the marker gets stuck when moving it outside the border even though the drag action is still taking place.
For example, in the video below there are two markers. In the first part of the video the black marker is dragged outside the border and I can bring it back without facing any problems. On the other hand, when dragging the red marker outside, the drag event can no longer be recognized and at 0:05 seconds you can see that the position displayed at the bottom is no longer updated even though I'm still moving the mouse. The difference between the two markers is that the black marker's position is not updated, while the red marker's position is updated every time the onDragUpdate is called.
The code for the example above is :
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map/plugin_api.dart';
import 'package:flutter_map_dragmarker/dragmarker.dart';
import 'package:latlong2/latlong.dart';
class DragMarkerBoundaryTest extends StatefulWidget {
const DragMarkerBoundaryTest({super.key});
@override
State<DragMarkerBoundaryTest> createState() => _DragMarkerBoundaryTestState();
}
class _DragMarkerBoundaryTestState extends State<DragMarkerBoundaryTest> {
final LatLng _marker1Position = LatLng(44.1461, 9.9000);
LatLng _marker2Position = LatLng(44.1461, 10.1122);
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
floatingActionButton: Text(
' Marker1 Position : $_marker1Position \n Marker2 Position : $_marker2Position'),
body: Center(
child: FlutterMap(
options: MapOptions(
absorbPanEventsOnScrollables: false,
center: _marker1Position,
zoom: 10.4,
maxZoom: 18.0),
children: [
TileLayer(
urlTemplate:
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
subdomains: const ['a', 'b', 'c']),
DragMarkers(
markers: [
DragMarker(
point: _marker1Position,
width: 80.0,
height: 80.0,
offset: const Offset(0.0, -8.0),
builder: (ctx) => const Icon(Icons.location_on,
size: 50, color: Colors.black),
feedbackBuilder: (ctx) => const Icon(Icons.edit_location,
size: 50, color: Colors.black),
feedbackOffset: const Offset(0.0, -8.0),
),
DragMarker(
point: _marker2Position,
width: 80.0,
height: 80.0,
offset: const Offset(0.0, -8.0),
builder: (ctx) => const Icon(Icons.location_on,
size: 50, color: Colors.red),
feedbackBuilder: (ctx) => const Icon(Icons.edit_location,
size: 50, color: Colors.red),
feedbackOffset: const Offset(0.0, -8.0),
onDragUpdate: (details, point) {
setState(() {
_marker2Position = point;
});
},
)
],
)
],
),
),
),
);
}
}
Platform Tested on : macOS flutter_map: ^3.1.0 flutter_map_dragmarker: ^4.1.2
I'm not sure if this is the intended behavior or if it is not recommended to update the position onDragUpdate, but in my use case, I need the position of the marker when it's being dragged.
Hmm I don't think that's a good idea in general (currently), it's not currently intended for that to be updated iirc. However, what's the use case, because it provides the location of the marker, so not quite sure why you need to update it whilst it's mid drag ?
Ok. That makes sense. My current use case is to find the distance between two markers, and visually it would look something like this. So I need to update the position of the markers so that I can draw a line between the two points.
Ok, you could try updating the point, but not calling setState on the map out of interest, see what happens. I think the problem is that when setState is called, the gesture handlers lose their dragging (That would need a bit longer to test all of that to check if I'm going mad or not :))
Actually, scrap that I think, as you would probably need setState to update the lines...
One thing you could do, is take a look at my other plugin flutter_map_line_editor, as it seems to sort of what you want as a by product, and not have the issue you have (when it goes offscreen). It is using an older version of dragmarker tho, so that may have some effect. I don't have a lot of time to debug further into that atm tho, but it may help spot where the difference is.
Hehe. Yea. So I can use a workaround where I just have two variables that define the same point like this:
LatLng _markerPosition = LatLng(44.1461, 9.9000);
final LatLng _initialMarkerPosition = LatLng(44.1461, 9.9000);
DragMarker(
point: _initialMarkerPosition,
width: 80.0,
height: 80.0,
offset: const Offset(0.0, -8.0),
builder: (ctx) => const Icon(Icons.location_on,
size: 50, color: Colors.black),
feedbackBuilder: (ctx) => const Icon(Icons.edit_location,
size: 50, color: Colors.black),
feedbackOffset: const Offset(0.0, -8.0),
onDragUpdate: (details, point) {
setState(() {
_markerPosition = point;
});
},
),
but I don't understand why updating the value causes the gesture callbacks to stop working and also it only happens when it's dragged outside.
And I'll take a look at the flutter_map_line_editor. Thanks a ton : )
And I'll take a look at the flutter_map_line_editor. Thanks a ton : )
Be aware that https://github.com/ibrierley/flutter_map_line_editor/pull/36 might get merged, so that line editor reuses this package instead of its drag marker implementation and things can break for your use case.
Yep, I wasn't really meaning to use that, just to try and spot why that doesn't break the dragging off screen when calling setState in comparison.
Bug
Whenever a rebuild is triggered while dragging a DragMarker it returns to its initial position. The example below shows two such dragMarkers. Although the position of Marker1 (black) should change on dragging, rebuilding the widget causes it to always return to its initial position. Maker2 (red) on the other hand is continuously updated on Drag update which allows for it to be moved around, but when it is dragged outside the border of the map it sometimes gets stuck even though its still being dragged.
https://user-images.githubusercontent.com/60810797/186679853-96a03fe8-5fed-4088-8791-295e363ee9e8.mov
Expected Output:
A rebuild should not cause any change in the position of a Drag Marker while it is being dragged.
Reproducing the Bug: