laggy on android, UX needs polish

This commit is contained in:
stopflock
2025-11-26 15:03:58 -06:00
parent c6d73d42ee
commit 153377e9e6
19 changed files with 200 additions and 241 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ class CameraProviderWithCache extends ChangeNotifier {
Timer? _debounceTimer;
/// Call this to get (quickly) all cached overlays for the given view.
/// Filters by currently enabled profiles.
/// Filters by currently enabled profiles only. Limiting is handled by MapView.
List<OsmNode> getCachedNodesForBounds(LatLngBounds bounds) {
final allNodes = NodeCache.instance.queryByBounds(bounds);
final enabledProfiles = AppState.instance.enabledProfiles;
+80 -34
View File
@@ -25,6 +25,7 @@ import 'map/camera_refresh_controller.dart';
import 'map/gps_controller.dart';
import 'map/suspected_location_markers.dart';
import 'network_status_indicator.dart';
import 'node_limit_indicator.dart';
import 'provisional_pin.dart';
import 'proximity_alert_banner.dart';
import '../dev_config.dart';
@@ -44,6 +45,7 @@ class MapView extends StatefulWidget {
this.onNodeTap,
this.onSuspectedLocationTap,
this.onSearchPressed,
this.onNodeLimitChanged,
});
final FollowMeMode followMeMode;
@@ -53,6 +55,7 @@ class MapView extends StatefulWidget {
final void Function(OsmNode)? onNodeTap;
final void Function(SuspectedLocation)? onSuspectedLocationTap;
final VoidCallback? onSearchPressed;
final void Function(bool isLimited)? onNodeLimitChanged;
@override
State<MapView> createState() => MapViewState();
@@ -76,7 +79,8 @@ class MapViewState extends State<MapView> {
// Track map center to clear queue on significant panning
LatLng? _lastCenter;
// Track node limit state for parent notification
bool _lastNodeLimitState = false;
// State for proximity alert banner
bool _showProximityBanner = false;
@@ -156,7 +160,6 @@ class MapViewState extends State<MapView> {
getNearbyNodes: () {
if (mounted) {
try {
final cameraProvider = context.read<CameraProviderWithCache>();
LatLngBounds? mapBounds;
try {
mapBounds = _controller.mapController.camera.visibleBounds;
@@ -164,7 +167,7 @@ class MapViewState extends State<MapView> {
return [];
}
return mapBounds != null
? cameraProvider.getCachedNodesForBounds(mapBounds)
? CameraProviderWithCache.instance.getCachedNodesForBounds(mapBounds)
: [];
} catch (e) {
debugPrint('[MapView] Could not get nearby nodes: $e');
@@ -395,38 +398,70 @@ class MapViewState extends State<MapView> {
// Edit sessions don't need to center - we're already centered from the node tap
// SheetAwareMap handles the visual positioning
// Fetch cached cameras for current map bounds (using Consumer so overlays redraw instantly)
Widget cameraLayers = Consumer<CameraProviderWithCache>(
builder: (context, cameraProvider, child) {
// Get current zoom level and map bounds (shared by all logic)
double currentZoom = 15.0; // fallback
LatLngBounds? mapBounds;
try {
currentZoom = _controller.mapController.camera.zoom;
mapBounds = _controller.mapController.camera.visibleBounds;
} catch (_) {
// Controller not ready yet, use fallback values
mapBounds = null;
}
final minZoom = _getMinZoomForNodes(context);
List<OsmNode> nodes;
if (currentZoom >= minZoom) {
// Above minimum zoom - get cached nodes
nodes = (mapBounds != null)
? cameraProvider.getCachedNodesForBounds(mapBounds)
: <OsmNode>[];
} else {
// Below minimum zoom - don't render any nodes
nodes = <OsmNode>[];
}
// Get current zoom level and map bounds (shared by all logic)
double currentZoom = 15.0; // fallback
LatLngBounds? mapBounds;
try {
currentZoom = _controller.mapController.camera.zoom;
mapBounds = _controller.mapController.camera.visibleBounds;
} catch (_) {
// Controller not ready yet, use fallback values
mapBounds = null;
}
final minZoom = _getMinZoomForNodes(context);
List<OsmNode> allNodes;
List<OsmNode> nodesToRender;
bool isLimitActive = false;
if (currentZoom >= minZoom) {
// Above minimum zoom - get cached nodes directly (no Provider needed)
allNodes = (mapBounds != null)
? CameraProviderWithCache.instance.getCachedNodesForBounds(mapBounds)
: <OsmNode>[];
// Filter out invalid coordinates before applying limit
final validNodes = allNodes.where((node) {
return (node.coord.latitude != 0 || node.coord.longitude != 0) &&
node.coord.latitude.abs() <= 90 &&
node.coord.longitude.abs() <= 180;
}).toList();
// Apply rendering limit to prevent UI lag
final maxNodes = appState.maxCameras;
if (validNodes.length > maxNodes) {
nodesToRender = validNodes.take(maxNodes).toList();
isLimitActive = true;
debugPrint('[MapView] Node limit active: rendering ${nodesToRender.length} of ${validNodes.length} devices');
} else {
nodesToRender = validNodes;
isLimitActive = false;
}
} else {
// Below minimum zoom - don't render any nodes
allNodes = <OsmNode>[];
nodesToRender = <OsmNode>[];
isLimitActive = false;
}
// Notify parent if limit state changed (for button disabling)
if (isLimitActive != _lastNodeLimitState) {
_lastNodeLimitState = isLimitActive;
// Schedule callback after build completes to avoid setState during build
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onNodeLimitChanged?.call(isLimitActive);
});
}
// Build camera layers using the limited nodes
Widget cameraLayers = LayoutBuilder(
builder: (context, constraints) {
// Determine if we should dim node markers (when suspected location is selected)
final shouldDimNodes = appState.selectedSuspectedLocation != null;
final markers = CameraMarkersBuilder.buildCameraMarkers(
cameras: nodes,
cameras: nodesToRender,
mapController: _controller.mapController,
userLocation: _gpsController.currentLocation,
selectedNodeId: widget.selectedNodeId,
@@ -451,7 +486,7 @@ class MapViewState extends State<MapView> {
// Filter out suspected locations that are too close to real nodes
final filteredSuspectedLocations = _filterSuspectedLocationsByProximity(
suspectedLocations: limitedSuspectedLocations,
realNodes: nodes,
realNodes: nodesToRender,
minDistance: appState.suspectedLocationMinDistance,
);
@@ -473,7 +508,7 @@ class MapViewState extends State<MapView> {
}
final overlays = DirectionConesBuilder.buildDirectionCones(
cameras: nodes,
cameras: nodesToRender,
zoom: currentZoom,
session: session,
editSession: editSession,
@@ -496,7 +531,7 @@ class MapViewState extends State<MapView> {
}
// Build edit lines connecting original nodes to their edited positions
final editLines = _buildEditLines(nodes);
final editLines = _buildEditLines(nodesToRender);
// Build center marker for add/edit sessions
final centerMarkers = <Marker>[];
@@ -573,9 +608,20 @@ class MapViewState extends State<MapView> {
if (editLines.isNotEmpty) PolylineLayer(polylines: editLines),
if (routeLines.isNotEmpty) PolylineLayer(polylines: routeLines),
MarkerLayer(markers: [...suspectedLocationMarkers, ...markers, ...centerMarkers]),
// Node limit indicator (top-left) - shown when limit is active
NodeLimitIndicator(
isActive: isLimitActive,
renderedCount: nodesToRender.length,
totalCount: isLimitActive ? allNodes.where((node) {
return (node.coord.latitude != 0 || node.coord.longitude != 0) &&
node.coord.latitude.abs() <= 90 &&
node.coord.longitude.abs() <= 180;
}).length : 0,
),
],
);
}
},
);
return Stack(
+1 -7
View File
@@ -44,12 +44,6 @@ class NetworkStatusIndicator extends StatelessWidget {
color = Colors.green;
break;
case NetworkStatusType.nodeLimitReached:
message = locService.t('networkStatus.nodeLimitReached');
icon = Icons.visibility_off;
color = Colors.amber;
break;
case NetworkStatusType.issues:
switch (networkStatus.currentIssueType) {
case NetworkIssueType.overpassApi:
@@ -67,7 +61,7 @@ class NetworkStatusIndicator extends StatelessWidget {
}
return Positioned(
top: 8, // Position relative to the map area (not the screen)
top: 56, // Position below node limit indicator when present
left: 8,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
+59
View File
@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import '../services/localization_service.dart';
class NodeLimitIndicator extends StatelessWidget {
final bool isActive;
final int renderedCount;
final int totalCount;
const NodeLimitIndicator({
super.key,
required this.isActive,
required this.renderedCount,
required this.totalCount,
});
@override
Widget build(BuildContext context) {
if (!isActive) {
return const SizedBox.shrink();
}
final locService = LocalizationService.instance;
final message = locService.t('nodeLimitIndicator.message')
.replaceAll('{rendered}', renderedCount.toString())
.replaceAll('{total}', totalCount.toString());
return Positioned(
top: 8, // Position at top-left of map area
left: 8,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.black87,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.amber, width: 1),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.visibility_off,
size: 16,
color: Colors.amber,
),
const SizedBox(width: 4),
Text(
message,
style: const TextStyle(
color: Colors.amber,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
],
),
),
);
}
}
+8 -2
View File
@@ -11,8 +11,14 @@ import 'advanced_edit_options_sheet.dart';
class NodeTagSheet extends StatelessWidget {
final OsmNode node;
final VoidCallback? onEditPressed;
final bool isNodeLimitActive;
const NodeTagSheet({super.key, required this.node, this.onEditPressed});
const NodeTagSheet({
super.key,
required this.node,
this.onEditPressed,
this.isNodeLimitActive = false,
});
@override
Widget build(BuildContext context) {
@@ -200,7 +206,7 @@ class NodeTagSheet extends StatelessWidget {
children: [
if (isEditable) ...[
ElevatedButton.icon(
onPressed: _openEditSheet,
onPressed: isNodeLimitActive ? null : _openEditSheet,
icon: const Icon(Icons.edit, size: 18),
label: Text(locService.edit),
style: ElevatedButton.styleFrom(