Skip to content

Commit 58ff560

Browse files
authored
feat: added interactivity to CircleLayer & refactored interactivity out into seperate classes (#1886)
1 parent 7c99ae3 commit 58ff560

23 files changed

+537
-282
lines changed

example/lib/pages/circle.dart

+159-21
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,187 @@
1+
import 'package:flutter/foundation.dart';
12
import 'package:flutter/material.dart';
23
import 'package:flutter_map/flutter_map.dart';
34
import 'package:flutter_map_example/misc/tile_providers.dart';
45
import 'package:flutter_map_example/widgets/drawer/menu_drawer.dart';
56
import 'package:latlong2/latlong.dart';
67

7-
class CirclePage extends StatelessWidget {
8+
typedef HitValue = ({String title, String subtitle});
9+
10+
class CirclePage extends StatefulWidget {
811
static const String route = '/circle';
912

1013
const CirclePage({super.key});
1114

15+
@override
16+
State<CirclePage> createState() => _CirclePageState();
17+
}
18+
19+
class _CirclePageState extends State<CirclePage> {
20+
final LayerHitNotifier<HitValue> _hitNotifier = ValueNotifier(null);
21+
List<HitValue>? _prevHitValues;
22+
List<CircleMarker<HitValue>>? _hoverCircles;
23+
24+
final _circlesRaw = <CircleMarker<HitValue>>[
25+
CircleMarker(
26+
point: const LatLng(51.5, -0.09),
27+
color: Colors.white.withOpacity(0.7),
28+
borderColor: Colors.black,
29+
borderStrokeWidth: 2,
30+
useRadiusInMeter: false,
31+
radius: 100,
32+
hitValue: (title: 'White', subtitle: 'Radius in logical pixels'),
33+
),
34+
CircleMarker(
35+
point: const LatLng(51.5, -0.09),
36+
color: Colors.black.withOpacity(0.7),
37+
borderColor: Colors.black,
38+
borderStrokeWidth: 2,
39+
useRadiusInMeter: false,
40+
radius: 50,
41+
hitValue: (
42+
title: 'Black',
43+
subtitle: 'Radius in logical pixels, should be above white.',
44+
),
45+
),
46+
CircleMarker(
47+
point: const LatLng(51.4937, -0.6638),
48+
// Dorney Lake is ~2km long
49+
color: Colors.green.withOpacity(0.9),
50+
borderColor: Colors.black,
51+
borderStrokeWidth: 2,
52+
useRadiusInMeter: true,
53+
radius: 1000, // 1000 meters
54+
hitValue: (
55+
title: 'Green',
56+
subtitle: 'Radius in meters, calibrated over ~2km rowing lake'
57+
),
58+
),
59+
];
60+
late final _circles =
61+
Map.fromEntries(_circlesRaw.map((e) => MapEntry(e.hitValue, e)));
62+
1263
@override
1364
Widget build(BuildContext context) {
1465
return Scaffold(
15-
appBar: AppBar(title: const Text('Circle')),
16-
drawer: const MenuDrawer(route),
66+
appBar: AppBar(title: const Text('Circles')),
67+
drawer: const MenuDrawer(CirclePage.route),
1768
body: FlutterMap(
1869
options: const MapOptions(
1970
initialCenter: LatLng(51.5, -0.09),
2071
initialZoom: 11,
2172
),
2273
children: [
2374
openStreetMapTileLayer,
24-
CircleLayer(
25-
circles: [
26-
CircleMarker(
27-
point: const LatLng(51.5, -0.09),
28-
color: Colors.blue.withOpacity(0.7),
29-
borderColor: Colors.black,
30-
borderStrokeWidth: 2,
31-
useRadiusInMeter: true,
32-
radius: 2000, // 2000 meters
75+
MouseRegion(
76+
hitTestBehavior: HitTestBehavior.deferToChild,
77+
cursor: SystemMouseCursors.click,
78+
onHover: (_) {
79+
final hitValues = _hitNotifier.value?.hitValues.toList();
80+
if (hitValues == null) return;
81+
82+
if (listEquals(hitValues, _prevHitValues)) return;
83+
_prevHitValues = hitValues;
84+
85+
final hoverCircles = hitValues.map((v) {
86+
final original = _circles[v]!;
87+
88+
return CircleMarker<HitValue>(
89+
point: original.point,
90+
radius: original.radius + 6.5,
91+
useRadiusInMeter: original.useRadiusInMeter,
92+
color: Colors.transparent,
93+
borderStrokeWidth: 15,
94+
borderColor: Colors.green,
95+
);
96+
}).toList();
97+
setState(() => _hoverCircles = hoverCircles);
98+
},
99+
onExit: (_) {
100+
_prevHitValues = null;
101+
setState(() => _hoverCircles = null);
102+
},
103+
child: GestureDetector(
104+
onTap: () => _openTouchedCirclesModal(
105+
'Tapped',
106+
_hitNotifier.value!.hitValues,
107+
_hitNotifier.value!.coordinate,
108+
),
109+
onLongPress: () => _openTouchedCirclesModal(
110+
'Long pressed',
111+
_hitNotifier.value!.hitValues,
112+
_hitNotifier.value!.coordinate,
113+
),
114+
onSecondaryTap: () => _openTouchedCirclesModal(
115+
'Secondary tapped',
116+
_hitNotifier.value!.hitValues,
117+
_hitNotifier.value!.coordinate,
33118
),
34-
CircleMarker(
35-
point: const LatLng(51.4937, -0.6638),
36-
// Dorney Lake is ~2km long
37-
color: Colors.green.withOpacity(0.9),
38-
borderColor: Colors.black,
39-
borderStrokeWidth: 2,
40-
useRadiusInMeter: true,
41-
radius: 1000, // 1000 meters
119+
child: CircleLayer(
120+
hitNotifier: _hitNotifier,
121+
circles: [
122+
..._circlesRaw,
123+
...?_hoverCircles,
124+
],
42125
),
43-
],
126+
),
44127
),
45128
],
46129
),
47130
);
48131
}
132+
133+
void _openTouchedCirclesModal(
134+
String eventType,
135+
List<HitValue> tappedCircles,
136+
LatLng coords,
137+
) {
138+
showModalBottomSheet<void>(
139+
context: context,
140+
builder: (context) => Padding(
141+
padding: const EdgeInsets.all(16),
142+
child: Column(
143+
crossAxisAlignment: CrossAxisAlignment.start,
144+
children: [
145+
const Text(
146+
'Tapped Circle(s)',
147+
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
148+
),
149+
Text(
150+
'$eventType at point: (${coords.latitude.toStringAsFixed(6)}, ${coords.longitude.toStringAsFixed(6)})',
151+
),
152+
const SizedBox(height: 8),
153+
Expanded(
154+
child: ListView.builder(
155+
itemBuilder: (context, index) {
156+
final tappedLineData = tappedCircles[index];
157+
return ListTile(
158+
leading: index == 0
159+
? const Icon(Icons.vertical_align_top)
160+
: index == tappedCircles.length - 1
161+
? const Icon(Icons.vertical_align_bottom)
162+
: const SizedBox.shrink(),
163+
title: Text(tappedLineData.title),
164+
subtitle: Text(tappedLineData.subtitle),
165+
dense: true,
166+
);
167+
},
168+
itemCount: tappedCircles.length,
169+
),
170+
),
171+
const SizedBox(height: 8),
172+
Align(
173+
alignment: Alignment.bottomCenter,
174+
child: SizedBox(
175+
width: double.infinity,
176+
child: OutlinedButton(
177+
onPressed: () => Navigator.pop(context),
178+
child: const Text('Close'),
179+
),
180+
),
181+
),
182+
],
183+
),
184+
),
185+
);
186+
}
49187
}

lib/flutter_map.dart

+5-4
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,15 @@ export 'package:flutter_map/src/layer/attribution_layer/rich/widget.dart';
2929
export 'package:flutter_map/src/layer/attribution_layer/simple.dart';
3030
export 'package:flutter_map/src/layer/circle_layer/circle_layer.dart';
3131
export 'package:flutter_map/src/layer/marker_layer/marker_layer.dart';
32-
export 'package:flutter_map/src/layer/misc/hit_detection.dart';
33-
export 'package:flutter_map/src/layer/misc/line_patterns/stroke_pattern.dart';
34-
export 'package:flutter_map/src/layer/misc/mobile_layer_transformer.dart';
35-
export 'package:flutter_map/src/layer/misc/translucent_pointer.dart';
3632
export 'package:flutter_map/src/layer/overlay_image_layer/overlay_image_layer.dart';
3733
export 'package:flutter_map/src/layer/polygon_layer/polygon_layer.dart';
3834
export 'package:flutter_map/src/layer/polyline_layer/polyline_layer.dart';
3935
export 'package:flutter_map/src/layer/scalebar/scalebar.dart';
36+
export 'package:flutter_map/src/layer/shared/layer_interactivity/layer_hit_notifier.dart';
37+
export 'package:flutter_map/src/layer/shared/layer_interactivity/layer_hit_result.dart';
38+
export 'package:flutter_map/src/layer/shared/line_patterns/stroke_pattern.dart';
39+
export 'package:flutter_map/src/layer/shared/mobile_layer_transformer.dart';
40+
export 'package:flutter_map/src/layer/shared/translucent_pointer.dart';
4041
export 'package:flutter_map/src/layer/tile_layer/tile_builder.dart';
4142
export 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart';
4243
export 'package:flutter_map/src/layer/tile_layer/tile_display.dart';

lib/src/layer/circle_layer/circle_layer.dart

+19-5
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,41 @@
1+
import 'dart:math';
12
import 'dart:ui';
23

34
import 'package:flutter/widgets.dart';
45
import 'package:flutter_map/flutter_map.dart';
6+
import 'package:flutter_map/src/layer/shared/layer_interactivity/internal_hit_detectable.dart';
57
import 'package:latlong2/latlong.dart' hide Path;
68

79
part 'circle_marker.dart';
810
part 'painter.dart';
911

1012
/// A layer that displays a list of [CircleMarker] on the map
1113
@immutable
12-
class CircleLayer extends StatelessWidget {
14+
class CircleLayer<R extends Object> extends StatelessWidget {
1315
/// The list of [CircleMarker]s.
14-
final List<CircleMarker> circles;
16+
final List<CircleMarker<R>> circles;
1517

16-
/// Create a new [CircleLayer] as a child for flutter map
17-
const CircleLayer({super.key, required this.circles});
18+
/// {@macro fm.lhn.layerHitNotifier.usage}
19+
final LayerHitNotifier<R>? hitNotifier;
20+
21+
/// Create a new [CircleLayer] as a child for [FlutterMap]
22+
const CircleLayer({
23+
super.key,
24+
required this.circles,
25+
this.hitNotifier,
26+
});
1827

1928
@override
2029
Widget build(BuildContext context) {
2130
final camera = MapCamera.of(context);
31+
2232
return MobileLayerTransformer(
2333
child: CustomPaint(
24-
painter: CirclePainter(circles, camera),
34+
painter: CirclePainter(
35+
circles: circles,
36+
camera: camera,
37+
hitNotifier: hitNotifier,
38+
),
2539
size: Size(camera.size.x, camera.size.y),
2640
isComplex: true,
2741
),

lib/src/layer/circle_layer/circle_marker.dart

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ part of 'circle_layer.dart';
33
/// Immutable marker options for [CircleMarker]. Circle markers are a more
44
/// simple and performant way to draw markers as the regular [Marker]
55
@immutable
6-
class CircleMarker {
6+
base class CircleMarker<R extends Object> extends HitDetectableElement<R> {
77
/// An optional [Key] for the [CircleMarker].
88
/// This key is not used internally.
99
final Key? key;
@@ -36,5 +36,6 @@ class CircleMarker {
3636
this.color = const Color(0xFF00FF00),
3737
this.borderStrokeWidth = 0.0,
3838
this.borderColor = const Color(0xFFFFFF00),
39+
super.hitValue,
3940
});
4041
}

0 commit comments

Comments
 (0)