Tarlan Isaev
 · Founder of Foodshare.club food rescue project

Floating Action Buttons Not Responding in Custom OSM Map Widget

Widgets & Design

Hi guys! I hope everyone is doing great!

I'm working on a custom OSM map widget and I'm encountering an issue with button interactivity. The map displays correctly, but the floating action buttons (zoom in/out, user location, traffic toggle) are not responding to taps. Here's a full code of how I'm implementing the widget:

// Automatic FlutterFlow imports
import '/backend/backend.dart';
import '/backend/schema/structs/index.dart';
import '/backend/supabase/supabase.dart';
import '/flutter_flow/flutter_flow_theme.dart';
import '/flutter_flow/flutter_flow_util.dart';
import '/custom_code/widgets/index.dart'; // Imports other custom widgets
import '/custom_code/actions/index.dart'; // Imports custom actions
import '/flutter_flow/custom_functions.dart'; // Imports custom functions
import 'package:flutter/material.dart';
// Begin custom widget code
// DO NOT REMOVE OR MODIFY THE CODE ABOVE!

import '/custom_code/widgets/index.dart';
import '/custom_code/actions/index.dart';
import '/flutter_flow/custom_functions.dart';

import 'package:flutter_osm_plugin/flutter_osm_plugin.dart' as osm;
import 'package:supabase_flutter/supabase_flutter.dart';
import 'dart:convert';

class OSMMapWidget extends StatefulWidget {
  const OSMMapWidget({
    Key? key,
    this.width,
    this.height,
    required this.latitudes,
    required this.longitudes,
    required this.titles,
    required this.descriptions,
    required this.addresses,
    required this.imageUrls,
    this.initialZoom = 14.0,
    required this.defaultLocation,
    required this.isInteractive,
    required this.allowZooming,
    required this.showZoomButtons,
    required this.showUserLocation,
    required this.showCompass,
    required this.showTraffic,
    required this.centerOnMarkerTap,
  }) : super(key: key);

  final double? width;
  final double? height;
  final List<double> latitudes;
  final List<double> longitudes;
  final List<String> titles;
  final List<String> descriptions;
  final List<String> addresses;
  final List<String> imageUrls;
  final double initialZoom;
  final LatLng defaultLocation;
  final bool isInteractive;
  final bool allowZooming;
  final bool showZoomButtons;
  final bool showUserLocation;
  final bool showCompass;
  final bool showTraffic;
  final bool centerOnMarkerTap;

  @override
  _OSMMapWidgetState createState() => _OSMMapWidgetState();
}

class _OSMMapWidgetState extends State<OSMMapWidget> {
  late osm.MapController _mapController;
  bool _isBottomSheetVisible = false;
  List<MarkerData> markerDataList = [];
  bool _trafficEnabled = false;
  Map<String, osm.GeoPoint> _markers = {};

  @override
  void initState() {
    super.initState();
    _mapController = osm.MapController(
      initPosition: osm.GeoPoint(
        latitude: widget.defaultLocation.latitude,
        longitude: widget.defaultLocation.longitude,
      ),
    );
    _createMarkerDataList();
    if (widget.showTraffic) {
      _trafficEnabled = true;
    }
    WidgetsBinding.instance.addPostFrameCallback((_) {
      addMarkers();
    });
  }

  void _createMarkerDataList() {
    for (int i = 0; i < widget.latitudes.length; i++) {
      if (i < widget.longitudes.length &&
          i < widget.titles.length &&
          i < widget.descriptions.length &&
          i < widget.addresses.length &&
          i < widget.imageUrls.length) {
        markerDataList.add(MarkerData(
          id: i.toString(),
          latitude: widget.latitudes[i],
          longitude: widget.longitudes[i],
          title: widget.titles[i],
          description: widget.descriptions[i],
          address: widget.addresses[i],
          imageUrl: widget.imageUrls[i],
        ));
      }
    }
  }

  Future<void> addMarkers() async {
    try {
      for (var markerData in markerDataList) {
        final geoPoint = osm.GeoPoint(
          latitude: markerData.latitude,
          longitude: markerData.longitude,
        );

        await _mapController.addMarker(
          geoPoint,
          markerIcon: osm.MarkerIcon(
            icon: Icon(
              Icons.location_on,
              color: Colors.red,
              size: 48,
            ),
          ),
        );

        _markers[markerData.id] = geoPoint;
      }

      if (markerDataList.isNotEmpty) {
        await _zoomToFitMarkers();
      }
    } catch (e) {
      print('Error adding markers: $e');
    }
  }

  Future<void> _zoomToFitMarkers() async {
    if (_markers.isEmpty) return;

    try {
      final bounds = osm.BoundingBox.fromGeoPoints(_markers.values.toList());
      await _mapController.zoomToBoundingBox(bounds);
    } catch (e) {
      print('Error zooming to bounds: $e');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Material(
      child: Stack(
        children: [
          SizedBox(
            width: widget.width ?? double.infinity,
            height: widget.height ?? double.infinity,
            child: osm.OSMFlutter(
              controller: _mapController,
              onGeoPointClicked: (geoPoint) => _handleMapTap(geoPoint),
              onMapIsReady: (isReady) {
                if (isReady) addMarkers();
              },
              osmOption: osm.OSMOption(
                userTrackingOption: osm.UserTrackingOption(
                  enableTracking: widget.showUserLocation,
                  unFollowUser: true,
                ),
                zoomOption: osm.ZoomOption(
                  initZoom: widget.initialZoom,
                  minZoomLevel: 2,
                  maxZoomLevel: 19,
                  stepZoom: 1.0,
                ),
                userLocationMarker: widget.showUserLocation
                    ? osm.UserLocationMaker(
                        personMarker: osm.MarkerIcon(
                          icon: Icon(
                            Icons.location_history,
                            color: Colors.blue,
                            size: 48,
                          ),
                        ),
                        directionArrowMarker: osm.MarkerIcon(
                          icon: Icon(
                            Icons.navigation,
                            size: 48,
                          ),
                        ),
                      )
                    : null,
                roadConfiguration: osm.RoadOption(
                  roadColor: Colors.blueGrey,
                ),
                markerOption: osm.MarkerOption(
                  defaultMarker: osm.MarkerIcon(
                    icon: Icon(
                      Icons.location_on,
                      color: Colors.red,
                      size: 48,
                    ),
                  ),
                ),
                showDefaultInfoWindow: false,
              ),
            ),
          ),
          if (!_isBottomSheetVisible && widget.isInteractive)
            Positioned(
              right: 16,
              bottom: 16,
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: [
                  if (widget.showZoomButtons && widget.allowZooming) ...[
                    _buildFloatingButton(
                      "zoomIn",
                      Icons.add,
                      () async => await _mapController.zoomIn(),
                    ),
                    SizedBox(height: 8),
                    _buildFloatingButton(
                      "zoomOut",
                      Icons.remove,
                      () async => await _mapController.zoomOut(),
                    ),
                    SizedBox(height: 8),
                  ],
                  if (widget.showUserLocation)
                    _buildFloatingButton(
                      "location",
                      Icons.my_location,
                      _goToUserLocation,
                    ),
                  if (widget.showTraffic) ...[
                    SizedBox(height: 8),
                    _buildFloatingButton(
                      "traffic",
                      _trafficEnabled ? Icons.traffic : Icons.traffic_outlined,
                      _toggleTraffic,
                    ),
                  ],
                ],
              ),
            ),
        ],
      ),
    );
  }

  Widget _buildFloatingButton(
      String tag, IconData icon, VoidCallback onPressed) {
    return FloatingActionButton(
      heroTag: tag,
      mini: true,
      child: Icon(icon),
      onPressed: onPressed,
    );
  }

  Future<void> _goToUserLocation() async {
    try {
      await _mapController.currentLocation();
      await _mapController.enableTracking();
    } catch (e) {
      print('Error getting user location: $e');
    }
  }

  void _handleMapTap(osm.GeoPoint geoPoint) {
    final tappedMarker = _findNearestMarker(geoPoint);
    if (tappedMarker != null) {
      _showBottomSheet(tappedMarker);
      if (widget.centerOnMarkerTap) {
        _mapController.goToLocation(osm.GeoPoint(
          latitude: tappedMarker.latitude,
          longitude: tappedMarker.longitude,
        ));
      }
    }
  }

  MarkerData? _findNearestMarker(osm.GeoPoint tappedPoint) {
    const double threshold = 0.001; // Approximately 111 meters
    for (var marker in markerDataList) {
      final distance = _calculateDistance(
        tappedPoint.latitude,
        tappedPoint.longitude,
        marker.latitude,
        marker.longitude,
      );
      if (distance < threshold) {
        return marker;
      }
    }
    return null;
  }

  double _calculateDistance(
      double lat1, double lon1, double lat2, double lon2) {
    return ((lat1 - lat2).abs() + (lon1 - lon2).abs()) / 2;
  }

  void _toggleTraffic() {
    setState(() {
      _trafficEnabled = !_trafficEnabled;
      // Note: OSM Flutter plugin doesn't directly support traffic layers
    });
  }

  void _showBottomSheet(MarkerData markerData) {
    setState(() {
      _isBottomSheetVisible = true;
    });

    showModalBottomSheet(
      context: context,
      isScrollControlled: true,
      backgroundColor: Colors.transparent,
      builder: (context) => BottomSheetContent(
        markerData: markerData,
        onClose: () {
          Navigator.pop(context);
          setState(() {
            _isBottomSheetVisible = false;
          });
        },
        onViewDetails: _navigateToPostDetails,
      ),
    ).then((_) {
      setState(() {
        _isBottomSheetVisible = false;
      });
    });
  }

  Future<void> _navigateToPostDetails(MarkerData markerData) async {
    try {
      final supabase = Supabase.instance.client;
      final likes = await fetchLikes(supabase, markerData.id);

      context.pushNamed(
        'PostDetails',
        queryParameters: {
          'postId': markerData.id,
          'title': markerData.title,
          'description': markerData.description,
          'address': markerData.address,
          'imageUrl': markerData.imageUrl,
          'latitude': markerData.latitude.toString(),
          'longitude': markerData.longitude.toString(),
          'likes': jsonEncode(likes),
        },
      );
    } catch (e) {
      print('Error navigating to PostDetails: $e');
    }
  }

  Future<List<Map<String, dynamic>>> fetchLikes(
      SupabaseClient supabase, String postId) async {
    try {
      final response =
          await supabase.from('likes').select().eq('post_id', postId);

      return (response as List).cast<Map<String, dynamic>>();
    } catch (e) {
      print('Error fetching likes: $e');
      return [];
    }
  }

  @override
  void dispose() {
    _mapController.dispose();
    super.dispose();
  }
}

class MarkerData {
  final String id;
  final double latitude;
  final double longitude;
  final String title;
  final String description;
  final String address;
  final String imageUrl;

  MarkerData({
    required this.id,
    required this.latitude,
    required this.longitude,
    required this.title,
    required this.description,
    required this.address,
    required this.imageUrl,
  });
}

class BottomSheetContent extends StatelessWidget {
  final MarkerData markerData;
  final VoidCallback onClose;
  final Function(MarkerData) onViewDetails;

  const BottomSheetContent({
    Key? key,
    required this.markerData,
    required this.onClose,
    required this.onViewDetails,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return DraggableScrollableSheet(
      initialChildSize: 0.6,
      minChildSize: 0.2,
      maxChildSize: 0.9,
      builder: (context, controller) {
        return Container(
          decoration: BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
          ),
          child: ListView(
            controller: controller,
            padding: EdgeInsets.all(16),
            children: [
              if (markerData.imageUrl.isNotEmpty)
                ClipRRect(
                  borderRadius: BorderRadius.circular(12),
                  child: Image.network(
                    markerData.imageUrl,
                    width: double.infinity,
                    height: 200,
                    fit: BoxFit.cover,
                    errorBuilder: (context, error, stackTrace) => Container(
                      height: 200,
                      color: Colors.grey[300],
                      child: Icon(Icons.error),
                    ),
                  ),
                ),
              SizedBox(height: 16),
              Text(
                markerData.title,
                style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
              ),
              SizedBox(height: 8),
              Text(markerData.address),
              SizedBox(height: 16),
              Text(markerData.description),
              SizedBox(height: 24),
              ElevatedButton(
                onPressed: () {
                  onClose();
                  onViewDetails(markerData);
                },
                child: Text('View Details'),
              ),
            ],
          ),
        );
      },
    );
  }
}

The buttons are visible but not responding to taps. Has anyone encountered a similar issue or have suggestions on how to make these buttons interactive within a custom widget in FlutterFlow? Any help or insights would be greatly appreciated.

Thanks in advance!

Project's URL: https://foodshareclub.flutterflow.app/map



What have you tried so far?

Code Review: I carefully reviewed my code to ensure that the buttons are correctly implemented. The buttons are created using FloatingActionButton widgets, and I've verified that they are properly positioned within a Positioned widget inside a Stack.

Widget Hierarchy: I checked the widget hierarchy to ensure there are no overlapping widgets that might be intercepting touch events intended for the buttons.

Rebuilding Widgets: I tried rebuilding the widget tree by calling setState() to see if refreshing the UI would resolve any underlying issues with widget rendering or interactivity.

Did you check FlutterFlow's Documentation for this topic?
Yes
3
2 replies