From 97586a1d91ab508b6868127bf2f617fbe882c12f Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Mon, 1 May 2023 20:19:40 +0200 Subject: [PATCH] Add MapEventNonRotatedSizeChange MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This new event is triggered when the map's non-rotated size is changed, it is not called when the map is initialized. This will fix TileLayer not loading new tiles when the map size changes. In passing I made some other improvements: - Expose nonRotatedSizeā€™s full type (CustomPoint? instead of CustomPoint?) to avoid unnecessary type casts. - Improve the check for the constraints being ready before setting initial zoom/center based on MapOptions bounds. - Remove unnecessary setState in FlutterMapStateā€™s emitMapEvent. The event emitting does not change the map's state and was causing double calls to setState in some code paths. - FlutterMapState tidy ups. - Removed the workaround for the initial map size being zero in TileLayer which caused tiles to not be loaded. The new MapEventNonRotatedSizeChange event removes the need for this workaround as during startup either: - The platform has already set flutter's resolution and thus all tiles are successfully loaded. - The platform has not set flutter's resolution but when it does it will cause LayoutBuilder to rebuild with the new size which will trigger a MapEventNonRotatedSizeChange event. This event will in turn trigger a tile load in TileLayer. --- lib/src/gestures/gestures.dart | 3 +- lib/src/gestures/map_events.dart | 65 ++++++ lib/src/layer/tile_layer/tile_layer.dart | 30 +-- lib/src/map/flutter_map_state.dart | 240 ++++++++++------------- 4 files changed, 166 insertions(+), 172 deletions(-) diff --git a/lib/src/gestures/gestures.dart b/lib/src/gestures/gestures.dart index 1ca39f57d..03b57d50b 100644 --- a/lib/src/gestures/gestures.dart +++ b/lib/src/gestures/gestures.dart @@ -555,8 +555,7 @@ abstract class MapGestureMixin extends State final direction = details.velocity.pixelsPerSecond / magnitude; final distance = (Offset.zero & - Size(mapState.nonrotatedSize!.x as double, - mapState.nonrotatedSize!.y as double)) + Size(mapState.nonrotatedSize!.x, mapState.nonrotatedSize!.y)) .shortestSide; final flingOffset = _focalStartLocal - _lastFocalLocal; diff --git a/lib/src/gestures/map_events.dart b/lib/src/gestures/map_events.dart index 39f223061..2b701c3e2 100644 --- a/lib/src/gestures/map_events.dart +++ b/lib/src/gestures/map_events.dart @@ -1,3 +1,4 @@ +import 'package:flutter_map/src/core/point.dart'; import 'package:latlong2/latlong.dart'; /// Event sources which are used to identify different types of @@ -21,6 +22,7 @@ enum MapEventSource { fitBounds, custom, scrollWheel, + nonRotatedSizeChange, } /// Base event class which is emitted by MapController instance, the event @@ -59,6 +61,59 @@ abstract class MapEventWithMove extends MapEvent { required super.center, required super.zoom, }); + + /// Returns a subclass of [MapEventWithMove] if the [source] belongs to a + /// movement event, otherwise returns null. + static MapEventWithMove? fromSource({ + required LatLng targetCenter, + required double targetZoom, + required LatLng oldCenter, + required double oldZoom, + required bool hasGesture, + required MapEventSource source, + String? id, + }) { + switch (source) { + case MapEventSource.flingAnimationController: + return MapEventFlingAnimation( + center: oldCenter, + zoom: oldZoom, + targetCenter: targetCenter, + targetZoom: targetZoom, + source: source, + ); + case MapEventSource.doubleTapZoomAnimationController: + return MapEventDoubleTapZoom( + center: oldCenter, + zoom: oldZoom, + targetCenter: targetCenter, + targetZoom: targetZoom, + source: source, + ); + case MapEventSource.scrollWheel: + return MapEventScrollWheelZoom( + center: oldCenter, + zoom: oldZoom, + targetCenter: targetCenter, + targetZoom: targetZoom, + source: source, + ); + case MapEventSource.onDrag: + case MapEventSource.onMultiFinger: + case MapEventSource.mapController: + case MapEventSource.custom: + return MapEventMove( + id: id, + center: oldCenter, + zoom: oldZoom, + targetCenter: targetCenter, + targetZoom: targetZoom, + source: source, + ); + default: + return null; + } + } } /// Event which is fired when map is tapped @@ -260,3 +315,13 @@ class MapEventRotateEnd extends MapEvent { required super.zoom, }); } + +class MapEventNonRotatedSizeChange extends MapEvent { + const MapEventNonRotatedSizeChange({ + required super.source, + required CustomPoint previousNonRotatedSize, + required CustomPoint nonRotatedSize, + required super.center, + required super.zoom, + }); +} diff --git a/lib/src/layer/tile_layer/tile_layer.dart b/lib/src/layer/tile_layer/tile_layer.dart index 8299451c7..de9f12bb1 100644 --- a/lib/src/layer/tile_layer/tile_layer.dart +++ b/lib/src/layer/tile_layer/tile_layer.dart @@ -375,12 +375,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { ); } - if (reloadTiles) { - _tryWaitForSizeToBeInitialized( - mapState, - () => _loadAndPruneInVisibleBounds(mapState), - ); - } + if (reloadTiles) _loadAndPruneInVisibleBounds(mapState); _initializedFromMapState = true; } @@ -658,27 +653,4 @@ class _TileLayerState extends State with TickerProviderStateMixin { bool _outsideZoomLimits(num zoom) => zoom < widget.minZoom || zoom > widget.maxZoom; - - // A workaround for the fact that FlutterMapState size initialization has a - // race condition where sometimes the size is set to CustomPoint(0,0) before - // it is set to the correct value. When this occurs, code that relies on the - // visible bounds will not work correctly. Sometimes it requires more than - // one postFrameCallback to get a non zero size. - void _tryWaitForSizeToBeInitialized( - FlutterMapState mapState, - VoidCallback callback, { - int retries = 3, - }) { - if (retries >= 0 && mapState.size == const CustomPoint(0, 0)) { - WidgetsBinding.instance.addPostFrameCallback((_) { - _tryWaitForSizeToBeInitialized( - mapState, - callback, - retries: retries - 1, - ); - }); - } else { - callback(); - } - } } diff --git a/lib/src/map/flutter_map_state.dart b/lib/src/map/flutter_map_state.dart index 93c0b8a51..a3fb820da 100644 --- a/lib/src/map/flutter_map_state.dart +++ b/lib/src/map/flutter_map_state.dart @@ -124,48 +124,60 @@ class FlutterMapState extends MapGestureMixin ); return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - //Update on layout change - setSize(constraints.maxWidth, constraints.maxHeight); - - if (options.bounds != null && - !_hasFitInitialBounds && - constraints.maxWidth != 0.0) { - final target = - getBoundsCenterZoom(options.bounds!, options.boundsOptions); - _zoom = target.zoom; - _center = target.center; - _hasFitInitialBounds = true; - } - - _pixelBounds = getPixelBounds(zoom); - _bounds = _calculateBounds(); - _pixelOrigin = getNewPixelOrigin(_center); - - return MapStateInheritedWidget( - mapState: this, - child: Listener( - onPointerDown: onPointerDown, - onPointerUp: onPointerUp, - onPointerCancel: onPointerCancel, - onPointerHover: onPointerHover, - onPointerSignal: onPointerSignal, - child: PositionedTapDetector2( - controller: _positionedTapController, - onTap: handleTap, - onSecondaryTap: handleSecondaryTap, - onLongPress: handleLongPress, - onDoubleTap: handleDoubleTap, - child: RawGestureDetector( - gestures: gestures, - child: _buildMap(), + builder: (BuildContext context, BoxConstraints constraints) { + // Update on layout change. + setSize(constraints.maxWidth, constraints.maxHeight); + + // If bounds were provided set the initial center/zoom to match those + // bounds once the parent constraints are available. + if (options.bounds != null && + !_hasFitInitialBounds && + _parentConstraintsAreSet(context, constraints)) { + final target = + getBoundsCenterZoom(options.bounds!, options.boundsOptions); + _zoom = target.zoom; + _center = target.center; + _hasFitInitialBounds = true; + } + + _pixelBounds = getPixelBounds(zoom); + _bounds = _calculateBounds(); + _pixelOrigin = getNewPixelOrigin(_center); + + return MapStateInheritedWidget( + mapState: this, + child: Listener( + onPointerDown: onPointerDown, + onPointerUp: onPointerUp, + onPointerCancel: onPointerCancel, + onPointerHover: onPointerHover, + onPointerSignal: onPointerSignal, + child: PositionedTapDetector2( + controller: _positionedTapController, + onTap: handleTap, + onSecondaryTap: handleSecondaryTap, + onLongPress: handleLongPress, + onDoubleTap: handleDoubleTap, + child: RawGestureDetector( + gestures: gestures, + child: _buildMap(), + ), ), ), - ), - ); - }); + ); + }, + ); } + // During flutter startup the native platform resolution is not immediately + // available which can cause constraints to be zero before they are updated + // in a subsequent build to the actual constraints. This check allows us to + // differentiate zero constraints caused by missing platform resolution vs + // zero constraints which were actually provided by the parent widget. + bool _parentConstraintsAreSet( + BuildContext context, BoxConstraints constraints) => + constraints.maxWidth != 0 || MediaQuery.of(context).size != Size.zero; + Widget _buildMap() { return ClipRect( child: Stack( @@ -224,17 +236,29 @@ class FlutterMapState extends MapGestureMixin Bounds get pixelBounds => _pixelBounds; // Original size of the map where rotation isn't calculated - CustomPoint? _nonrotatedSize; - CustomPoint? get nonrotatedSize => _nonrotatedSize; + CustomPoint? _nonrotatedSize; + CustomPoint? get nonrotatedSize => _nonrotatedSize; void setSize(double width, double height) { - final isCurrSizeNull = _nonrotatedSize == null; - if (isCurrSizeNull || + if (_nonrotatedSize != null || _nonrotatedSize!.x != width || _nonrotatedSize!.y != height) { - _nonrotatedSize = CustomPoint(width, height); + final previousNonRotatedSize = _nonrotatedSize; + _nonrotatedSize = CustomPoint(width, height); _updateSizeByOriginalSizeAndRotation(); + + if (previousNonRotatedSize != null) { + emitMapEvent( + MapEventNonRotatedSizeChange( + source: MapEventSource.nonRotatedSizeChange, + previousNonRotatedSize: previousNonRotatedSize, + nonRotatedSize: _nonrotatedSize!, + center: center, + zoom: zoom, + ), + ); + } } } @@ -263,87 +287,13 @@ class FlutterMapState extends MapGestureMixin _pixelOrigin = getNewPixelOrigin(_center); } - void _handleMoveEmit(LatLng targetCenter, double targetZoom, LatLng oldCenter, - double oldZoom, bool hasGesture, MapEventSource source, String? id) { - if (source == MapEventSource.flingAnimationController) { - emitMapEvent( - MapEventFlingAnimation( - center: oldCenter, - zoom: oldZoom, - targetCenter: targetCenter, - targetZoom: targetZoom, - source: source, - ), - ); - } else if (source == MapEventSource.doubleTapZoomAnimationController) { - emitMapEvent( - MapEventDoubleTapZoom( - center: oldCenter, - zoom: oldZoom, - targetCenter: targetCenter, - targetZoom: targetZoom, - source: source, - ), - ); - } else if (source == MapEventSource.scrollWheel) { - emitMapEvent( - MapEventScrollWheelZoom( - center: oldCenter, - zoom: oldZoom, - targetCenter: targetCenter, - targetZoom: targetZoom, - source: source, - ), - ); - } else if (source == MapEventSource.onDrag || - source == MapEventSource.onMultiFinger) { - emitMapEvent( - MapEventMove( - center: oldCenter, - zoom: oldZoom, - targetCenter: targetCenter, - targetZoom: targetZoom, - source: source, - ), - ); - } else if (source == MapEventSource.mapController) { - emitMapEvent( - MapEventMove( - id: id, - center: oldCenter, - zoom: oldZoom, - targetCenter: targetCenter, - targetZoom: targetZoom, - source: source, - ), - ); - } else if (source == MapEventSource.custom) { - // for custom source, emit move event if zoom or center has changed - if (targetZoom != oldZoom || - targetCenter.latitude != oldCenter.latitude || - targetCenter.longitude != oldCenter.longitude) { - emitMapEvent( - MapEventMove( - id: id, - center: oldCenter, - zoom: oldZoom, - targetCenter: targetCenter, - targetZoom: targetZoom, - source: source, - ), - ); - } - } - } - void emitMapEvent(MapEvent event) { if (event.source == MapEventSource.mapController && event is MapEventMove) { handleAnimationInterruptions(event); } - setState(() { - widget.options.onMapEvent?.call(event); - }); + widget.options.onMapEvent?.call(event); + mapController.mapEventSink.add(event); } @@ -386,14 +336,16 @@ class FlutterMapState extends MapGestureMixin return MoveAndRotateResult(moveSucc, rotateSucc); } - bool move(LatLng newCenter, double newZoom, - {bool hasGesture = false, required MapEventSource source, String? id}) { + bool move( + LatLng newCenter, + double newZoom, { + bool hasGesture = false, + required MapEventSource source, + String? id, + }) { newZoom = fitZoomToBounds(newZoom); - final mapMoved = newCenter != _center || newZoom != _zoom; - if (!mapMoved) { - return false; - } + if (newCenter == _center && newZoom == _zoom) return false; if (isOutOfBounds(newCenter)) { if (!options.slideOnBoundaries) { @@ -427,16 +379,25 @@ class FlutterMapState extends MapGestureMixin _bounds = _calculateBounds(); _pixelOrigin = getNewPixelOrigin(newCenter); - _handleMoveEmit( - newCenter, newZoom, oldCenter, oldZoom, hasGesture, source, id); + final movementEvent = MapEventWithMove.fromSource( + targetCenter: newCenter, + targetZoom: newZoom, + oldCenter: oldCenter, + oldZoom: oldZoom, + hasGesture: hasGesture, + source: source, + ); + if (movementEvent != null) emitMapEvent(movementEvent); options.onPositionChanged?.call( - MapPosition( - center: newCenter, - bounds: _bounds, - zoom: newZoom, - hasGesture: hasGesture), - hasGesture); + MapPosition( + center: newCenter, + bounds: _bounds, + zoom: newZoom, + hasGesture: hasGesture, + ), + hasGesture, + ); return true; } @@ -649,15 +610,12 @@ class FlutterMapState extends MapGestureMixin } LatLng? pointToLatLng(CustomPoint localPoint) { - if (nonrotatedSize == null) { - return null; - } - - final width = nonrotatedSize!.x; - final height = nonrotatedSize!.y; + if (nonrotatedSize == null) return null; - final localPointCenterDistance = - CustomPoint((width / 2) - localPoint.x, (height / 2) - localPoint.y); + final localPointCenterDistance = CustomPoint( + (nonrotatedSize!.x / 2) - localPoint.x, + (nonrotatedSize!.y / 2) - localPoint.y, + ); final mapCenter = options.crs.latLngToPoint(center, zoom); var point = mapCenter - localPointCenterDistance;