Skip to content

Commit

Permalink
feat: added interactivity to CircleLayer & refactored interactivity…
Browse files Browse the repository at this point in the history
… out into seperate classes (#1886)
  • Loading branch information
JaffaKetchup authored May 23, 2024
1 parent 7c99ae3 commit 58ff560
Show file tree
Hide file tree
Showing 23 changed files with 537 additions and 282 deletions.
180 changes: 159 additions & 21 deletions example/lib/pages/circle.dart
Original file line number Diff line number Diff line change
@@ -1,49 +1,187 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_example/misc/tile_providers.dart';
import 'package:flutter_map_example/widgets/drawer/menu_drawer.dart';
import 'package:latlong2/latlong.dart';

class CirclePage extends StatelessWidget {
typedef HitValue = ({String title, String subtitle});

class CirclePage extends StatefulWidget {
static const String route = '/circle';

const CirclePage({super.key});

@override
State<CirclePage> createState() => _CirclePageState();
}

class _CirclePageState extends State<CirclePage> {
final LayerHitNotifier<HitValue> _hitNotifier = ValueNotifier(null);
List<HitValue>? _prevHitValues;
List<CircleMarker<HitValue>>? _hoverCircles;

final _circlesRaw = <CircleMarker<HitValue>>[
CircleMarker(
point: const LatLng(51.5, -0.09),
color: Colors.white.withOpacity(0.7),
borderColor: Colors.black,
borderStrokeWidth: 2,
useRadiusInMeter: false,
radius: 100,
hitValue: (title: 'White', subtitle: 'Radius in logical pixels'),
),
CircleMarker(
point: const LatLng(51.5, -0.09),
color: Colors.black.withOpacity(0.7),
borderColor: Colors.black,
borderStrokeWidth: 2,
useRadiusInMeter: false,
radius: 50,
hitValue: (
title: 'Black',
subtitle: 'Radius in logical pixels, should be above white.',
),
),
CircleMarker(
point: const LatLng(51.4937, -0.6638),
// Dorney Lake is ~2km long
color: Colors.green.withOpacity(0.9),
borderColor: Colors.black,
borderStrokeWidth: 2,
useRadiusInMeter: true,
radius: 1000, // 1000 meters
hitValue: (
title: 'Green',
subtitle: 'Radius in meters, calibrated over ~2km rowing lake'
),
),
];
late final _circles =
Map.fromEntries(_circlesRaw.map((e) => MapEntry(e.hitValue, e)));

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Circle')),
drawer: const MenuDrawer(route),
appBar: AppBar(title: const Text('Circles')),
drawer: const MenuDrawer(CirclePage.route),
body: FlutterMap(
options: const MapOptions(
initialCenter: LatLng(51.5, -0.09),
initialZoom: 11,
),
children: [
openStreetMapTileLayer,
CircleLayer(
circles: [
CircleMarker(
point: const LatLng(51.5, -0.09),
color: Colors.blue.withOpacity(0.7),
borderColor: Colors.black,
borderStrokeWidth: 2,
useRadiusInMeter: true,
radius: 2000, // 2000 meters
MouseRegion(
hitTestBehavior: HitTestBehavior.deferToChild,
cursor: SystemMouseCursors.click,
onHover: (_) {
final hitValues = _hitNotifier.value?.hitValues.toList();
if (hitValues == null) return;

if (listEquals(hitValues, _prevHitValues)) return;
_prevHitValues = hitValues;

final hoverCircles = hitValues.map((v) {
final original = _circles[v]!;

return CircleMarker<HitValue>(
point: original.point,
radius: original.radius + 6.5,
useRadiusInMeter: original.useRadiusInMeter,
color: Colors.transparent,
borderStrokeWidth: 15,
borderColor: Colors.green,
);
}).toList();
setState(() => _hoverCircles = hoverCircles);
},
onExit: (_) {
_prevHitValues = null;
setState(() => _hoverCircles = null);
},
child: GestureDetector(
onTap: () => _openTouchedCirclesModal(
'Tapped',
_hitNotifier.value!.hitValues,
_hitNotifier.value!.coordinate,
),
onLongPress: () => _openTouchedCirclesModal(
'Long pressed',
_hitNotifier.value!.hitValues,
_hitNotifier.value!.coordinate,
),
onSecondaryTap: () => _openTouchedCirclesModal(
'Secondary tapped',
_hitNotifier.value!.hitValues,
_hitNotifier.value!.coordinate,
),
CircleMarker(
point: const LatLng(51.4937, -0.6638),
// Dorney Lake is ~2km long
color: Colors.green.withOpacity(0.9),
borderColor: Colors.black,
borderStrokeWidth: 2,
useRadiusInMeter: true,
radius: 1000, // 1000 meters
child: CircleLayer(
hitNotifier: _hitNotifier,
circles: [
..._circlesRaw,
...?_hoverCircles,
],
),
],
),
),
],
),
);
}

void _openTouchedCirclesModal(
String eventType,
List<HitValue> tappedCircles,
LatLng coords,
) {
showModalBottomSheet<void>(
context: context,
builder: (context) => Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Tapped Circle(s)',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
Text(
'$eventType at point: (${coords.latitude.toStringAsFixed(6)}, ${coords.longitude.toStringAsFixed(6)})',
),
const SizedBox(height: 8),
Expanded(
child: ListView.builder(
itemBuilder: (context, index) {
final tappedLineData = tappedCircles[index];
return ListTile(
leading: index == 0
? const Icon(Icons.vertical_align_top)
: index == tappedCircles.length - 1
? const Icon(Icons.vertical_align_bottom)
: const SizedBox.shrink(),
title: Text(tappedLineData.title),
subtitle: Text(tappedLineData.subtitle),
dense: true,
);
},
itemCount: tappedCircles.length,
),
),
const SizedBox(height: 8),
Align(
alignment: Alignment.bottomCenter,
child: SizedBox(
width: double.infinity,
child: OutlinedButton(
onPressed: () => Navigator.pop(context),
child: const Text('Close'),
),
),
),
],
),
),
);
}
}
9 changes: 5 additions & 4 deletions lib/flutter_map.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,15 @@ export 'package:flutter_map/src/layer/attribution_layer/rich/widget.dart';
export 'package:flutter_map/src/layer/attribution_layer/simple.dart';
export 'package:flutter_map/src/layer/circle_layer/circle_layer.dart';
export 'package:flutter_map/src/layer/marker_layer/marker_layer.dart';
export 'package:flutter_map/src/layer/misc/hit_detection.dart';
export 'package:flutter_map/src/layer/misc/line_patterns/stroke_pattern.dart';
export 'package:flutter_map/src/layer/misc/mobile_layer_transformer.dart';
export 'package:flutter_map/src/layer/misc/translucent_pointer.dart';
export 'package:flutter_map/src/layer/overlay_image_layer/overlay_image_layer.dart';
export 'package:flutter_map/src/layer/polygon_layer/polygon_layer.dart';
export 'package:flutter_map/src/layer/polyline_layer/polyline_layer.dart';
export 'package:flutter_map/src/layer/scalebar/scalebar.dart';
export 'package:flutter_map/src/layer/shared/layer_interactivity/layer_hit_notifier.dart';
export 'package:flutter_map/src/layer/shared/layer_interactivity/layer_hit_result.dart';
export 'package:flutter_map/src/layer/shared/line_patterns/stroke_pattern.dart';
export 'package:flutter_map/src/layer/shared/mobile_layer_transformer.dart';
export 'package:flutter_map/src/layer/shared/translucent_pointer.dart';
export 'package:flutter_map/src/layer/tile_layer/tile_builder.dart';
export 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart';
export 'package:flutter_map/src/layer/tile_layer/tile_display.dart';
Expand Down
24 changes: 19 additions & 5 deletions lib/src/layer/circle_layer/circle_layer.dart
Original file line number Diff line number Diff line change
@@ -1,27 +1,41 @@
import 'dart:math';
import 'dart:ui';

import 'package:flutter/widgets.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map/src/layer/shared/layer_interactivity/internal_hit_detectable.dart';
import 'package:latlong2/latlong.dart' hide Path;

part 'circle_marker.dart';
part 'painter.dart';

/// A layer that displays a list of [CircleMarker] on the map
@immutable
class CircleLayer extends StatelessWidget {
class CircleLayer<R extends Object> extends StatelessWidget {
/// The list of [CircleMarker]s.
final List<CircleMarker> circles;
final List<CircleMarker<R>> circles;

/// Create a new [CircleLayer] as a child for flutter map
const CircleLayer({super.key, required this.circles});
/// {@macro fm.lhn.layerHitNotifier.usage}
final LayerHitNotifier<R>? hitNotifier;

/// Create a new [CircleLayer] as a child for [FlutterMap]
const CircleLayer({
super.key,
required this.circles,
this.hitNotifier,
});

@override
Widget build(BuildContext context) {
final camera = MapCamera.of(context);

return MobileLayerTransformer(
child: CustomPaint(
painter: CirclePainter(circles, camera),
painter: CirclePainter(
circles: circles,
camera: camera,
hitNotifier: hitNotifier,
),
size: Size(camera.size.x, camera.size.y),
isComplex: true,
),
Expand Down
3 changes: 2 additions & 1 deletion lib/src/layer/circle_layer/circle_marker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ part of 'circle_layer.dart';
/// Immutable marker options for [CircleMarker]. Circle markers are a more
/// simple and performant way to draw markers as the regular [Marker]
@immutable
class CircleMarker {
base class CircleMarker<R extends Object> extends HitDetectableElement<R> {
/// An optional [Key] for the [CircleMarker].
/// This key is not used internally.
final Key? key;
Expand Down Expand Up @@ -36,5 +36,6 @@ class CircleMarker {
this.color = const Color(0xFF00FF00),
this.borderStrokeWidth = 0.0,
this.borderColor = const Color(0xFFFFFF00),
super.hitValue,
});
}
Loading

0 comments on commit 58ff560

Please sign in to comment.