Add a Pattern to a Polygon in Flutter
This tutorial shows how to create patterned polygon fills — stripes, dashed/dotted borders, or custom patterns — instead of a plain solid color.
Prerequisites
Before you begin, ensure you have:
- Completed the Flutter Setup Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
There is no
Polygon/Polyline/PatternItemtype orpolygons:/polylines:set on the map widget in this SDK. Polygon fills and outlines areFillStyleLayer/LineStyleLayerover aGeoJsonSource, styled with plain MapLibre style spec paint properties. Real tiled pattern fills usefill-pattern, which references an image registered withStyleController.addImage— this replaces the "draw dozens of overlapping polylines to fake stripes" workaround with the actual supported mechanism.
Pattern Fill Using fill-pattern
Generate a small striped tile once, register it as an image, then reference it from fill-pattern:
import 'dart:convert';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class StripedPolygonScreen extends StatefulWidget {
@override
_StripedPolygonScreenState createState() => _StripedPolygonScreenState();
}
class _StripedPolygonScreenState extends State<StripedPolygonScreen> {
MapController? mapController;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Striped Polygon')),
body: MapMetricsView(
options: MapOptions(
initCenter: Position(2.34, 48.85), // lng, lat
initZoom: 12.0,
initStyle:
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
),
onMapCreated: (MapController controller) {
mapController = controller;
},
onStyleLoaded: (StyleController style) {
_addStripedPolygon(style);
},
),
);
}
/// Draw a small repeatable diagonal-stripe tile.
Future<Uint8List> _generateStripeTile({
int size = 32,
Color stripeColor = Colors.blue,
}) async {
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder);
canvas.drawRect(
Rect.fromLTWH(0, 0, size.toDouble(), size.toDouble()),
Paint()..color = stripeColor.withOpacity(0.15),
);
final linePaint = Paint()
..color = stripeColor.withOpacity(0.6)
..strokeWidth = 3;
for (double x = -size.toDouble(); x < size * 2; x += 8) {
canvas.drawLine(
Offset(x, size.toDouble()),
Offset(x + size, 0),
linePaint,
);
}
final picture = recorder.endRecording();
final image = await picture.toImage(size, size);
final bytes = await image.toByteData(format: ui.ImageByteFormat.png);
return bytes!.buffer.asUint8List();
}
Future<void> _addStripedPolygon(StyleController style) async {
final tile = await _generateStripeTile();
await style.addImage('stripe-pattern', tile);
final geoJson = {
'type': 'Feature',
'properties': {},
'geometry': {
'type': 'Polygon',
'coordinates': [
[
[2.28, 48.88],
[2.40, 48.88],
[2.40, 48.82],
[2.28, 48.82],
[2.28, 48.88], // close
]
],
},
};
await style.addSource(
GeoJsonSource(id: 'region-source', data: jsonEncode(geoJson)),
);
await style.addLayer(
const FillStyleLayer(
id: 'region_fill',
sourceId: 'region-source',
paint: {'fill-pattern': 'stripe-pattern'},
),
);
await style.addLayer(
const LineStyleLayer(
id: 'region_outline',
sourceId: 'region-source',
paint: {'line-color': '#3b82f6', 'line-width': 2.0},
),
);
}
}Dashed Border Polygon
A dashed outline is a line-dasharray on the border's LineStyleLayer — no separate polyline pattern type needed:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class DashedPolygonScreen extends StatefulWidget {
@override
_DashedPolygonScreenState createState() => _DashedPolygonScreenState();
}
class _DashedPolygonScreenState extends State<DashedPolygonScreen> {
MapController? mapController;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Dashed Border Polygon')),
body: MapMetricsView(
options: MapOptions(
initCenter: Position(2.335, 48.855), // lng, lat
initZoom: 13.0,
initStyle:
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
),
onMapCreated: (MapController controller) {
mapController = controller;
},
onStyleLoaded: (StyleController style) {
_addDashedPolygon(style);
},
),
);
}
Future<void> _addDashedPolygon(StyleController style) async {
final geoJson = {
'type': 'Feature',
'properties': {},
'geometry': {
'type': 'Polygon',
'coordinates': [
[
[2.300, 48.870],
[2.370, 48.870],
[2.370, 48.840],
[2.300, 48.840],
[2.300, 48.870],
]
],
},
};
await style.addSource(
GeoJsonSource(id: 'dashed-area', data: jsonEncode(geoJson)),
);
await style.addLayer(
const FillStyleLayer(
id: 'dashed_area_fill',
sourceId: 'dashed-area',
paint: {'fill-color': '#f97316', 'fill-opacity': 0.15},
),
);
await style.addLayer(
const LineStyleLayer(
id: 'dashed_border',
sourceId: 'dashed-area',
paint: {
'line-color': '#f97316',
'line-width': 3.0,
'line-dasharray': [1.5, 1.0],
},
),
);
}
}Multiple Pattern Styles
Show several polygons with different visual treatments. A "dotted" look is a short dash with a round cap:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class MultiPatternScreen extends StatefulWidget {
@override
_MultiPatternScreenState createState() => _MultiPatternScreenState();
}
class _MultiPatternScreenState extends State<MultiPatternScreen> {
MapController? mapController;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Pattern Styles')),
body: Stack(
children: [
MapMetricsView(
options: MapOptions(
initCenter: Position(2.335, 48.855), // lng, lat
initZoom: 12.5,
initStyle:
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
),
onMapCreated: (MapController controller) {
mapController = controller;
},
onStyleLoaded: (StyleController style) {
_addPatternZones(style);
},
),
// Legend
Positioned(
bottom: 16,
left: 16,
child: Card(
child: Padding(
padding: EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('Zones',
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 12)),
_legendRow(Colors.blue, 'A: Solid fill'),
_legendRow(Colors.green, 'B: Thick border'),
_legendRow(Colors.red, 'C: Transparent'),
_legendRow(Colors.purple, 'D: Dotted border'),
],
),
),
),
),
],
),
);
}
Future<void> _addPatternZones(StyleController style) async {
// Zone A: Solid fill
await style.addSource(
GeoJsonSource(
id: 'zone-a',
data: jsonEncode({
'type': 'Feature',
'properties': {},
'geometry': {
'type': 'Polygon',
'coordinates': [
[
[2.280, 48.870], [2.320, 48.870],
[2.320, 48.855], [2.280, 48.855], [2.280, 48.870],
]
],
},
}),
),
);
await style.addLayer(
const FillStyleLayer(
id: 'zone_a_fill',
sourceId: 'zone-a',
paint: {'fill-color': '#3b82f6', 'fill-opacity': 0.3},
),
);
await style.addLayer(
const LineStyleLayer(
id: 'zone_a_outline',
sourceId: 'zone-a',
paint: {'line-color': '#3b82f6', 'line-width': 2.0},
),
);
// Zone B: Light fill with thick border
await style.addSource(
GeoJsonSource(
id: 'zone-b',
data: jsonEncode({
'type': 'Feature',
'properties': {},
'geometry': {
'type': 'Polygon',
'coordinates': [
[
[2.330, 48.870], [2.370, 48.870],
[2.370, 48.855], [2.330, 48.855], [2.330, 48.870],
]
],
},
}),
),
);
await style.addLayer(
const FillStyleLayer(
id: 'zone_b_fill',
sourceId: 'zone-b',
paint: {'fill-color': '#22c55e', 'fill-opacity': 0.1},
),
);
await style.addLayer(
const LineStyleLayer(
id: 'zone_b_outline',
sourceId: 'zone-b',
paint: {'line-color': '#22c55e', 'line-width': 4.0},
),
);
// Zone C: Very transparent
await style.addSource(
GeoJsonSource(
id: 'zone-c',
data: jsonEncode({
'type': 'Feature',
'properties': {},
'geometry': {
'type': 'Polygon',
'coordinates': [
[
[2.280, 48.850], [2.320, 48.850],
[2.320, 48.835], [2.280, 48.835], [2.280, 48.850],
]
],
},
}),
),
);
await style.addLayer(
const FillStyleLayer(
id: 'zone_c_fill',
sourceId: 'zone-c',
paint: {'fill-color': '#ef4444', 'fill-opacity': 0.05},
),
);
await style.addLayer(
const LineStyleLayer(
id: 'zone_c_outline',
sourceId: 'zone-c',
paint: {'line-color': '#ef4444', 'line-width': 2.0},
),
);
// Zone D: Dotted border (short dash + round cap)
await style.addSource(
GeoJsonSource(
id: 'zone-d',
data: jsonEncode({
'type': 'Feature',
'properties': {},
'geometry': {
'type': 'Polygon',
'coordinates': [
[
[2.330, 48.850], [2.370, 48.850],
[2.370, 48.835], [2.330, 48.835], [2.330, 48.850],
]
],
},
}),
),
);
await style.addLayer(
const LineStyleLayer(
id: 'zone_d_dotted_border',
sourceId: 'zone-d',
layout: {'line-cap': 'round'},
paint: {
'line-color': '#a855f7',
'line-width': 3.0,
'line-dasharray': [0.1, 1.5],
},
),
);
}
Widget _legendRow(Color color, String label) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 1),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 14,
height: 14,
decoration: BoxDecoration(
color: color.withOpacity(0.3),
border: Border.all(color: color, width: 1.5),
)),
SizedBox(width: 6),
Text(label, style: TextStyle(fontSize: 11)),
],
),
);
}
}Pattern Techniques
| Technique | Method | Best For |
|---|---|---|
| Solid fill + border | FillStyleLayer fill-color/fill-opacity + LineStyleLayer line-color | Default zones |
| Tiled pattern fill | FillStyleLayer fill-pattern referencing an addImage'd tile | Restricted / textured zones |
| Dashed border | LineStyleLayer line-dasharray, e.g. [1.5, 1.0] | Boundaries, limits |
| Dotted border | LineStyleLayer line-dasharray with a short dash + line-cap: round | Proposed areas |
| Transparency | Low fill-opacity | Background regions |
Next Steps
- Add a Polygon — Basic polygon drawing
- Add a GeoJSON Polygon — GeoJSON-based polygons
- Show Polygon Info on Click — Interactive polygons
Tip: Combine a semi-transparent FillStyleLayer with a LineStyleLayer using line-dasharray for a professional "planned area" or "restricted zone" look. Reserve fill-pattern tile images for cases where a repeating texture (stripes, crosshatch, dots) genuinely needs to render at the source pixel level, since it costs an extra addImage call and a raster asset to design.