From fb1c58fdb1b06be452a005cb7ed3ef2be0cd5f6a Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 5 Oct 2023 19:36:19 +0200 Subject: [PATCH 1/7] refactor: avoid using stack for circle layer --- lib/src/layer/circle_layer.dart | 74 ++++++++++++++------------------- 1 file changed, 31 insertions(+), 43 deletions(-) diff --git a/lib/src/layer/circle_layer.dart b/lib/src/layer/circle_layer.dart index f4ea50ad6..9e79f0de1 100644 --- a/lib/src/layer/circle_layer.dart +++ b/lib/src/layer/circle_layer.dart @@ -32,30 +32,14 @@ class CircleLayer extends StatelessWidget { @override Widget build(BuildContext context) { - const distance = Distance(); 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,36 +47,40 @@ 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) { - 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); + const distance = Distance(); + circles.forEach((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; + } - if (circle.borderStrokeWidth > 0) { + final rect = Offset.zero & size; + canvas.clipRect(rect); final paint = Paint() - ..style = PaintingStyle.stroke - ..color = circle.borderColor - ..strokeWidth = circle.borderStrokeWidth; + ..style = PaintingStyle.fill + ..color = circle.color; + + _paintCircle(canvas, offset, circle.useRadiusInMeter ? realRadius! : circle.radius, paint); + + if (circle.borderStrokeWidth > 0) { + final paint = Paint() + ..style = PaintingStyle.stroke + ..color = circle.borderColor + ..strokeWidth = circle.borderStrokeWidth; - _paintCircle(canvas, offset, - circle.useRadiusInMeter ? radius : circle.radius, paint); - } + _paintCircle(canvas, offset, circle.useRadiusInMeter ? realRadius! : circle.radius, paint); + } + }); } void _paintCircle(Canvas canvas, Offset offset, double radius, Paint paint) { From d82bcdca7a883877aa7199381e47e07ad9f0a7e9 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 5 Oct 2023 19:42:11 +0200 Subject: [PATCH 2/7] refactor: avoid clipRect for each circles --- lib/src/layer/circle_layer.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/src/layer/circle_layer.dart b/lib/src/layer/circle_layer.dart index 9e79f0de1..9e45e5ac1 100644 --- a/lib/src/layer/circle_layer.dart +++ b/lib/src/layer/circle_layer.dart @@ -32,10 +32,10 @@ class CircleLayer extends StatelessWidget { @override Widget build(BuildContext context) { + final map = MapCamera.of(context); return LayoutBuilder( builder: (context, bc) { final size = Size(bc.maxWidth, bc.maxHeight); - final map = MapCamera.of(context); return CustomPaint( painter: CirclePainter(circles, map), size: size, @@ -55,6 +55,9 @@ class CirclePainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { const distance = Distance(); + final rect = Offset.zero & size; + canvas.clipRect(rect); + circles.forEach((circle) { final offset = map.getOffsetFromOrigin(circle.point); double? realRadius; @@ -64,8 +67,6 @@ class CirclePainter extends CustomPainter { realRadius = delta.distance; } - final rect = Offset.zero & size; - canvas.clipRect(rect); final paint = Paint() ..style = PaintingStyle.fill ..color = circle.color; From c86090cdab57fb3fd4c46e885b07ef738440ecaa Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 5 Oct 2023 19:43:03 +0200 Subject: [PATCH 3/7] refactor: radius --- lib/src/layer/circle_layer.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/layer/circle_layer.dart b/lib/src/layer/circle_layer.dart index 9e45e5ac1..2c5444a50 100644 --- a/lib/src/layer/circle_layer.dart +++ b/lib/src/layer/circle_layer.dart @@ -60,18 +60,18 @@ class CirclePainter extends CustomPainter { circles.forEach((circle) { final offset = map.getOffsetFromOrigin(circle.point); - double? realRadius; + double radius = circle.radius; if (circle.useRadiusInMeter) { final r = distance.offset(circle.point, circle.radius, 180); final delta = offset - map.getOffsetFromOrigin(r); - realRadius = delta.distance; + radius = delta.distance; } final paint = Paint() ..style = PaintingStyle.fill ..color = circle.color; - _paintCircle(canvas, offset, circle.useRadiusInMeter ? realRadius! : circle.radius, paint); + _paintCircle(canvas, offset, radius, paint); if (circle.borderStrokeWidth > 0) { final paint = Paint() @@ -79,7 +79,7 @@ class CirclePainter extends CustomPainter { ..color = circle.borderColor ..strokeWidth = circle.borderStrokeWidth; - _paintCircle(canvas, offset, circle.useRadiusInMeter ? realRadius! : circle.radius, paint); + _paintCircle(canvas, offset, radius, paint); } }); } From 2ec62d23a716b4ade9437c7d3dee228f8d24e8c7 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 5 Oct 2023 23:28:57 +0200 Subject: [PATCH 4/7] fix: lint --- lib/src/layer/circle_layer.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/layer/circle_layer.dart b/lib/src/layer/circle_layer.dart index 2c5444a50..2562e594b 100644 --- a/lib/src/layer/circle_layer.dart +++ b/lib/src/layer/circle_layer.dart @@ -58,7 +58,7 @@ class CirclePainter extends CustomPainter { final rect = Offset.zero & size; canvas.clipRect(rect); - circles.forEach((circle) { + for (final circle in circles) { final offset = map.getOffsetFromOrigin(circle.point); double radius = circle.radius; if (circle.useRadiusInMeter) { @@ -81,7 +81,7 @@ class CirclePainter extends CustomPainter { _paintCircle(canvas, offset, radius, paint); } - }); + } } void _paintCircle(Canvas canvas, Offset offset, double radius, Paint paint) { From 5661eaa0c14fd5e9d97a23e52c7dd62ffdd956a9 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 5 Oct 2023 23:45:44 +0200 Subject: [PATCH 5/7] fix: tests --- test/layer/circle_layer_test.dart | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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); }); } From bc3cc999f284a89a5208e4e79a4df515a315bd10 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 Oct 2023 09:15:19 +0200 Subject: [PATCH 6/7] feat: use drawPoints to batch canvas draw as much as possible --- lib/src/layer/circle_layer.dart | 58 +++++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/lib/src/layer/circle_layer.dart b/lib/src/layer/circle_layer.dart index 2562e594b..97a3d73ef 100644 --- a/lib/src/layer/circle_layer.dart +++ b/lib/src/layer/circle_layer.dart @@ -58,6 +58,9 @@ class CirclePainter extends CustomPainter { final rect = Offset.zero & size; canvas.clipRect(rect); + // 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; @@ -66,26 +69,55 @@ class CirclePainter extends CustomPainter { final delta = offset - map.getOffsetFromOrigin(r); radius = delta.distance; } - - final paint = Paint() - ..style = PaintingStyle.fill - ..color = circle.color; - - _paintCircle(canvas, offset, radius, paint); + points[circle.color] ??= {}; + points[circle.color]![radius] ??= []; + points[circle.color]![radius]!.add(offset); if (circle.borderStrokeWidth > 0) { - final paint = Paint() - ..style = PaintingStyle.stroke - ..color = circle.borderColor - ..strokeWidth = circle.borderStrokeWidth; + 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); + } + } - _paintCircle(canvas, offset, radius, paint); + // 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() + ..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); + } + } + + // 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 From 201d1287dbb9c5980722430354f52a1bddb07653 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 Oct 2023 09:46:06 +0200 Subject: [PATCH 7/7] feat: many circles page --- example/lib/main.dart | 2 + example/lib/pages/many_circles.dart | 94 +++++++++++++++++++++++++++++ example/lib/widgets/drawer.dart | 7 +++ lib/src/layer/circle_layer.dart | 2 + 4 files changed, 105 insertions(+) create mode 100644 example/lib/pages/many_circles.dart 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 97a3d73ef..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;