Skip to content

Commit d4ec207

Browse files
committed
Add an onTap callback to polylines.
1 parent 1bee1be commit d4ec207

File tree

2 files changed

+140
-16
lines changed

2 files changed

+140
-16
lines changed

example/lib/pages/polyline.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class _PolylinePageState extends State<PolylinePage> {
3939
userAgentPackageName: 'dev.fleaflet.flutter_map.example',
4040
),
4141
PolylineLayer(
42+
interactive: true,
4243
polylines: [
4344
Polyline(
4445
points: [
@@ -72,6 +73,8 @@ class _PolylinePageState extends State<PolylinePage> {
7273
color: Colors.blue.withOpacity(0.6),
7374
borderStrokeWidth: 20,
7475
borderColor: Colors.red.withOpacity(0.4),
76+
onTap: (LatLng point) =>
77+
openDialog('blue line: $point'),
7578
),
7679
Polyline(
7780
points: [
@@ -83,6 +86,8 @@ class _PolylinePageState extends State<PolylinePage> {
8386
color: Colors.black.withOpacity(0.2),
8487
borderStrokeWidth: 20,
8588
borderColor: Colors.white30,
89+
onTap: (LatLng point) =>
90+
openDialog('black/white line: $point'),
8691
),
8792
Polyline(
8893
points: [
@@ -116,4 +121,15 @@ class _PolylinePageState extends State<PolylinePage> {
116121
),
117122
);
118123
}
124+
125+
Future<void> openDialog(String message) => showDialog<void>(
126+
context: context,
127+
builder: (BuildContext context) {
128+
return Dialog(
129+
child: Padding(
130+
padding: const EdgeInsets.all(8),
131+
child: Text(message),
132+
),
133+
);
134+
});
119135
}

lib/src/layer/polyline_layer.dart

Lines changed: 124 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'dart:core';
2+
import 'dart:math' as math;
23
import 'dart:ui' as ui;
34

45
import 'package:flutter/widgets.dart';
@@ -20,6 +21,7 @@ class Polyline {
2021
final StrokeCap strokeCap;
2122
final StrokeJoin strokeJoin;
2223
final bool useStrokeWidthInMeter;
24+
final void Function(LatLng point)? onTap;
2325

2426
LatLngBounds? _boundingBox;
2527

@@ -38,6 +40,7 @@ class Polyline {
3840
this.strokeCap = StrokeCap.round,
3941
this.strokeJoin = StrokeJoin.round,
4042
this.useStrokeWidthInMeter = false,
43+
this.onTap,
4144
});
4245

4346
/// Used to batch draw calls to the canvas.
@@ -54,45 +57,78 @@ class Polyline {
5457
useStrokeWidthInMeter);
5558
}
5659

60+
class _Hit {
61+
final Polyline polyline;
62+
final LatLng point;
63+
64+
const _Hit(this.polyline, this.point);
65+
}
66+
67+
class _LastHit {
68+
_Hit? hit;
69+
}
70+
5771
@immutable
5872
class PolylineLayer extends StatelessWidget {
5973
final List<Polyline> polylines;
60-
final bool polylineCulling;
74+
final bool interactive;
6175

6276
const PolylineLayer({
6377
super.key,
6478
required this.polylines,
65-
this.polylineCulling = false,
79+
//@Deprecated('Let's always cull')
80+
bool polylineCulling = true,
81+
this.interactive = false,
6682
});
6783

6884
@override
6985
Widget build(BuildContext context) {
7086
final map = MapCamera.of(context);
7187

88+
final lastHit = _LastHit();
89+
final paint = CustomPaint(
90+
painter: _PolylinePainter(
91+
polylines
92+
.where((p) => p.boundingBox.isOverlapping(map.visibleBounds))
93+
.toList(),
94+
map,
95+
interactive ? lastHit : null,
96+
),
97+
size: Size(map.size.x, map.size.y),
98+
isComplex: true,
99+
);
100+
101+
if (!interactive) {
102+
return MobileLayerTransformer(child: paint);
103+
}
104+
72105
return MobileLayerTransformer(
73-
child: CustomPaint(
74-
painter: PolylinePainter(
75-
polylineCulling
76-
? polylines
77-
.where((p) => p.boundingBox.isOverlapping(map.visibleBounds))
78-
.toList()
79-
: polylines,
80-
map,
81-
),
82-
size: Size(map.size.x, map.size.y),
83-
isComplex: true,
106+
child: GestureDetector(
107+
behavior: HitTestBehavior.deferToChild,
108+
onTap: () {
109+
final hit = lastHit.hit;
110+
if (hit == null) return;
111+
112+
final onTap = hit.polyline.onTap;
113+
if (onTap != null) {
114+
onTap(hit.point);
115+
}
116+
},
117+
child: paint,
84118
),
85119
);
86120
}
87121
}
88122

89-
class PolylinePainter extends CustomPainter {
123+
class _PolylinePainter extends CustomPainter {
90124
final List<Polyline> polylines;
91125

92126
final MapCamera map;
93127
final LatLngBounds bounds;
128+
final _LastHit? lastHit;
94129

95-
PolylinePainter(this.polylines, this.map) : bounds = map.visibleBounds;
130+
_PolylinePainter(this.polylines, this.map, this.lastHit)
131+
: bounds = map.visibleBounds;
96132

97133
int get hash => _hash ??= Object.hashAll(polylines);
98134

@@ -112,6 +148,59 @@ class PolylinePainter extends CustomPainter {
112148
);
113149
}
114150

151+
@override
152+
bool? hitTest(Offset position) {
153+
if (lastHit == null) {
154+
return null;
155+
}
156+
157+
final touch = map.pointToLatLng(math.Point(position.dx, position.dy));
158+
final origin = map.project(map.center).toOffset() - map.size.toOffset() / 2;
159+
160+
Polyline? hit;
161+
162+
outer:
163+
for (final p in polylines.reversed) {
164+
if (!p.boundingBox.contains(touch)) {
165+
continue;
166+
}
167+
168+
final offsets = getOffsets(origin, p.points);
169+
for (int i = 0; i < offsets.length - 1; i++) {
170+
final o1 = offsets[i];
171+
final o2 = offsets[i + 1];
172+
173+
final distance = math.sqrt(_distToSegmentSquared(
174+
position.dx,
175+
position.dy,
176+
o1.dx,
177+
o1.dy,
178+
o2.dx,
179+
o2.dy,
180+
));
181+
182+
if (distance < p.strokeWidth) {
183+
// We break out of the loop after we find the top-most candidate
184+
// polyline. However, we only register a hit if this polyline is
185+
// tappable. This let's (by design) non-interactive polylines
186+
// occlude polylines beneath.
187+
if (p.onTap != null) {
188+
hit = p;
189+
}
190+
break outer;
191+
}
192+
}
193+
}
194+
195+
if (hit != null) {
196+
lastHit!.hit = _Hit(hit, touch);
197+
return true;
198+
}
199+
200+
lastHit!.hit = null;
201+
return false;
202+
}
203+
115204
@override
116205
void paint(Canvas canvas, Size size) {
117206
final rect = Offset.zero & size;
@@ -291,9 +380,28 @@ class PolylinePainter extends CustomPainter {
291380
}
292381

293382
@override
294-
bool shouldRepaint(PolylinePainter oldDelegate) {
383+
bool shouldRepaint(_PolylinePainter oldDelegate) {
295384
return oldDelegate.bounds != bounds ||
296385
oldDelegate.polylines.length != polylines.length ||
297386
oldDelegate.hash != hash;
298387
}
299388
}
389+
390+
double _distanceSq(double x0, double y0, double x1, double y1) {
391+
final dx = x0 - x1;
392+
final dy = y0 - y1;
393+
return dx * dx + dy * dy;
394+
}
395+
396+
double _distToSegmentSquared(
397+
double px, double py, double x0, double y0, double x1, double y1) {
398+
final dx = x1 - x0;
399+
final dy = y1 - y0;
400+
final distanceSq = dx * dx + dy * dy;
401+
if (distanceSq == 0) {
402+
return _distanceSq(px, py, x0, y0);
403+
}
404+
405+
final t = (((px - x0) * dx + (py - y0) * dy) / distanceSq).clamp(0, 1);
406+
return _distanceSq(px, py, x0 + t * dx, y0 + t * dy);
407+
}

0 commit comments

Comments
 (0)