diff --git a/example/android/gradle.properties b/example/android/gradle.properties index fbee1d8cd..195a4efd6 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -1,2 +1,4 @@ org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true +android.builtInKotlin=false +android.newDsl=false diff --git a/lib/src/layer/tile_layer/tile_image.dart b/lib/src/layer/tile_layer/tile_image.dart index 71fe06a54..34d9556a7 100644 --- a/lib/src/layer/tile_layer/tile_image.dart +++ b/lib/src/layer/tile_layer/tile_image.dart @@ -59,6 +59,8 @@ class TileImage extends ChangeNotifier { ImageStream? _imageStream; late ImageStreamListener _listener; + Timer? _loadDelayTimer; + /// Create a new object for a tile image. TileImage({ required this.vsync, @@ -132,28 +134,33 @@ class TileImage extends ChangeNotifier { } /// Initiate loading of the image. - void load() { - if (cancelLoading.isCompleted) return; + Future load({Duration delay = Duration.zero}) async { + void startLoad() { + if (cancelLoading.isCompleted) return; - loadStarted = DateTime.now(); + loadStarted = DateTime.now(); - try { - final oldImageStream = _imageStream; - _imageStream = imageProvider.resolve(ImageConfiguration.empty); + try { + final oldImageStream = _imageStream; + _imageStream = imageProvider.resolve(ImageConfiguration.empty); - if (_imageStream!.key != oldImageStream?.key) { - oldImageStream?.removeListener(_listener); + if (_imageStream!.key != oldImageStream?.key) { + oldImageStream?.removeListener(_listener); - _listener = ImageStreamListener( - _onImageLoadSuccess, - onError: _onImageLoadError, - ); - _imageStream!.addListener(_listener); + _listener = ImageStreamListener( + _onImageLoadSuccess, + onError: _onImageLoadError, + ); + _imageStream!.addListener(_listener); + } + } catch (e, s) { + // Make sure all exceptions are handled - #444 / #536 + _onImageLoadError(e, s); } - } catch (e, s) { - // Make sure all exceptions are handled - #444 / #536 - _onImageLoadError(e, s); } + + if (delay <= Duration.zero) return startLoad(); + _loadDelayTimer = Timer(delay, startLoad); } void _onImageLoadSuccess(ImageInfo imageInfo, bool synchronousCall) { @@ -233,6 +240,7 @@ class TileImage extends ChangeNotifier { } } + _loadDelayTimer?.cancel(); cancelLoading.complete(); _readyToDisplay = false; diff --git a/lib/src/layer/tile_layer/tile_image_manager.dart b/lib/src/layer/tile_layer/tile_image_manager.dart index a581b3992..fbc373ad9 100644 --- a/lib/src/layer/tile_layer/tile_image_manager.dart +++ b/lib/src/layer/tile_layer/tile_image_manager.dart @@ -182,7 +182,7 @@ class TileImageManager { tileBounds.atZoom(tile.coordinates.z).wrap(tile.coordinates), layer, ); - tile.load(); + tile.load(delay: layer.tileLoadDelay); } } diff --git a/lib/src/layer/tile_layer/tile_layer.dart b/lib/src/layer/tile_layer/tile_layer.dart index c6100c40a..32ea4a4f3 100644 --- a/lib/src/layer/tile_layer/tile_layer.dart +++ b/lib/src/layer/tile_layer/tile_layer.dart @@ -227,6 +227,24 @@ class TileLayer extends StatefulWidget { /// the [key]. final TileUpdateTransformer tileUpdateTransformer; + /// Duration between a tile becoming 'alive' (built/viewed) and it beginning + /// to load its resource image. + /// + /// This allows tiles which are visible only very briefly (for example, during + /// fast gestures) to be destroyed (unviewed) without wasting resources (such + /// as network usage and metered tile loads) trying to load its image. This + /// may speed up loading of other images. + /// + /// Note that destroyed tiles always stop loading if they are - but this + /// prevents even the start of their loading. + /// + /// To change how map events and gestures influence how tiles load, such as + /// to delay all tile loading until a gesture has paused, see + /// [tileUpdateTransformer]. + /// + /// Defaults to 30ms. Set to `Duration.zero` to disable delay. + final Duration tileLoadDelay; + /// Create a new [TileLayer] for the [FlutterMap] widget. TileLayer({ super.key, @@ -261,6 +279,7 @@ class TileLayer extends StatefulWidget { this.reset, this.tileBounds, TileUpdateTransformer? tileUpdateTransformer, + this.tileLoadDelay = const Duration(milliseconds: 30), String userAgentPackageName = 'unknown', }) : assert( tileDisplay.when( @@ -712,7 +731,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { // Create the new Tiles. for (final tile in tilesToLoad) { - tile.load(); + tile.load(delay: widget.tileLoadDelay); } } diff --git a/lib/src/layer/tile_layer/tile_update_transformer.dart b/lib/src/layer/tile_layer/tile_update_transformer.dart index 2b1fb565e..c1a290a7e 100644 --- a/lib/src/layer/tile_layer/tile_update_transformer.dart +++ b/lib/src/layer/tile_layer/tile_update_transformer.dart @@ -104,9 +104,9 @@ abstract class TileUpdateTransformers { /// This may improve performance, and reduce the number of tile requests, but /// at the expense of UX: new tiles will not be loaded until [duration] after /// the final tile load event in a series. For example, a fling gesture will - /// not load new tiles during its animation, only at the end. Best used in - /// combination with the cancellable tile provider, for even more fine-tuned - /// optimization. + /// not load new tiles during its animation, only at the end. + /// + /// A more balanced tradeoff is the setting of [TileLayer.tileLoadDelay]. /// /// Implementation follows that in /// ['package:stream_transform'](https://pub.dev/documentation/stream_transform/latest/stream_transform/RateLimit/debounce.html). diff --git a/test/flutter_map_test.dart b/test/flutter_map_test.dart index 6d9599380..eb80388eb 100644 --- a/test/flutter_map_test.dart +++ b/test/flutter_map_test.dart @@ -28,6 +28,11 @@ void main() { expect(find.byType(RawImage), findsWidgets); expect(find.byType(MarkerLayer), findsWidgets); expect(find.byType(FlutterLogo), findsOneWidget); + await tester.pumpAndSettle( + const Duration(milliseconds: 30), + EnginePhase.build, + const Duration(minutes: 1), + ); }); testWidgets( diff --git a/test/layer/circle_layer_test.dart b/test/layer/circle_layer_test.dart index a8cdb5cf7..038971227 100644 --- a/test/layer/circle_layer_test.dart +++ b/test/layer/circle_layer_test.dart @@ -30,5 +30,11 @@ void main() { find.descendant( of: find.byType(CircleLayer), matching: find.byType(CustomPaint)), findsOneWidget); + + await tester.pumpAndSettle( + const Duration(milliseconds: 30), + EnginePhase.build, + const Duration(minutes: 1), + ); }); } diff --git a/test/layer/marker_layer_test.dart b/test/layer/marker_layer_test.dart index 8834c41f5..dc53f695f 100644 --- a/test/layer/marker_layer_test.dart +++ b/test/layer/marker_layer_test.dart @@ -23,5 +23,10 @@ void main() { expect(find.byType(FlutterMap), findsOneWidget); expect(find.byType(MarkerLayer), findsWidgets); expect(find.byKey(key), findsOneWidget); + await tester.pumpAndSettle( + const Duration(milliseconds: 30), + EnginePhase.build, + const Duration(minutes: 1), + ); }); } diff --git a/test/layer/polygon_layer_test.dart b/test/layer/polygon_layer_test.dart index e87e01f9a..cb48d44fc 100644 --- a/test/layer/polygon_layer_test.dart +++ b/test/layer/polygon_layer_test.dart @@ -32,6 +32,12 @@ void main() { find.descendant( of: find.byType(PolygonLayer), matching: find.byType(CustomPaint)), findsOneWidget); + + await tester.pumpAndSettle( + const Duration(milliseconds: 30), + EnginePhase.build, + const Duration(minutes: 1), + ); }); test('polygon normal/rotation', () { diff --git a/test/layer/polyline_layer_test.dart b/test/layer/polyline_layer_test.dart index 9fdf43f44..49d4445e5 100644 --- a/test/layer/polyline_layer_test.dart +++ b/test/layer/polyline_layer_test.dart @@ -30,5 +30,11 @@ void main() { find.descendant( of: find.byType(PolylineLayer), matching: find.byType(CustomPaint)), findsOneWidget); + + await tester.pumpAndSettle( + const Duration(milliseconds: 30), + EnginePhase.build, + const Duration(minutes: 1), + ); }); } diff --git a/test/map/map_controller_test.dart b/test/map/map_controller_test.dart index fb6d649b7..6cd0a51d0 100644 --- a/test/map/map_controller_test.dart +++ b/test/map/map_controller_test.dart @@ -91,6 +91,12 @@ void main() { expect(camera.center, equals(expectedCenter)); expect(camera.zoom, equals(expectedZoom)); } + + await tester.pumpAndSettle( + const Duration(milliseconds: 30), + EnginePhase.build, + const Duration(minutes: 1), + ); }); testWidgets('test fit bounds methods with rotation', (tester) async { @@ -620,6 +626,12 @@ void main() { expectedCenter: const LatLng(1.2239447514276816, 31.954672909718134), expectedZoom: 5.368867444131886, ); + + await tester.pumpAndSettle( + const Duration(milliseconds: 30), + EnginePhase.build, + const Duration(minutes: 1), + ); }); testWidgets('test fit coordinates methods', (tester) async { @@ -818,6 +830,12 @@ void main() { expectedCenter: const LatLng(1.223944751427707, 31.954672909718177), expectedZoom: 5.368867444131889, ); + + await tester.pumpAndSettle( + const Duration(milliseconds: 30), + EnginePhase.build, + const Duration(minutes: 1), + ); }); testWidgets('test fit inside bounds with rotation', (tester) async { @@ -1373,5 +1391,11 @@ void main() { expectedCenter: const LatLng(1.2682880092901039, 31.90701622809375), expectedZoom: 5.728195363812886, ); + + await tester.pumpAndSettle( + const Duration(milliseconds: 30), + EnginePhase.build, + const Duration(minutes: 1), + ); }); }