Skip to content

Show Polygon Info on Click in Flutter

This tutorial shows how to display information about a polygon when the user taps on it — useful for showing zone details, area statistics, or district information.

Prerequisites

Before you begin, ensure you have:

Tappable Polygons with Info Card

Tap a polygon to see its details in a bottom card:

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

class PolygonInfoScreen extends StatefulWidget {
  @override
  _PolygonInfoScreenState createState() => _PolygonInfoScreenState();
}

class _PolygonInfoScreenState extends State<PolygonInfoScreen> {
  MapMetricsController? mapController;
  String? selectedZoneId;

  final List<Map<String, dynamic>> zones = [
    {
      'id': 'zone_a',
      'name': '1st Arrondissement',
      'description': 'Historic center with the Louvre, Tuileries Garden, and Palais Royal.',
      'population': '17,600',
      'area': '1.83 km²',
      'color': Colors.blue,
      'points': [
        LatLng(48.865, 2.327),
        LatLng(48.865, 2.345),
        LatLng(48.856, 2.345),
        LatLng(48.856, 2.327),
      ],
    },
    {
      'id': 'zone_b',
      'name': '4th Arrondissement',
      'description': 'Home to Notre-Dame, Île de la Cité, and the Marais district.',
      'population': '28,600',
      'area': '1.60 km²',
      'color': Colors.green,
      'points': [
        LatLng(48.858, 2.345),
        LatLng(48.858, 2.365),
        LatLng(48.848, 2.365),
        LatLng(48.848, 2.345),
      ],
    },
    {
      'id': 'zone_c',
      'name': '5th Arrondissement',
      'description': 'The Latin Quarter with the Panthéon and Luxembourg Gardens.',
      'population': '60,200',
      'area': '2.54 km²',
      'color': Colors.orange,
      'points': [
        LatLng(48.852, 2.335),
        LatLng(48.852, 2.360),
        LatLng(48.842, 2.360),
        LatLng(48.842, 2.335),
      ],
    },
  ];

  Map<String, dynamic>? get selectedZone =>
      selectedZoneId != null
          ? zones.firstWhere((z) => z['id'] == selectedZoneId)
          : null;

  Set<Polygon> get polygons => zones.map((zone) {
    final isSelected = zone['id'] == selectedZoneId;
    final Color color = zone['color'];
    return Polygon(
      polygonId: PolygonId(zone['id']),
      points: zone['points'],
      strokeWidth: isSelected ? 3 : 2,
      strokeColor: isSelected ? color : color.withOpacity(0.6),
      fillColor: isSelected ? color.withOpacity(0.35) : color.withOpacity(0.15),
      consumeTapEvents: true,
      onTap: () {
        setState(() {
          selectedZoneId = selectedZoneId == zone['id'] ? null : zone['id'];
        });
      },
    );
  }).toSet();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('District Info')),
      body: Stack(
        children: [
          MapMetrics(
            styleUrl: 'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
            onMapCreated: (controller) => mapController = controller,
            onMapClick: (Point point, LatLng coordinates) {
              // Dismiss info when tapping outside polygons
              setState(() => selectedZoneId = null);
            },
            initialCameraPosition: CameraPosition(
              target: LatLng(48.855, 2.347),
              zoom: 14.0,
            ),
            polygons: polygons,
          ),
          // Info card
          if (selectedZone != 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: Column(
                    mainAxisSize: MainAxisSize.min,
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Row(
                        children: [
                          Container(
                            width: 14,
                            height: 14,
                            decoration: BoxDecoration(
                              color: selectedZone!['color'],
                              borderRadius: BorderRadius.circular(3),
                            ),
                          ),
                          SizedBox(width: 8),
                          Expanded(
                            child: Text(
                              selectedZone!['name'],
                              style: TextStyle(
                                fontSize: 18,
                                fontWeight: FontWeight.bold,
                              ),
                            ),
                          ),
                          IconButton(
                            icon: Icon(Icons.close, size: 20),
                            onPressed: () =>
                                setState(() => selectedZoneId = null),
                            padding: EdgeInsets.zero,
                            constraints: BoxConstraints(),
                          ),
                        ],
                      ),
                      SizedBox(height: 8),
                      Text(
                        selectedZone!['description'],
                        style: TextStyle(color: Colors.grey[700], fontSize: 14),
                      ),
                      SizedBox(height: 12),
                      Row(
                        children: [
                          _statChip(Icons.people, 'Population',
                              selectedZone!['population']),
                          SizedBox(width: 16),
                          _statChip(
                              Icons.square_foot, 'Area', selectedZone!['area']),
                        ],
                      ),
                    ],
                  ),
                ),
              ),
            ),
        ],
      ),
    );
  }

  Widget _statChip(IconData icon, String label, String value) {
    return Row(
      children: [
        Icon(icon, size: 16, color: Colors.grey),
        SizedBox(width: 4),
        Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(label, style: TextStyle(fontSize: 11, color: Colors.grey)),
            Text(value,
                style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
          ],
        ),
      ],
    );
  }
}

Highlighting the Selected Polygon

The selected polygon gets:

  • Thicker border (strokeWidth: 3)
  • More opaque fill (0.35 vs 0.15)
  • Full-color stroke instead of semi-transparent

This visual feedback makes it clear which zone is selected.

Next Steps


Tip: Use consumeTapEvents: true on polygons so taps are captured by the polygon and don't fall through to the map's onMapClick handler.