diff --git a/example/lib/main.dart b/example/lib/main.dart index b4798c21f..e4d59eca8 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -9,6 +9,7 @@ import 'package:flutter_map_example/pages/fallback_url_network_page.dart'; import 'package:flutter_map_example/pages/home.dart'; import 'package:flutter_map_example/pages/interactive_test_page.dart'; import 'package:flutter_map_example/pages/latlng_to_screen_point.dart'; +import 'package:flutter_map_example/pages/many_circles.dart'; import 'package:flutter_map_example/pages/many_markers.dart'; import 'package:flutter_map_example/pages/map_controller.dart'; import 'package:flutter_map_example/pages/map_inside_listview.dart'; @@ -60,6 +61,7 @@ class MyApp extends StatelessWidget { PluginZoomButtons.route: (context) => const PluginZoomButtons(), OfflineMapPage.route: (context) => const OfflineMapPage(), MovingMarkersPage.route: (context) => const MovingMarkersPage(), + ManyCirclesPage.route: (context) => const ManyCirclesPage(), CirclePage.route: (context) => const CirclePage(), OverlayImagePage.route: (context) => const OverlayImagePage(), PolygonPage.route: (context) => const PolygonPage(), diff --git a/example/lib/pages/many_circles.dart b/example/lib/pages/many_circles.dart new file mode 100644 index 000000000..a465713f8 --- /dev/null +++ b/example/lib/pages/many_circles.dart @@ -0,0 +1,94 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map_example/widgets/drawer.dart'; +import 'package:latlong2/latlong.dart'; + +const maxCirclesCount = 20000; + +/// On this page, [maxCirclesCount] circles are randomly generated +/// across europe, and then you can limit them with a slider +/// +/// This way, you can test how map performs under a lot of circles +class ManyCirclesPage extends StatefulWidget { + static const String route = '/many_circles'; + + const ManyCirclesPage({Key? key}) : super(key: key); + + @override + _ManyCirclesPageState createState() => _ManyCirclesPageState(); +} + +class _ManyCirclesPageState extends State { + double doubleInRange(Random source, num start, num end) => + source.nextDouble() * (end - start) + start; + List allCircles = []; + + int _sliderVal = maxCirclesCount ~/ 10; + + @override + void initState() { + super.initState(); + Future.microtask(() { + final r = Random(); + for (var x = 0; x < maxCirclesCount; x++) { + allCircles.add( + CircleMarker( + point: LatLng( + doubleInRange(r, 37, 55), + doubleInRange(r, -9, 30), + ), + color: Colors.red, + radius: 5, + ), + ); + } + setState(() {}); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('A lot of circles')), + drawer: buildDrawer(context, ManyCirclesPage.route), + body: Column( + children: [ + Slider( + min: 0, + max: maxCirclesCount.toDouble(), + divisions: maxCirclesCount ~/ 500, + label: 'Circles', + value: _sliderVal.toDouble(), + onChanged: (newVal) { + _sliderVal = newVal.toInt(); + setState(() {}); + }, + ), + Text('$_sliderVal circles'), + Flexible( + child: FlutterMap( + options: const MapOptions( + initialCenter: LatLng(50, 20), + initialZoom: 5, + interactionOptions: InteractionOptions( + flags: InteractiveFlag.all - InteractiveFlag.rotate, + ), + ), + children: [ + TileLayer( + urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + userAgentPackageName: 'dev.fleaflet.flutter_map.example', + ), + CircleLayer( + circles: allCircles.sublist( + 0, min(allCircles.length, _sliderVal))), + ], + ), + ), + ], + ), + ); + } +} diff --git a/example/lib/widgets/drawer.dart b/example/lib/widgets/drawer.dart index 7ed984e44..c76c9214e 100644 --- a/example/lib/widgets/drawer.dart +++ b/example/lib/widgets/drawer.dart @@ -10,6 +10,7 @@ import 'package:flutter_map_example/pages/fallback_url_network_page.dart'; import 'package:flutter_map_example/pages/home.dart'; import 'package:flutter_map_example/pages/interactive_test_page.dart'; import 'package:flutter_map_example/pages/latlng_to_screen_point.dart'; +import 'package:flutter_map_example/pages/many_circles.dart'; import 'package:flutter_map_example/pages/many_markers.dart'; import 'package:flutter_map_example/pages/map_controller.dart'; import 'package:flutter_map_example/pages/map_inside_listview.dart'; @@ -180,6 +181,12 @@ Drawer buildDrawer(BuildContext context, String currentRoute) { MovingMarkersPage.route, currentRoute, ), + _buildMenuItem( + context, + const Text('Many Circles'), + ManyCirclesPage.route, + currentRoute, + ), const Divider(), _buildMenuItem( context, diff --git a/lib/src/layer/circle_layer.dart b/lib/src/layer/circle_layer.dart index f4ea50ad6..759ec9e51 100644 --- a/lib/src/layer/circle_layer.dart +++ b/lib/src/layer/circle_layer.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:flutter/widgets.dart'; import 'package:flutter_map/src/map/camera/camera.dart'; import 'package:latlong2/latlong.dart' hide Path; @@ -32,30 +34,14 @@ class CircleLayer extends StatelessWidget { @override Widget build(BuildContext context) { - const distance = Distance(); + final map = MapCamera.of(context); return LayoutBuilder( builder: (context, bc) { final size = Size(bc.maxWidth, bc.maxHeight); - final map = MapCamera.of(context); - final circleWidgets = circles.map((circle) { - final offset = map.getOffsetFromOrigin(circle.point); - double? realRadius; - if (circle.useRadiusInMeter) { - final r = distance.offset(circle.point, circle.radius, 180); - final delta = offset - map.getOffsetFromOrigin(r); - realRadius = delta.distance; - } - return CustomPaint( - key: circle.key, - painter: CirclePainter( - circle, - offset: offset, - radius: realRadius ?? 0, - ), - size: size, - ); - }).toList(growable: false); - return Stack(children: circleWidgets); + return CustomPaint( + painter: CirclePainter(circles, map), + size: size, + ); }, ); } @@ -63,40 +49,77 @@ class CircleLayer extends StatelessWidget { @immutable class CirclePainter extends CustomPainter { - final CircleMarker circle; - final Offset offset; - final double radius; + final List circles; + final MapCamera map; - const CirclePainter( - this.circle, { - this.offset = Offset.zero, - this.radius = 0, - }); + const CirclePainter(this.circles, this.map); @override void paint(Canvas canvas, Size size) { + const distance = Distance(); final rect = Offset.zero & size; canvas.clipRect(rect); - final paint = Paint() - ..style = PaintingStyle.fill - ..color = circle.color; - _paintCircle(canvas, offset, - circle.useRadiusInMeter ? radius : circle.radius, paint); + // Let's calculate all the points grouped by color and radius + final points = >>{}; + final pointsBorder = >>{}; + for (final circle in circles) { + final offset = map.getOffsetFromOrigin(circle.point); + double radius = circle.radius; + if (circle.useRadiusInMeter) { + final r = distance.offset(circle.point, circle.radius, 180); + final delta = offset - map.getOffsetFromOrigin(r); + radius = delta.distance; + } + points[circle.color] ??= {}; + points[circle.color]![radius] ??= []; + points[circle.color]![radius]!.add(offset); + + if (circle.borderStrokeWidth > 0) { + double radiusBorder = circle.radius + circle.borderStrokeWidth; + if (circle.useRadiusInMeter) { + final rBorder = distance.offset(circle.point, radiusBorder, 180); + final deltaBorder = offset - map.getOffsetFromOrigin(rBorder); + radiusBorder = deltaBorder.distance; + } + pointsBorder[circle.borderColor] ??= {}; + pointsBorder[circle.borderColor]![radiusBorder] ??= []; + pointsBorder[circle.borderColor]![radiusBorder]!.add(offset); + } + } - if (circle.borderStrokeWidth > 0) { + // Now that all the points are grouped, let's draw them + // First by border in order to be under the circle + for (final color in pointsBorder.keys) { final paint = Paint() - ..style = PaintingStyle.stroke - ..color = circle.borderColor - ..strokeWidth = circle.borderStrokeWidth; + ..strokeCap = StrokeCap.round + ..isAntiAlias = false + ..color = color; + final pointsByRadius = pointsBorder[color]!; + for (final radius in pointsByRadius.keys) { + final pointsByRadiusColor = pointsByRadius[radius]!; + final radiusPaint = paint..strokeWidth = radius; + _paintCircle(canvas, pointsByRadiusColor, radiusPaint); + } + } - _paintCircle(canvas, offset, - circle.useRadiusInMeter ? radius : circle.radius, paint); + // And then the circle + for (final color in points.keys) { + final paint = Paint() + ..isAntiAlias = false + ..strokeCap = StrokeCap.round + ..color = color; + final pointsByRadius = points[color]!; + for (final radius in pointsByRadius.keys) { + final pointsByRadiusColor = pointsByRadius[radius]!; + final radiusPaint = paint..strokeWidth = radius; + _paintCircle(canvas, pointsByRadiusColor, radiusPaint); + } } } - void _paintCircle(Canvas canvas, Offset offset, double radius, Paint paint) { - canvas.drawCircle(offset, radius, paint); + void _paintCircle(Canvas canvas, List offsets, Paint paint) { + canvas.drawPoints(PointMode.points, offsets, paint); } @override diff --git a/test/layer/circle_layer_test.dart b/test/layer/circle_layer_test.dart index 3a2a30d42..b9c80bb1c 100644 --- a/test/layer/circle_layer_test.dart +++ b/test/layer/circle_layer_test.dart @@ -23,7 +23,13 @@ void main() { await tester.pumpWidget(TestApp(circles: circles)); expect(find.byType(FlutterMap), findsOneWidget); - expect(find.byType(CircleLayer), findsWidgets); - expect(find.byKey(key), findsOneWidget); + expect(find.byType(CircleLayer), findsOneWidget); + + // Assert that batching works and all circles are drawn into the same + // CustomPaint/Canvas. + expect( + find.descendant( + of: find.byType(CircleLayer), matching: find.byType(CustomPaint)), + findsOneWidget); }); }