Flutter —Free Hand Polygon Drawing on Google Maps
So, it’s very interesting to draw free hand polygon on Google Maps. There’s many articles talking about it for Android or iOS, but unfortunately, there’s no one for Flutter. So, I decided to take this challenge and do it for Flutter.
Here’s the steps we will do.
First: Create new Flutter project
You can do so from Android Studio or using command.
flutter create myapp
Second: Adding dependency
We should add google maps dependency which I use google_maps_flutter package, I encourage you to use latest version of it.
google_maps_flutter: ^1.0.6
Don’t forget pub get.
Integration with Android:
Edit the AndroidManifest.xml file
<meta-data android:name="com.google.android.geo.API_KEY"
android:value="YOUR KEY HERE"/>
Integration with iOS:
- Objective-C: specify your API key in the application delegate
ios/Runner/AppDelegate.m
#include "AppDelegate.h"
#include "GeneratedPluginRegistrant.h"
#import "GoogleMaps/GoogleMaps.h"
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[GMSServices provideAPIKey:@"YOUR KEY HERE"];
[GeneratedPluginRegistrant registerWithRegistry:self];
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
@end
- Swift: specify your API key in the application delegate
ios/Runner/AppDelegate.swift
import UIKit
import Flutter
import GoogleMaps
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GMSServices.provideAPIKey("YOUR KEY HERE")
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
Third: Flutter Google Map UI
- Adding important dependencies
import 'dart:async';
import 'dart:collection';
import 'dart:io';
import 'dart:math' as Math;
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
- Adding some final definitions
final Completer<GoogleMapController> _controller = Completer();
final CameraPosition _kGooglePlex = CameraPosition(
target: LatLng(37.42796133580664, -122.085749655962),
zoom: 14.4746,
);
final Set<Polygon> _polygons = HashSet<Polygon>();
final Set<Polyline> _polyLines = HashSet<Polyline>();
- Adding main UI, Google Map
GoogleMap(
mapType: MapType.normal,
initialCameraPosition: _kGooglePlex,
polygons: _polygons,
polylines: _polyLines,
onMapCreated: (GoogleMapController controller) {
_controller.complete(controller);
},
),
- Floating Action Button to manage the activate/deactivate the drawing
FloatingActionButton(
onPressed: _toggleDrawing,
tooltip: 'Drawing',
child: Icon((_drawPolygonEnabled) ? Icons.cancel : Icons.edit),
),
All of them is in Scaffold.
Fourth: Make it able to drawing
It depends on GestureDetector that detect the touches of user in the global position, and then convert it to points for PolyLine, and after finishing user drawing, it converts the points to Polygon.
- Set new variables that controlling the activate/deactivate of drawing and drawing data
bool _drawPolygonEnabled = false;
List<LatLng> _userPolyLinesLatLngList = List();
bool _clearDrawing = false;
int lastXCoordinate, lastYCoordinate;
- Wrap GoogleMap widget inside GestureDetector and set the important attributes to it
GestureDetector(
onPanUpdate: (_drawPolygonEnabled) ? _onPanUpdate : null,
onPanEnd: (_drawPolygonEnabled) ? _onPanEnd : null,
child: GoogleMap(
mapType: MapType.normal,
initialCameraPosition: _kGooglePlex,
polygons: _polygons,
polylines: _polyLines,
onMapCreated: (GoogleMapController controller) {
_controller.complete(controller);
},
),
),
- Toggle button function
_toggleDrawing() {
_clearPolygons();
setState(() => _drawPolygonEnabled = !_drawPolygonEnabled);
}
- GestureDetector onPanUpdate function
_onPanUpdate(DragUpdateDetails details) async {
// To start draw new polygon every time.
if (_clearDrawing) {
_clearDrawing = false;
_clearPolygons();
}
if (_drawPolygonEnabled) {
double x, y;
if (Platform.isAndroid) {
// It times in 3 without any meaning,
// We think it's an issue with GoogleMaps package.
x = details.globalPosition.dx * 3;
y = details.globalPosition.dy * 3;
} else if (Platform.isIOS) {
x = details.globalPosition.dx;
y = details.globalPosition.dy;
}
// Round the x and y.
int xCoordinate = x.round();
int yCoordinate = y.round();
// Check if the distance between last point is not too far.
// to prevent two fingers drawing.
if (_lastXCoordinate != null && _lastYCoordinate != null) {
var distance = Math.sqrt(Math.pow(xCoordinate - _lastXCoordinate, 2) + Math.pow(yCoordinate - _lastYCoordinate, 2));
// Check if the distance of point and point is large.
if (distance > 80.0) return;
}
// Cached the coordinate.
_lastXCoordinate = xCoordinate;
_lastYCoordinate = yCoordinate;
ScreenCoordinate screenCoordinate = ScreenCoordinate(x: xCoordinate, y: yCoordinate);
final GoogleMapController controller = await _controller.future;
LatLng latLng = await controller.getLatLng(screenCoordinate);
try {
// Add new point to list.
_userPolyLinesLatLngList.add(latLng);
_polyLines.removeWhere((polyline) => polyline.polylineId.value == 'user_polyline');
_polyLines.add(
Polyline(
polylineId: PolylineId('user_polyline'),
points: _userPolyLinesLatLngList,
width: 2,
color: Colors.blue,
),
);
} catch (e) {
print(" error painting $e");
}
setState(() {});
}
}
- GestureDetector onPanEnd function
_onPanEnd(DragEndDetails details) async {
// Reset last cached coordinate
_lastXCoordinate = null;
_lastYCoordinate = null;
if (_drawPolygonEnabled) {
_polygons.removeWhere((polygon) => polygon.polygonId.value == 'user_polygon');
_polygons.add(
Polygon(
polygonId: PolygonId('user_polygon'),
points: _userPolyLinesLatLngList,
strokeWidth: 2,
strokeColor: Colors.blue,
fillColor: Colors.blue.withOpacity(0.4),
),
);
setState(() {
_clearDrawing = true;
});
}
}
- Clear polygon and polylines function
_clearPolygons() {
setState(() {
_polyLines.clear();
_polygons.clear();
_userPolyLinesLatLngList.clear();
});
}
Result
Summary
I did this approach with my-self as a challenge, and I think I did it well. It may be there’s other approaches. So you can choose to choose your own code.
I uploaded my full code on GitHub Repo, you can check it, fork it and use it as an opensource. Feel free to add any comment.