Skip to content

Tap to Highlight Features in Flutter

This tutorial shows how to highlight map features when the user taps on them — the Flutter equivalent of hover effects on web maps.

Prerequisites

Before you begin, ensure you have:

Highlight Tapped Marker

Change a marker's appearance when tapped:

dart
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';

class TapHighlightScreen extends StatefulWidget {
  @override
  _TapHighlightScreenState createState() => _TapHighlightScreenState();
}

class _TapHighlightScreenState extends State<TapHighlightScreen> {
  MapMetricsController? mapController;
  String? selectedMarkerId;

  final List<Map<String, dynamic>> cities = [
    {'id': 'paris', 'name': 'Paris', 'lat': 48.8566, 'lng': 2.3522},
    {'id': 'london', 'name': 'London', 'lat': 51.5074, 'lng': -0.1276},
    {'id': 'berlin', 'name': 'Berlin', 'lat': 52.52, 'lng': 13.405},
    {'id': 'rome', 'name': 'Rome', 'lat': 41.9028, 'lng': 12.4964},
    {'id': 'madrid', 'name': 'Madrid', 'lat': 40.4168, 'lng': -3.7038},
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Tap to Highlight')),
      body: Stack(
        children: [
          MapMetrics(
            styleUrl:
                'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
            onMapCreated: (MapMetricsController controller) {
              mapController = controller;
            },
            initialCameraPosition: CameraPosition(
              target: LatLng(48.0, 5.0),
              zoom: 4.0,
            ),
            markers: _buildMarkers(),
            onMapClick: (Point point, LatLng coordinates) {
              // Deselect when tapping the map background
              setState(() {
                selectedMarkerId = null;
              });
            },
          ),
          // Info panel for selected city
          if (selectedMarkerId != null)
            Positioned(
              top: 16,
              left: 16,
              right: 16,
              child: _buildInfoPanel(),
            ),
        ],
      ),
    );
  }

  Set<Marker> _buildMarkers() {
    return cities.map((city) {
      final isSelected = city['id'] == selectedMarkerId;

      return Marker(
        markerId: MarkerId(city['id']),
        position: LatLng(city['lat'], city['lng']),
        icon: BitmapDescriptor.defaultMarkerWithHue(
          isSelected ? BitmapDescriptor.hueBlue : BitmapDescriptor.hueRed,
        ),
        infoWindow: InfoWindow(title: city['name']),
        onTap: () {
          setState(() {
            selectedMarkerId = city['id'];
          });
        },
      );
    }).toSet();
  }

  Widget _buildInfoPanel() {
    final city = cities.firstWhere((c) => c['id'] == selectedMarkerId);
    return Card(
      elevation: 4,
      child: ListTile(
        leading: Icon(Icons.location_on, color: Colors.blue, size: 32),
        title: Text(city['name'],
            style: TextStyle(fontWeight: FontWeight.bold)),
        subtitle: Text(
          'Lat: ${city['lat']}, Lng: ${city['lng']}',
        ),
        trailing: IconButton(
          icon: Icon(Icons.close),
          onPressed: () {
            setState(() {
              selectedMarkerId = null;
            });
          },
        ),
      ),
    );
  }
}

Highlight GeoJSON Circles on Tap

Tap on circle features to highlight them:

dart
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';

class CircleTapHighlightScreen extends StatefulWidget {
  @override
  _CircleTapHighlightScreenState createState() =>
      _CircleTapHighlightScreenState();
}

class _CircleTapHighlightScreenState extends State<CircleTapHighlightScreen> {
  MapMetricsController? mapController;
  int? highlightedIndex;

  final List<Map<String, dynamic>> locations = [
    {'name': 'Paris', 'lat': 48.8566, 'lng': 2.3522, 'visitors': '30M'},
    {'name': 'London', 'lat': 51.5074, 'lng': -0.1276, 'visitors': '20M'},
    {'name': 'Berlin', 'lat': 52.52, 'lng': 13.405, 'visitors': '14M'},
    {'name': 'Rome', 'lat': 41.9028, 'lng': 12.4964, 'visitors': '10M'},
    {'name': 'Madrid', 'lat': 40.4168, 'lng': -3.7038, 'visitors': '7M'},
    {'name': 'Amsterdam', 'lat': 52.3676, 'lng': 4.9041, 'visitors': '8M'},
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Circle Tap Highlight')),
      body: Stack(
        children: [
          MapMetrics(
            styleUrl:
                'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
            onMapCreated: (MapMetricsController controller) {
              mapController = controller;
            },
            initialCameraPosition: CameraPosition(
              target: LatLng(48.0, 5.0),
              zoom: 4.0,
            ),
            circles: _buildCircles(),
            onMapClick: (Point point, LatLng coordinates) {
              _checkCircleTap(coordinates);
            },
          ),
          if (highlightedIndex != null)
            Positioned(
              bottom: 24,
              left: 16,
              right: 16,
              child: Card(
                elevation: 6,
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(12),
                ),
                child: Padding(
                  padding: EdgeInsets.all(16),
                  child: Row(
                    children: [
                      Container(
                        width: 40,
                        height: 40,
                        decoration: BoxDecoration(
                          color: Colors.blue,
                          shape: BoxShape.circle,
                        ),
                        child: Icon(Icons.location_city,
                            color: Colors.white, size: 24),
                      ),
                      SizedBox(width: 12),
                      Expanded(
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            Text(
                              locations[highlightedIndex!]['name'],
                              style: TextStyle(
                                  fontSize: 18, fontWeight: FontWeight.bold),
                            ),
                            Text(
                              'Annual visitors: ${locations[highlightedIndex!]['visitors']}',
                              style: TextStyle(color: Colors.grey[600]),
                            ),
                          ],
                        ),
                      ),
                      IconButton(
                        icon: Icon(Icons.close),
                        onPressed: () {
                          setState(() {
                            highlightedIndex = null;
                          });
                        },
                      ),
                    ],
                  ),
                ),
              ),
            ),
        ],
      ),
    );
  }

  Set<Circle> _buildCircles() {
    return locations.asMap().entries.map((entry) {
      final i = entry.key;
      final loc = entry.value;
      final isHighlighted = i == highlightedIndex;

      return Circle(
        circleId: CircleId('circle_$i'),
        center: LatLng(loc['lat'], loc['lng']),
        radius: isHighlighted ? 60000 : 40000, // meters
        fillColor: isHighlighted
            ? Colors.blue.withOpacity(0.5)
            : Colors.blue.withOpacity(0.2),
        strokeColor: isHighlighted ? Colors.blue : Colors.blue.withOpacity(0.6),
        strokeWidth: isHighlighted ? 3 : 1,
      );
    }).toSet();
  }

  void _checkCircleTap(LatLng tapPosition) {
    // Find the closest location within a threshold
    int? closestIndex;
    double closestDistance = double.infinity;

    for (int i = 0; i < locations.length; i++) {
      final loc = locations[i];
      final distance = _approximateDistance(
        tapPosition.latitude,
        tapPosition.longitude,
        loc['lat'],
        loc['lng'],
      );

      if (distance < 0.5 && distance < closestDistance) {
        // ~0.5 degrees threshold
        closestDistance = distance;
        closestIndex = i;
      }
    }

    setState(() {
      highlightedIndex = closestIndex;
    });
  }

  /// Simple approximate distance in degrees
  double _approximateDistance(
      double lat1, double lng1, double lat2, double lng2) {
    final dLat = (lat1 - lat2);
    final dLng = (lng1 - lng2);
    return (dLat * dLat + dLng * dLng);
  }
}

Highlight Polygon Region on Tap

Tap on a polygon region to highlight it:

dart
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';

class PolygonTapHighlightScreen extends StatefulWidget {
  @override
  _PolygonTapHighlightScreenState createState() =>
      _PolygonTapHighlightScreenState();
}

class _PolygonTapHighlightScreenState
    extends State<PolygonTapHighlightScreen> {
  MapMetricsController? mapController;
  String? selectedRegion;

  final Map<String, List<LatLng>> regions = {
    'North': [
      LatLng(49.0, 1.0),
      LatLng(49.0, 4.0),
      LatLng(51.0, 4.0),
      LatLng(51.0, 1.0),
      LatLng(49.0, 1.0),
    ],
    'East': [
      LatLng(47.0, 4.0),
      LatLng(47.0, 8.0),
      LatLng(49.0, 8.0),
      LatLng(49.0, 4.0),
      LatLng(47.0, 4.0),
    ],
    'South': [
      LatLng(43.0, 1.0),
      LatLng(43.0, 4.0),
      LatLng(45.0, 4.0),
      LatLng(45.0, 1.0),
      LatLng(43.0, 1.0),
    ],
  };

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Polygon Tap Highlight')),
      body: MapMetrics(
        styleUrl:
            'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
        onMapCreated: (MapMetricsController controller) {
          mapController = controller;
        },
        initialCameraPosition: CameraPosition(
          target: LatLng(47.0, 4.0),
          zoom: 5.0,
        ),
        polygons: regions.entries.map((entry) {
          final isSelected = entry.key == selectedRegion;
          return Polygon(
            polygonId: PolygonId(entry.key),
            points: entry.value,
            fillColor: isSelected
                ? Colors.blue.withOpacity(0.5)
                : Colors.grey.withOpacity(0.2),
            strokeColor: isSelected ? Colors.blue : Colors.grey,
            strokeWidth: isSelected ? 3 : 1,
            consumeTapEvents: true,
            onTap: () {
              setState(() {
                selectedRegion =
                    selectedRegion == entry.key ? null : entry.key;
              });
            },
          );
        }).toSet(),
      ),
    );
  }
}

Next Steps


Tip: On mobile, use tap instead of hover for interactivity. Combine highlighted state with setState() to redraw markers, circles, or polygons with different styles instantly.