From f01363e72d88090de6ba5c5524f2aacd039d96c3 Mon Sep 17 00:00:00 2001 From: ruinivist Date: Sat, 5 Jul 2025 21:48:32 +0000 Subject: [PATCH] lazy build perf improvements: build extent from screen size + factor in scaling + refactor --- example/lib/app.dart | 21 +++++++++++----- example/lib/widgets/fps.dart | 39 +++++++++++++++++++++++++++++ lib/core/controller/controller.dart | 31 +++++++++++++---------- 3 files changed, 72 insertions(+), 19 deletions(-) create mode 100644 example/lib/widgets/fps.dart diff --git a/example/lib/app.dart b/example/lib/app.dart index fe70501..8bad91d 100644 --- a/example/lib/app.dart +++ b/example/lib/app.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:infinite_lazy_2d_grid/infinite_lazy_2d_grid.dart'; +import 'widgets/fps.dart'; + class App extends StatefulWidget { const App({super.key}); @@ -14,10 +16,12 @@ class _AppState extends State { @override void initState() { super.initState(); - - controller.addChild(const Offset(100, 100), () => const InfoContainer(color: Colors.red)); - controller.addChild(const Offset(200, 200), () => const InfoContainer(color: Colors.green)); - controller.addChild(const Offset(300, 300), () => const InfoContainer(color: Colors.blue)); + for (int i = 0; i < 10000; i++) { + controller.addChild( + Offset((i % 80) * 100.0, (i ~/ 80) * 100.0), + () => InfoContainer(color: Colors.primaries[i % Colors.primaries.length]), + ); + } } @override @@ -41,7 +45,12 @@ class _AppState extends State { ), ], ), - body: CanvasView(controller: controller, canvasBackground: SingleColorBackround(Colors.white)), + body: Stack( + children: [ + CanvasView(controller: controller, canvasBackground: SingleColorBackround(Colors.white)), + Positioned(bottom: 16, left: 16, child: Fps()), + ], + ), ); } } @@ -54,7 +63,7 @@ class InfoContainer extends StatelessWidget { Widget build(BuildContext context) { return GestureDetector( onTap: () { - print("tapped"); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Tapped a container'))); }, child: Container(color: color, width: 50, height: 50), ); diff --git a/example/lib/widgets/fps.dart b/example/lib/widgets/fps.dart new file mode 100644 index 0000000..f28a7ab --- /dev/null +++ b/example/lib/widgets/fps.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; + +class Fps extends StatefulWidget { + const Fps({super.key}); + + @override + State createState() => _FpsState(); +} + +class _FpsState extends State { + @override + void initState() { + SchedulerBinding.instance.addPersistentFrameCallback(_frame); + super.initState(); + } + + int _lastFrameTime = 0; + String _frameRate = "0 fps"; + void _frame(Duration elapsed) { + int elapsedMicroseconds = elapsed.inMicroseconds; + double elapsedSeconds = (elapsedMicroseconds - _lastFrameTime) * 1e-6; + if (elapsedSeconds != 0) { + _lastFrameTime = elapsedMicroseconds; + setState(() { + _frameRate = '${(1.0 / elapsedSeconds).toStringAsFixed(2)} fps'; + }); + } + // redraw if mounted + if (mounted) { + SchedulerBinding.instance.scheduleFrame(); + } + } + + @override + Widget build(BuildContext context) { + return Text(_frameRate); + } +} diff --git a/lib/core/controller/controller.dart b/lib/core/controller/controller.dart index 4004e64..a4b2f4b 100644 --- a/lib/core/controller/controller.dart +++ b/lib/core/controller/controller.dart @@ -14,9 +14,8 @@ class CanvasController with ChangeNotifier { double _baseScale, _scale; late Size _canvasSize; final Map _children = {}; // int for IDs - final Offset? buildCacheExtent; - late final Offset _buildExtent; - final Offset _initialBuildExtent; + final Offset? _buildCacheExtent; + late Offset _buildExtent; final Size _hashCellSize; bool _init = false; late final SpatialHashing _spatialHash; @@ -26,13 +25,14 @@ class CanvasController with ChangeNotifier { CanvasController({ double initialScale = 1, this.debug = false, - this.buildCacheExtent, - Size hashCellSize = const Size(200, 200), - Offset initialBuildExtent = const Offset(1000, 1000), + Offset? buildCacheExtent, + Size hashCellSize = const Size(100, 100), }) : _scale = initialScale, _baseScale = initialScale, _hashCellSize = hashCellSize, - _initialBuildExtent = initialBuildExtent, + _buildCacheExtent = buildCacheExtent != null ? buildCacheExtent + Offset(50, 50) : null, + // only top left is considered to if a widget has long width, it'll not be rendered + // unless the cache extent is sufficient assert(initialScale > 0, 'Initial scale must be greater than 0') { _spatialHash = SpatialHashing(cellSize: _hashCellSize); } @@ -48,13 +48,16 @@ class CanvasController with ChangeNotifier { /// Update the canvas size when the widget size changes. void onCanvasSizeChange(Size size) { + if (size == Size.zero) return; // ignore the zero side, linux first build pass error if (_init && size == _canvasSize) return; + _buildExtent = Offset(size.width, size.height) + (_buildCacheExtent ?? Offset(size.width * 0.1, size.height * 0.1)); _canvasSize = size; // allow resize due to canvas resize + + // if the first init, re-render as not I have the canvas size to build widgets if (!_init) { - _buildExtent = - Offset(size.width, size.height) + (buildCacheExtent ?? Offset(size.width * 0.5, size.height * 0.5)); _init = true; + Future.microtask(notifyListeners); } } @@ -138,9 +141,10 @@ class CanvasController with ChangeNotifier { /// Currently rendered widgets with their position info List widgetsWithScreenPositions() { - final idsToBuild = _init - ? _childrenWithinBuildArea(_gsCenter, _buildExtent) - : _childrenWithinBuildArea(Offset.zero, _initialBuildExtent); + if (!_init) return []; + + final idsToBuild = _childrenWithinBuildArea(_gsCenter, _buildExtent); + print("building ${idsToBuild.length} widgets at ${_gsCenter.toPoint()} with extent ${_buildExtent.toPoint()}"); return idsToBuild.map((id) { final item = _children[id]!; @@ -152,7 +156,8 @@ class CanvasController with ChangeNotifier { } List _childrenWithinBuildArea(Offset center, Offset extent) { - final items = _spatialHash.getPointsAround(center.toPoint(), extent); + Offset halfExtent = Offset((extent.dx / (2 * _scale)).ceilToDouble(), (extent.dy / (2 * _scale)).ceilToDouble()); + final items = _spatialHash.getPointsAround(center.toPoint(), halfExtent); return items.map((item) => item.data).toList(); // data is the child id here } }