Skip to content

Commit

Permalink
feat!: added TileUpdateTransformers.debounce & reverted #1840 (#1850)
Browse files Browse the repository at this point in the history
  • Loading branch information
JaffaKetchup committed Apr 2, 2024
1 parent 149f847 commit bcd0b7b
Show file tree
Hide file tree
Showing 6 changed files with 231 additions and 66 deletions.
3 changes: 3 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:flutter_map_example/pages/bundled_offline_map.dart';
import 'package:flutter_map_example/pages/cancellable_tile_provider.dart';
import 'package:flutter_map_example/pages/circle.dart';
import 'package:flutter_map_example/pages/custom_crs/custom_crs.dart';
import 'package:flutter_map_example/pages/debouncing_tile_update_transformer.dart';
import 'package:flutter_map_example/pages/epsg3413_crs.dart';
import 'package:flutter_map_example/pages/epsg4326_crs.dart';
import 'package:flutter_map_example/pages/fallback_url_page.dart';
Expand Down Expand Up @@ -86,6 +87,8 @@ class MyApp extends StatelessWidget {
FallbackUrlPage.route: (context) => const FallbackUrlPage(),
SecondaryTapPage.route: (context) => const SecondaryTapPage(),
RetinaPage.route: (context) => const RetinaPage(),
DebouncingTileUpdateTransformerPage.route: (context) =>
const DebouncingTileUpdateTransformerPage(),
},
);
}
Expand Down
112 changes: 112 additions & 0 deletions example/lib/pages/debouncing_tile_update_transformer.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_example/widgets/drawer/menu_drawer.dart';
import 'package:flutter_map_example/widgets/notice_banner.dart';
import 'package:latlong2/latlong.dart';

class DebouncingTileUpdateTransformerPage extends StatefulWidget {
static const String route = '/debouncing_tile_update_transformer_page';

const DebouncingTileUpdateTransformerPage({super.key});

@override
State<DebouncingTileUpdateTransformerPage> createState() =>
_DebouncingTileUpdateTransformerPageState();
}

class _DebouncingTileUpdateTransformerPageState
extends State<DebouncingTileUpdateTransformerPage> {
int _changeEndKeyRefresher = 0;
double _durationInMilliseconds = 20;

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Debouncing Tile Update Transformer')),
drawer: const MenuDrawer(DebouncingTileUpdateTransformerPage.route),
body: Column(
children: [
const NoticeBanner.informational(
text:
'This TileUpdateTransformer debounces TileUpdateEvents so they '
"don't occur too frequently, which can improve performance and "
'reduce tile requests.\nHowever, this does lead to reduced UX, '
'as tiles will not be loaded during long movements or '
'animations, resulting in the background grey breaking the '
'illusion of a seamless map.',
url:
'https://docs.fleaflet.dev/layers/tile-layer#tile-update-transformers',
sizeTransition: 1360,
),
Expanded(
child: Stack(
children: [
FlutterMap(
options: MapOptions(
initialCenter: const LatLng(51.5, -0.09),
initialZoom: 5,
cameraConstraint: CameraConstraint.contain(
bounds: LatLngBounds(
const LatLng(-90, -180),
const LatLng(90, 180),
),
),
),
children: [
TileLayer(
key: ValueKey('TileLayer-$_changeEndKeyRefresher'),
urlTemplate:
'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'dev.fleaflet.flutter_map.example',
tileUpdateTransformer: TileUpdateTransformers.debounce(
Duration(milliseconds: _durationInMilliseconds.toInt()),
),
),
],
),
Positioned(
left: 16,
top: 16,
right: 16,
child: DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(32),
),
child: Padding(
padding: const EdgeInsets.only(
left: 16, right: 8, top: 4, bottom: 4),
child: Row(
children: [
const Tooltip(
message: 'Adjust Duration',
child: Icon(Icons.timer),
),
Expanded(
child: Slider.adaptive(
value: _durationInMilliseconds,
onChanged: (v) =>
setState(() => _durationInMilliseconds = v),
onChangeEnd: (v) =>
setState(() => _changeEndKeyRefresher++),
min: 0,
max: 500,
divisions: 100,
label: _durationInMilliseconds == 0
? 'Instant/No Debounce'
: '${_durationInMilliseconds.toInt()} ms',
),
),
],
),
),
),
)
],
),
),
],
),
);
}
}
6 changes: 6 additions & 0 deletions example/lib/widgets/drawer/menu_drawer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:flutter_map_example/pages/bundled_offline_map.dart';
import 'package:flutter_map_example/pages/cancellable_tile_provider.dart';
import 'package:flutter_map_example/pages/circle.dart';
import 'package:flutter_map_example/pages/custom_crs/custom_crs.dart';
import 'package:flutter_map_example/pages/debouncing_tile_update_transformer.dart';
import 'package:flutter_map_example/pages/epsg3413_crs.dart';
import 'package:flutter_map_example/pages/epsg4326_crs.dart';
import 'package:flutter_map_example/pages/fallback_url_page.dart';
Expand Down Expand Up @@ -137,6 +138,11 @@ class MenuDrawer extends StatelessWidget {
routeName: CancellableTileProviderPage.route,
currentRoute: currentRoute,
),
MenuItemWidget(
caption: 'Debouncing Tile Update Transformer',
routeName: DebouncingTileUpdateTransformerPage.route,
currentRoute: currentRoute,
),
const Divider(),
MenuItemWidget(
caption: 'Polygon Stress Test',
Expand Down
18 changes: 12 additions & 6 deletions example/lib/widgets/notice_banner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ class NoticeBanner extends StatelessWidget {
foregroundColor = const Color(0xFF072100),
backgroundColor = const Color(0xFFB8F397);

const NoticeBanner.informational({
super.key,
required this.text,
required this.url,
required this.sizeTransition,
}) : icon = Icons.info_outline,
foregroundColor = const Color(0xFF072100),
backgroundColor = Colors.lightBlueAccent;

final String text;
final String? url;
final double sizeTransition;
Expand All @@ -33,10 +42,7 @@ class NoticeBanner extends StatelessWidget {
return LayoutBuilder(
builder: (context, constraints) {
return Container(
padding: EdgeInsets.symmetric(
horizontal: 8,
vertical: constraints.maxWidth <= sizeTransition ? 8 : 0,
),
padding: const EdgeInsets.all(12),
width: double.infinity,
color: backgroundColor,
child: Flex(
Expand All @@ -46,14 +52,14 @@ class NoticeBanner extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: foregroundColor, size: 32),
const SizedBox(height: 12, width: 16),
const SizedBox(height: 8, width: 12),
Text(
text,
style: TextStyle(color: foregroundColor),
textAlign: TextAlign.center,
),
if (url != null) ...[
const SizedBox(height: 0, width: 16),
const SizedBox(height: 8, width: 12),
TextButton.icon(
icon: const Icon(Icons.open_in_new),
label: const Text('Learn more'),
Expand Down
63 changes: 12 additions & 51 deletions lib/src/layer/tile_layer/tile_layer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -197,35 +197,19 @@ class TileLayer extends StatefulWidget {
/// Only load tiles that are within these bounds
final LatLngBounds? tileBounds;

/// This transformer modifies how/when tile updates and pruning are triggered
/// based on [MapEvent]s. It is a StreamTransformer and therefore it is
/// possible to filter/modify/throttle the [TileUpdateEvent]s. Defaults to
/// [TileUpdateTransformers.ignoreTapEvents] which disables loading/pruning
/// for map taps, secondary taps and long presses. See TileUpdateTransformers
/// for more transformer presets or implement your own.
/// Restricts and limits [TileUpdateEvent]s (which are emitted 'by'
/// [MapEvent]s), which cause tiles to update.
///
/// Note: Changing the [tileUpdateTransformer] after TileLayer is created has
/// 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.
/// For more information, see [TileUpdateTransformer].
///
/// 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.
/// Defaults to [TileUpdateTransformers.ignoreTapEvents], which disables
/// updates for map taps, secondary taps and long presses, which alone should
/// not cause the camera to change position.
///
/// - 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;
/// Note that changing this after the layer has already been built will have
/// no effect. If necessary, force a rebuild of the entire layer by changing
/// the [key].
final TileUpdateTransformer tileUpdateTransformer;

/// Create a new [TileLayer] for the [FlutterMap] widget.
TileLayer({
Expand Down Expand Up @@ -258,7 +242,6 @@ 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 @@ -353,9 +336,6 @@ 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 @@ -370,26 +350,8 @@ 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.
// where possible, or filter as necessary
@override
void didChangeDependencies() {
super.didChangeDependencies();
Expand All @@ -404,7 +366,7 @@ class _TileLayerState extends State<TileLayer> with TickerProviderStateMixin {
_tileUpdateSubscription = mapController.mapEventStream
.map((mapEvent) => TileUpdateEvent(mapEvent: mapEvent))
.transform(widget.tileUpdateTransformer)
.listen((event) => _loadingDelay(() => _onTileUpdateEvent(event)));
.listen(_onTileUpdateEvent);
}

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

Expand Down
Loading

0 comments on commit bcd0b7b

Please sign in to comment.