Merge FB-06: always-interactive map with recenter control

# Conflicts:
#	docs/feedback/FB-06-always-interactive-map-recenter.md
This commit is contained in:
2026-08-25 00:14:11 -05:00
3 changed files with 279 additions and 7 deletions

View File

@@ -27,6 +27,7 @@ import '../../domain/models.dart';
import '../../geo/geo.dart' as geo;
import '../../tiles/tile_config.dart';
import '../theme.dart' show ripprRadiusLarge;
import 'glass_panel.dart';
import 'pulsing_location_marker.dart';
import 'skeleton_map_layer.dart';
@@ -267,11 +268,12 @@ class _RideMapState extends State<RideMap> with WidgetsBindingObserver {
? (widget.ambientPosition == null ? 2 : ambientZoom)
: (bounds.isDegenerate ? shortRideZoom : maxTileZoom),
maxZoom: maxTileZoom,
interactionOptions: hasPoints
? const InteractionOptions(
flags: InteractiveFlag.pinchZoom | InteractiveFlag.drag,
)
: const InteractionOptions(flags: InteractiveFlag.none),
// FB-06: pan/zoom must always be available, idle or recording -- the old
// `hasPoints` gate locked the map to `InteractiveFlag.none` whenever no trip
// was recording, which is exactly the "can't zoom and move around" report.
interactionOptions: const InteractionOptions(
flags: InteractiveFlag.pinchZoom | InteractiveFlag.drag,
),
onPositionChanged: !widget.follow
? null
: (position, hasGesture) {
@@ -326,7 +328,43 @@ class _RideMapState extends State<RideMap> with WidgetsBindingObserver {
),
);
return _sized(child: map);
// FB-06: the recenter control only ever makes sense once there is a `follow`
// mode to return to and the rider has actually panned away from it -- a static
// (non-following) map, like a finished ride in Trip Detail, never shows this.
final showRecenter = widget.follow && !_following;
return _sized(
child: Stack(
children: [
map,
if (showRecenter)
Positioned(
right: 16,
bottom: 16,
child: GlassPanel(
borderRadius: const BorderRadius.all(Radius.circular(999)),
child: IconButton(
key: const Key('recenter-button'),
icon: const Icon(Icons.my_location),
onPressed: _recenter,
),
),
),
],
),
);
}
/// FB-06: return the camera to the rider's latest known position and resume
/// following new updates. Zoom is left untouched -- the rider may have
/// deliberately zoomed in or out, and recenter should not undo that.
void _recenter() {
final ll.LatLng? target = widget.points.isNotEmpty
? ll.LatLng(widget.points.last.latitude, widget.points.last.longitude)
: widget.ambientPosition;
if (target == null) return;
setState(() => _following = true);
_controller.move(target, _controller.camera.zoom);
}
/// One polyline per speed run within each segment.