From 965e08da7cb59c137372f6f60ec1dea8639034d1 Mon Sep 17 00:00:00 2001 From: lume-code <63221039+lume-code@users.noreply.github.com> Date: Wed, 17 Jun 2026 12:15:28 +0400 Subject: [PATCH] [google_maps_flutter_platform_interface] Add editable polyline/polygon API Adds the platform-interface surface for editable polylines and polygons: - `editable` property and `onEdited` callback on `Polyline` and `Polygon`. - `PolylineEditEvent` and `PolygonEditEvent` map event types. - `onPolylineEdited` / `onPolygonEdited` streams on the platform interface (default `UnimplementedError`, so the addition is non-breaking). Bumps to 2.16.0. First of three PRs splitting flutter/packages#11492 along the federated plugin (interface lands and publishes first). --- .../CHANGELOG.md | 5 +- .../lib/src/events/map_event.dart | 28 +++++ .../google_maps_flutter_platform.dart | 10 ++ .../lib/src/types/polygon.dart | 23 +++- .../lib/src/types/polyline.dart | 22 +++- .../pubspec.yaml | 2 +- .../google_maps_flutter_platform_test.dart | 14 +++ .../test/types/polygon_test.dart | 117 ++++++++++++++++++ .../test/types/polyline_test.dart | 101 +++++++++++++++ 9 files changed, 318 insertions(+), 4 deletions(-) create mode 100644 packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/polygon_test.dart create mode 100644 packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/polyline_test.dart diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md index 1c2d3c2fb1ca..1d162eb103cb 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md @@ -1,5 +1,8 @@ -## NEXT +## 2.16.0 +* Adds `editable` property and `onEdited` callback to `Polyline` and `Polygon`. +* Adds `PolylineEditEvent` and `PolygonEditEvent` event types. +* Adds `onPolylineEdited` and `onPolygonEdited` streams to platform interface. * Updates minimum supported SDK version to Flutter 3.38/Dart 3.10. ## 2.15.0 diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/events/map_event.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/events/map_event.dart index 0d3f47cde019..7bb06a9b76af 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/events/map_event.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/events/map_event.dart @@ -136,6 +136,18 @@ class PolylineTapEvent extends MapEvent { PolylineTapEvent(super.mapId, super.polylineId); } +/// An event fired when a [Polyline] path is edited by the user. +class PolylineEditEvent extends MapEvent { + /// Build a PolylineEdit Event triggered from the map represented by `mapId`. + /// + /// The `value` of this event is a [PolylineId] object that represents the edited Polyline. + /// [points] contains the updated path after the edit. + PolylineEditEvent(super.mapId, super.polylineId, this.points); + + /// The updated list of points after the edit. + final List points; +} + /// An event fired when a [Polygon] is tapped. class PolygonTapEvent extends MapEvent { /// Build an PolygonTap Event triggered from the map represented by `mapId`. @@ -144,6 +156,22 @@ class PolygonTapEvent extends MapEvent { PolygonTapEvent(super.mapId, super.polygonId); } +/// An event fired when a [Polygon] path is edited by the user. +class PolygonEditEvent extends MapEvent { + /// Build a PolygonEdit Event triggered from the map represented by `mapId`. + /// + /// The `value` of this event is a [PolygonId] object that represents the edited Polygon. + /// [points] contains the updated outer boundary after the edit. + /// [holes] contains the updated holes after the edit. + PolygonEditEvent(super.mapId, super.polygonId, this.points, this.holes); + + /// The updated outer boundary points after the edit. + final List points; + + /// The updated list of holes after the edit. + final List> holes; +} + /// An event fired when a [Circle] is tapped. class CircleTapEvent extends MapEvent { /// Build an CircleTap Event triggered from the map represented by `mapId`. diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart index 4ca64f66a8c6..ed2862259dab 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart @@ -327,11 +327,21 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { throw UnimplementedError('onPolylineTap() has not been implemented.'); } + /// A [Polyline] path has been edited by the user. Currently only supported on web. + Stream onPolylineEdited({required int mapId}) { + throw UnimplementedError('onPolylineEdited() has not been implemented.'); + } + /// A [Polygon] has been tapped. Stream onPolygonTap({required int mapId}) { throw UnimplementedError('onPolygonTap() has not been implemented.'); } + /// A [Polygon] path has been edited by the user. Currently only supported on web. + Stream onPolygonEdited({required int mapId}) { + throw UnimplementedError('onPolygonEdited() has not been implemented.'); + } + /// A [Circle] has been tapped. Stream onCircleTap({required int mapId}) { throw UnimplementedError('onCircleTap() has not been implemented.'); diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon.dart index ecc249202f95..b2e80ec55c26 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon.dart @@ -33,6 +33,8 @@ class Polygon implements MapsObject { this.visible = true, this.zIndex = 0, this.onTap, + this.editable = false, + this.onEdited, }); /// Uniquely identifies a [Polygon]. @@ -92,6 +94,19 @@ class Polygon implements MapsObject { /// Callbacks to receive tap events for polygon placed on this map. final VoidCallback? onTap; + /// True if the user can edit this polygon by dragging its vertices. + /// + /// When true, the polygon renders with draggable vertex handles. + /// Currently only supported on web. + final bool editable; + + /// Called when the user edits the polygon path by dragging vertices. + /// + /// The callback receives the updated outer boundary [points] and the + /// updated list of [holes]. Only fires when [editable] is true. + /// Currently only supported on web. + final void Function(List points, List> holes)? onEdited; + /// Creates a new [Polygon] object whose values are the same as this instance, /// unless overwritten by the specified parameters. Polygon copyWith({ @@ -105,6 +120,8 @@ class Polygon implements MapsObject { bool? visibleParam, int? zIndexParam, VoidCallback? onTapParam, + bool? editableParam, + void Function(List points, List> holes)? onEditedParam, }) { return Polygon( polygonId: polygonId, @@ -118,6 +135,8 @@ class Polygon implements MapsObject { visible: visibleParam ?? visible, onTap: onTapParam ?? onTap, zIndex: zIndexParam ?? zIndex, + editable: editableParam ?? editable, + onEdited: onEditedParam ?? onEdited, ); } @@ -146,6 +165,7 @@ class Polygon implements MapsObject { addIfPresent('strokeWidth', strokeWidth); addIfPresent('visible', visible); addIfPresent('zIndex', zIndex); + addIfPresent('editable', editable); json['points'] = _pointsToJson(); @@ -172,7 +192,8 @@ class Polygon implements MapsObject { visible == other.visible && strokeColor == other.strokeColor && strokeWidth == other.strokeWidth && - zIndex == other.zIndex; + zIndex == other.zIndex && + editable == other.editable; } @override diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline.dart index 71656d80fd0c..9f49071e2e71 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline.dart @@ -36,6 +36,8 @@ class Polyline implements MapsObject { this.width = 10, this.zIndex = 0, this.onTap, + this.editable = false, + this.onEdited, }); /// Uniquely identifies a [Polyline]. @@ -114,6 +116,18 @@ class Polyline implements MapsObject { /// Callbacks to receive tap events for polyline placed on this map. final VoidCallback? onTap; + /// True if the user can edit this polyline by dragging its vertices. + /// + /// When true, the polyline renders with draggable vertex handles. + /// Currently only supported on web. + final bool editable; + + /// Called when the user edits the polyline path by dragging vertices. + /// + /// The callback receives the updated list of [LatLng] points. + /// Only fires when [editable] is true. Currently only supported on web. + final void Function(List points)? onEdited; + /// Creates a new [Polyline] object whose values are the same as this instance, /// unless overwritten by the specified parameters. Polyline copyWith({ @@ -129,6 +143,8 @@ class Polyline implements MapsObject { int? widthParam, int? zIndexParam, VoidCallback? onTapParam, + bool? editableParam, + void Function(List points)? onEditedParam, }) { return Polyline( polylineId: polylineId, @@ -144,6 +160,8 @@ class Polyline implements MapsObject { width: widthParam ?? width, onTap: onTapParam ?? onTap, zIndex: zIndexParam ?? zIndex, + editable: editableParam ?? editable, + onEdited: onEditedParam ?? onEdited, ); } @@ -178,6 +196,7 @@ class Polyline implements MapsObject { addIfPresent('visible', visible); addIfPresent('width', width); addIfPresent('zIndex', zIndex); + addIfPresent('editable', editable); json['points'] = _pointsToJson(); @@ -206,7 +225,8 @@ class Polyline implements MapsObject { endCap == other.endCap && visible == other.visible && width == other.width && - zIndex == other.zIndex; + zIndex == other.zIndex && + editable == other.editable; } @override diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml index 226c23f7e39b..deb836ce66b5 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/packages/tree/main/packages/google_maps_f issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.15.0 +version: 2.16.0 environment: sdk: ^3.10.0 diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart index 2254a30780a9..657f06b6c2d5 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart @@ -95,6 +95,20 @@ void main() { ); }); + test('onPolylineEdited() throws UnimplementedError', () { + expect( + () => BuildViewGoogleMapsFlutterPlatform().onPolylineEdited(mapId: 0), + throwsUnimplementedError, + ); + }); + + test('onPolygonEdited() throws UnimplementedError', () { + expect( + () => BuildViewGoogleMapsFlutterPlatform().onPolygonEdited(mapId: 0), + throwsUnimplementedError, + ); + }); + test('default implementation of `getStyleError` returns null', () async { final GoogleMapsFlutterPlatform platform = BuildViewGoogleMapsFlutterPlatform(); expect(await platform.getStyleError(mapId: 0), null); diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/polygon_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/polygon_test.dart new file mode 100644 index 000000000000..b18a346fd0f7 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/polygon_test.dart @@ -0,0 +1,117 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart' show Colors; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$Polygon', () { + test('constructor defaults', () { + const polygon = Polygon(polygonId: PolygonId('ABC123')); + + expect(polygon.consumeTapEvents, equals(false)); + expect(polygon.fillColor, equals(Colors.black)); + expect(polygon.geodesic, equals(false)); + expect(polygon.visible, equals(true)); + expect(polygon.strokeWidth, equals(10)); + expect(polygon.zIndex, equals(0)); + expect(polygon.points, equals(const [])); + expect(polygon.holes, equals(const >[])); + expect(polygon.onTap, isNull); + expect(polygon.editable, equals(false)); + expect(polygon.onEdited, isNull); + }); + + test('construct with editable', () { + const polygon = Polygon(polygonId: PolygonId('ABC123'), editable: true); + + expect(polygon.editable, equals(true)); + }); + + test('toJson includes editable', () { + const polygon = Polygon(polygonId: PolygonId('ABC123'), editable: true); + + final json = polygon.toJson() as Map; + expect(json['editable'], equals(true)); + }); + + test('clone', () { + const polygon = Polygon(polygonId: PolygonId('ABC123'), editable: true); + final Polygon clone = polygon.clone(); + + expect(identical(clone, polygon), isFalse); + expect(clone, equals(polygon)); + expect(clone.editable, equals(true)); + }); + + test('copyWith editable', () { + const polygon = Polygon(polygonId: PolygonId('ABC123')); + final Polygon copy = polygon.copyWith(editableParam: true); + + expect(copy.polygonId, equals(const PolygonId('ABC123'))); + expect(copy.editable, equals(true)); + }); + + test('copyWith onEdited', () { + const polygon = Polygon(polygonId: PolygonId('ABC123')); + final log = []; + final Polygon copy = polygon.copyWith( + onEditedParam: (List points, List> holes) { + log.add('onEdited'); + }, + ); + + copy.onEdited!([const LatLng(1.0, 2.0)], >[]); + expect(log, contains('onEdited')); + }); + + test('onEdited callback receives holes', () { + List? receivedPoints; + List>? receivedHoles; + final polygon = Polygon( + polygonId: const PolygonId('ABC123'), + editable: true, + onEdited: (List points, List> holes) { + receivedPoints = points; + receivedHoles = holes; + }, + ); + + final testPoints = [const LatLng(0, 0), const LatLng(1, 1), const LatLng(0, 1)]; + final testHoles = >[ + [const LatLng(0.2, 0.2), const LatLng(0.4, 0.4), const LatLng(0.2, 0.4)], + ]; + + polygon.onEdited!(testPoints, testHoles); + + expect(receivedPoints, equals(testPoints)); + expect(receivedHoles, equals(testHoles)); + }); + + test('equality includes editable', () { + const p1 = Polygon(polygonId: PolygonId('ABC123')); + const p2 = Polygon(polygonId: PolygonId('ABC123'), editable: true); + + expect(p1, isNot(equals(p2))); + }); + + test('equality ignores onEdited', () { + final p1 = Polygon( + polygonId: const PolygonId('ABC123'), + editable: true, + onEdited: (List points, List> holes) {}, + ); + final p2 = Polygon( + polygonId: const PolygonId('ABC123'), + editable: true, + onEdited: (List points, List> holes) {}, + ); + + expect(p1, equals(p2)); + }); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/polyline_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/polyline_test.dart new file mode 100644 index 000000000000..f86a1a289c18 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/polyline_test.dart @@ -0,0 +1,101 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart' show Colors; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$Polyline', () { + test('constructor defaults', () { + const polyline = Polyline(polylineId: PolylineId('ABC123')); + + expect(polyline.consumeTapEvents, equals(false)); + expect(polyline.color, equals(Colors.black)); + expect(polyline.geodesic, equals(false)); + expect(polyline.visible, equals(true)); + expect(polyline.width, equals(10)); + expect(polyline.zIndex, equals(0)); + expect(polyline.points, equals(const [])); + expect(polyline.patterns, equals(const [])); + expect(polyline.onTap, isNull); + expect(polyline.editable, equals(false)); + expect(polyline.onEdited, isNull); + }); + + test('construct with editable', () { + const polyline = Polyline(polylineId: PolylineId('ABC123'), editable: true); + + expect(polyline.editable, equals(true)); + }); + + test('toJson includes editable', () { + const polyline = Polyline(polylineId: PolylineId('ABC123'), editable: true); + + final json = polyline.toJson() as Map; + expect(json['editable'], equals(true)); + }); + + test('toJson excludes editable when false', () { + const polyline = Polyline(polylineId: PolylineId('ABC123')); + + final json = polyline.toJson() as Map; + expect(json['editable'], equals(false)); + }); + + test('clone', () { + const polyline = Polyline(polylineId: PolylineId('ABC123'), editable: true); + final Polyline clone = polyline.clone(); + + expect(identical(clone, polyline), isFalse); + expect(clone, equals(polyline)); + expect(clone.editable, equals(true)); + }); + + test('copyWith editable', () { + const polyline = Polyline(polylineId: PolylineId('ABC123')); + final Polyline copy = polyline.copyWith(editableParam: true); + + expect(copy.polylineId, equals(const PolylineId('ABC123'))); + expect(copy.editable, equals(true)); + }); + + test('copyWith onEdited', () { + const polyline = Polyline(polylineId: PolylineId('ABC123')); + final log = []; + final Polyline copy = polyline.copyWith( + onEditedParam: (List points) { + log.add('onEdited'); + }, + ); + + copy.onEdited!([const LatLng(1.0, 2.0)]); + expect(log, contains('onEdited')); + }); + + test('equality includes editable', () { + const p1 = Polyline(polylineId: PolylineId('ABC123')); + const p2 = Polyline(polylineId: PolylineId('ABC123'), editable: true); + + expect(p1, isNot(equals(p2))); + }); + + test('equality ignores onEdited', () { + final p1 = Polyline( + polylineId: const PolylineId('ABC123'), + editable: true, + onEdited: (List points) {}, + ); + final p2 = Polyline( + polylineId: const PolylineId('ABC123'), + editable: true, + onEdited: (List points) {}, + ); + + expect(p1, equals(p2)); + }); + }); +}