Skip to content

Commit bcd0b7b

Browse files
authored
feat!: added TileUpdateTransformers.debounce & reverted #1840 (#1850)
1 parent 149f847 commit bcd0b7b

File tree

6 files changed

+231
-66
lines changed

6 files changed

+231
-66
lines changed

example/lib/main.dart

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:flutter_map_example/pages/bundled_offline_map.dart';
44
import 'package:flutter_map_example/pages/cancellable_tile_provider.dart';
55
import 'package:flutter_map_example/pages/circle.dart';
66
import 'package:flutter_map_example/pages/custom_crs/custom_crs.dart';
7+
import 'package:flutter_map_example/pages/debouncing_tile_update_transformer.dart';
78
import 'package:flutter_map_example/pages/epsg3413_crs.dart';
89
import 'package:flutter_map_example/pages/epsg4326_crs.dart';
910
import 'package:flutter_map_example/pages/fallback_url_page.dart';
@@ -86,6 +87,8 @@ class MyApp extends StatelessWidget {
8687
FallbackUrlPage.route: (context) => const FallbackUrlPage(),
8788
SecondaryTapPage.route: (context) => const SecondaryTapPage(),
8889
RetinaPage.route: (context) => const RetinaPage(),
90+
DebouncingTileUpdateTransformerPage.route: (context) =>
91+
const DebouncingTileUpdateTransformerPage(),
8992
},
9093
);
9194
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_map/flutter_map.dart';
3+
import 'package:flutter_map_example/widgets/drawer/menu_drawer.dart';
4+
import 'package:flutter_map_example/widgets/notice_banner.dart';
5+
import 'package:latlong2/latlong.dart';
6+
7+
class DebouncingTileUpdateTransformerPage extends StatefulWidget {
8+
static const String route = '/debouncing_tile_update_transformer_page';
9+
10+
const DebouncingTileUpdateTransformerPage({super.key});
11+
12+
@override
13+
State<DebouncingTileUpdateTransformerPage> createState() =>
14+
_DebouncingTileUpdateTransformerPageState();
15+
}
16+
17+
class _DebouncingTileUpdateTransformerPageState
18+
extends State<DebouncingTileUpdateTransformerPage> {
19+
int _changeEndKeyRefresher = 0;
20+
double _durationInMilliseconds = 20;
21+
22+
@override
23+
Widget build(BuildContext context) {
24+
return Scaffold(
25+
appBar: AppBar(title: const Text('Debouncing Tile Update Transformer')),
26+
drawer: const MenuDrawer(DebouncingTileUpdateTransformerPage.route),
27+
body: Column(
28+
children: [
29+
const NoticeBanner.informational(
30+
text:
31+
'This TileUpdateTransformer debounces TileUpdateEvents so they '
32+
"don't occur too frequently, which can improve performance and "
33+
'reduce tile requests.\nHowever, this does lead to reduced UX, '
34+
'as tiles will not be loaded during long movements or '
35+
'animations, resulting in the background grey breaking the '
36+
'illusion of a seamless map.',
37+
url:
38+
'https://docs.fleaflet.dev/layers/tile-layer#tile-update-transformers',
39+
sizeTransition: 1360,
40+
),
41+
Expanded(
42+
child: Stack(
43+
children: [
44+
FlutterMap(
45+
options: MapOptions(
46+
initialCenter: const LatLng(51.5, -0.09),
47+
initialZoom: 5,
48+
cameraConstraint: CameraConstraint.contain(
49+
bounds: LatLngBounds(
50+
const LatLng(-90, -180),
51+
const LatLng(90, 180),
52+
),
53+
),
54+
),
55+
children: [
56+
TileLayer(
57+
key: ValueKey('TileLayer-$_changeEndKeyRefresher'),
58+
urlTemplate:
59+
'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
60+
userAgentPackageName: 'dev.fleaflet.flutter_map.example',
61+
tileUpdateTransformer: TileUpdateTransformers.debounce(
62+
Duration(milliseconds: _durationInMilliseconds.toInt()),
63+
),
64+
),
65+
],
66+
),
67+
Positioned(
68+
left: 16,
69+
top: 16,
70+
right: 16,
71+
child: DecoratedBox(
72+
decoration: BoxDecoration(
73+
color: Theme.of(context).colorScheme.background,
74+
borderRadius: BorderRadius.circular(32),
75+
),
76+
child: Padding(
77+
padding: const EdgeInsets.only(
78+
left: 16, right: 8, top: 4, bottom: 4),
79+
child: Row(
80+
children: [
81+
const Tooltip(
82+
message: 'Adjust Duration',
83+
child: Icon(Icons.timer),
84+
),
85+
Expanded(
86+
child: Slider.adaptive(
87+
value: _durationInMilliseconds,
88+
onChanged: (v) =>
89+
setState(() => _durationInMilliseconds = v),
90+
onChangeEnd: (v) =>
91+
setState(() => _changeEndKeyRefresher++),
92+
min: 0,
93+
max: 500,
94+
divisions: 100,
95+
label: _durationInMilliseconds == 0
96+
? 'Instant/No Debounce'
97+
: '${_durationInMilliseconds.toInt()} ms',
98+
),
99+
),
100+
],
101+
),
102+
),
103+
),
104+
)
105+
],
106+
),
107+
),
108+
],
109+
),
110+
);
111+
}
112+
}

example/lib/widgets/drawer/menu_drawer.dart

+6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:flutter_map_example/pages/bundled_offline_map.dart';
44
import 'package:flutter_map_example/pages/cancellable_tile_provider.dart';
55
import 'package:flutter_map_example/pages/circle.dart';
66
import 'package:flutter_map_example/pages/custom_crs/custom_crs.dart';
7+
import 'package:flutter_map_example/pages/debouncing_tile_update_transformer.dart';
78
import 'package:flutter_map_example/pages/epsg3413_crs.dart';
89
import 'package:flutter_map_example/pages/epsg4326_crs.dart';
910
import 'package:flutter_map_example/pages/fallback_url_page.dart';
@@ -137,6 +138,11 @@ class MenuDrawer extends StatelessWidget {
137138
routeName: CancellableTileProviderPage.route,
138139
currentRoute: currentRoute,
139140
),
141+
MenuItemWidget(
142+
caption: 'Debouncing Tile Update Transformer',
143+
routeName: DebouncingTileUpdateTransformerPage.route,
144+
currentRoute: currentRoute,
145+
),
140146
const Divider(),
141147
MenuItemWidget(
142148
caption: 'Polygon Stress Test',

example/lib/widgets/notice_banner.dart

+12-6
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ class NoticeBanner extends StatelessWidget {
2020
foregroundColor = const Color(0xFF072100),
2121
backgroundColor = const Color(0xFFB8F397);
2222

23+
const NoticeBanner.informational({
24+
super.key,
25+
required this.text,
26+
required this.url,
27+
required this.sizeTransition,
28+
}) : icon = Icons.info_outline,
29+
foregroundColor = const Color(0xFF072100),
30+
backgroundColor = Colors.lightBlueAccent;
31+
2332
final String text;
2433
final String? url;
2534
final double sizeTransition;
@@ -33,10 +42,7 @@ class NoticeBanner extends StatelessWidget {
3342
return LayoutBuilder(
3443
builder: (context, constraints) {
3544
return Container(
36-
padding: EdgeInsets.symmetric(
37-
horizontal: 8,
38-
vertical: constraints.maxWidth <= sizeTransition ? 8 : 0,
39-
),
45+
padding: const EdgeInsets.all(12),
4046
width: double.infinity,
4147
color: backgroundColor,
4248
child: Flex(
@@ -46,14 +52,14 @@ class NoticeBanner extends StatelessWidget {
4652
mainAxisAlignment: MainAxisAlignment.center,
4753
children: [
4854
Icon(icon, color: foregroundColor, size: 32),
49-
const SizedBox(height: 12, width: 16),
55+
const SizedBox(height: 8, width: 12),
5056
Text(
5157
text,
5258
style: TextStyle(color: foregroundColor),
5359
textAlign: TextAlign.center,
5460
),
5561
if (url != null) ...[
56-
const SizedBox(height: 0, width: 16),
62+
const SizedBox(height: 8, width: 12),
5763
TextButton.icon(
5864
icon: const Icon(Icons.open_in_new),
5965
label: const Text('Learn more'),

lib/src/layer/tile_layer/tile_layer.dart

+12-51
Original file line numberDiff line numberDiff line change
@@ -197,35 +197,19 @@ class TileLayer extends StatefulWidget {
197197
/// Only load tiles that are within these bounds
198198
final LatLngBounds? tileBounds;
199199

200-
/// This transformer modifies how/when tile updates and pruning are triggered
201-
/// based on [MapEvent]s. It is a StreamTransformer and therefore it is
202-
/// possible to filter/modify/throttle the [TileUpdateEvent]s. Defaults to
203-
/// [TileUpdateTransformers.ignoreTapEvents] which disables loading/pruning
204-
/// for map taps, secondary taps and long presses. See TileUpdateTransformers
205-
/// for more transformer presets or implement your own.
200+
/// Restricts and limits [TileUpdateEvent]s (which are emitted 'by'
201+
/// [MapEvent]s), which cause tiles to update.
206202
///
207-
/// Note: Changing the [tileUpdateTransformer] after TileLayer is created has
208-
/// no affect.
209-
final TileUpdateTransformer tileUpdateTransformer;
210-
211-
/// Defines the minimum delay time from last map event before the tile layers
212-
/// are updated. This delay acts as a debounce period to prevent frequent
213-
/// reloading of tile layers in response to rapid, successive events
214-
/// (e.g., zooming or panning).
215-
///
216-
/// 16ms could be a good starting point for most applications.
217-
/// This at 60fps this will wait one frame after the last event.
203+
/// For more information, see [TileUpdateTransformer].
218204
///
219-
/// By setting this delay, we ensure that map layer updates are performed
220-
/// only after a period of inactivity, enhancing performance and user
221-
/// experience on lower performance devices.
205+
/// Defaults to [TileUpdateTransformers.ignoreTapEvents], which disables
206+
/// updates for map taps, secondary taps and long presses, which alone should
207+
/// not cause the camera to change position.
222208
///
223-
/// - If multiple events occur within this delay period, only the last event
224-
/// triggers the tile layer update, reducing unnecessary processing and
225-
/// network requests.
226-
/// - If the [loadingDelay] is `Duration.zero`, the delay is completely
227-
/// disabled and the tile layer will update as soon as possible.
228-
final Duration loadingDelay;
209+
/// Note that changing this after the layer has already been built will have
210+
/// no effect. If necessary, force a rebuild of the entire layer by changing
211+
/// the [key].
212+
final TileUpdateTransformer tileUpdateTransformer;
229213

230214
/// Create a new [TileLayer] for the [FlutterMap] widget.
231215
TileLayer({
@@ -258,7 +242,6 @@ class TileLayer extends StatefulWidget {
258242
this.evictErrorTileStrategy = EvictErrorTileStrategy.none,
259243
this.reset,
260244
this.tileBounds,
261-
this.loadingDelay = Duration.zero,
262245
TileUpdateTransformer? tileUpdateTransformer,
263246
String userAgentPackageName = 'unknown',
264247
}) : assert(
@@ -353,9 +336,6 @@ class _TileLayerState extends State<TileLayer> with TickerProviderStateMixin {
353336
TileRangeCalculator(tileSize: widget.tileSize);
354337
late TileScaleCalculator _tileScaleCalculator;
355338

356-
/// Delay Timer for [TileLayer.loadingDelay]
357-
Timer? _delayTimer;
358-
359339
// We have to hold on to the mapController hashCode to determine whether we
360340
// need to reinitialize the listeners. didChangeDependencies is called on
361341
// every map movement and if we unsubscribe and resubscribe every time we
@@ -370,26 +350,8 @@ class _TileLayerState extends State<TileLayer> with TickerProviderStateMixin {
370350
_loadAndPruneInVisibleBounds(MapCamera.of(context));
371351
});
372352

373-
/// This method is used to delay the execution of a function by the specified
374-
/// [TileLayer.loadingDelay]. This is useful to prevent frequent reloading
375-
/// of tile layers in response to rapid, successive events (e.g., zooming
376-
/// or panning).
377-
void _loadingDelay(VoidCallback action) {
378-
//execute immediately if delay is zero.
379-
if (widget.loadingDelay == Duration.zero) {
380-
action();
381-
return;
382-
}
383-
384-
// Cancel the previous timer if it is still active.
385-
_delayTimer?.cancel();
386-
387-
// Reset the timer to wait for the debounce duration
388-
_delayTimer = Timer(widget.loadingDelay, action);
389-
}
390-
391353
// This is called on every map movement so we should avoid expensive logic
392-
// where possible.
354+
// where possible, or filter as necessary
393355
@override
394356
void didChangeDependencies() {
395357
super.didChangeDependencies();
@@ -404,7 +366,7 @@ class _TileLayerState extends State<TileLayer> with TickerProviderStateMixin {
404366
_tileUpdateSubscription = mapController.mapEventStream
405367
.map((mapEvent) => TileUpdateEvent(mapEvent: mapEvent))
406368
.transform(widget.tileUpdateTransformer)
407-
.listen((event) => _loadingDelay(() => _onTileUpdateEvent(event)));
369+
.listen(_onTileUpdateEvent);
408370
}
409371

410372
var reloadTiles = false;
@@ -499,7 +461,6 @@ class _TileLayerState extends State<TileLayer> with TickerProviderStateMixin {
499461
_resetSub?.cancel();
500462
_pruneLater?.cancel();
501463
widget.tileProvider.dispose();
502-
_delayTimer?.cancel();
503464
super.dispose();
504465
}
505466

0 commit comments

Comments
 (0)