Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add direct support for debouncing TileLayer updates #1840

Merged
merged 9 commits into from
Mar 1, 2024
45 changes: 43 additions & 2 deletions lib/src/layer/tile_layer/tile_layer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,25 @@ class TileLayer extends StatefulWidget {
/// no affect.
final TileUpdateTransformer tileUpdateTransformer;

/// Defines the minimum delay time from last map event before the tile layers
/// are updated. This delay acts as a debounce period to prevent frequent
/// reloading of tile layers in response to rapid, successive events
/// (e.g., zooming or panning).
///
/// 16ms could be a good starting point for most applications.
/// This at 60fps this will wait one frame after the last event.
///
/// By setting this delay, we ensure that map layer updates are performed
/// only after a period of inactivity, enhancing performance and user
/// experience on lower performance devices.
///
/// - If multiple events occur within this delay period, only the last event
/// triggers the tile layer update, reducing unnecessary processing and
/// network requests.
/// - If the [loadingDelay] is `Duration.zero`, the delay is completely
/// disabled and the tile layer will update as soon as possible.
final Duration loadingDelay;

/// Create a new [TileLayer] for the [FlutterMap] widget.
TileLayer({
super.key,
Expand Down Expand Up @@ -239,6 +258,7 @@ class TileLayer extends StatefulWidget {
this.evictErrorTileStrategy = EvictErrorTileStrategy.none,
this.reset,
this.tileBounds,
this.loadingDelay = Duration.zero,
TileUpdateTransformer? tileUpdateTransformer,
String userAgentPackageName = 'unknown',
}) : assert(
Expand Down Expand Up @@ -333,6 +353,9 @@ class _TileLayerState extends State<TileLayer> with TickerProviderStateMixin {
TileRangeCalculator(tileSize: widget.tileSize);
late TileScaleCalculator _tileScaleCalculator;

/// Delay Timer for [TileLayer.loadingDelay]
Timer? _delayTimer;

// We have to hold on to the mapController hashCode to determine whether we
// need to reinitialize the listeners. didChangeDependencies is called on
// every map movement and if we unsubscribe and resubscribe every time we
Expand All @@ -347,6 +370,24 @@ class _TileLayerState extends State<TileLayer> with TickerProviderStateMixin {
_loadAndPruneInVisibleBounds(MapCamera.of(context));
});

/// This method is used to delay the execution of a function by the specified
/// [TileLayer.loadingDelay]. This is useful to prevent frequent reloading
/// of tile layers in response to rapid, successive events (e.g., zooming
/// or panning).
void _loadingDelay(VoidCallback action) {
//execute immediately if delay is zero.
if (widget.loadingDelay == Duration.zero) {
action();
return;
}

// Cancel the previous timer if it is still active.
_delayTimer?.cancel();

// Reset the timer to wait for the debounce duration
_delayTimer = Timer(widget.loadingDelay, action);
}

// This is called on every map movement so we should avoid expensive logic
// where possible.
@override
Expand All @@ -363,7 +404,7 @@ class _TileLayerState extends State<TileLayer> with TickerProviderStateMixin {
_tileUpdateSubscription = mapController.mapEventStream
.map((mapEvent) => TileUpdateEvent(mapEvent: mapEvent))
.transform(widget.tileUpdateTransformer)
.listen(_onTileUpdateEvent);
.listen((event) => _loadingDelay(() => _onTileUpdateEvent(event)));
}

var reloadTiles = false;
Expand Down Expand Up @@ -458,7 +499,7 @@ class _TileLayerState extends State<TileLayer> with TickerProviderStateMixin {
_resetSub?.cancel();
_pruneLater?.cancel();
widget.tileProvider.dispose();

_delayTimer?.cancel();
super.dispose();
}

Expand Down
Loading