[google_maps_flutter_platfomr_interface] Add Marker drag events (#2653)

This PR adds onDragStart(LatLng) and onDrag(LatLng) events to Marker objects, in addition to the already existing onDragEnd.
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 5d361d8..464c33e 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,3 +1,7 @@
+## 2.1.2
+
+* Add additional marker drag events
+
 ## 2.1.1
 
 * Method `buildViewWithTextDirection` has been added to the platform interface.
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 be42648..614cbe8 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
@@ -102,6 +102,26 @@
   InfoWindowTapEvent(int mapId, MarkerId markerId) : super(mapId, markerId);
 }
 
+/// An event fired when a [Marker] is starting to be dragged to a new [LatLng].
+class MarkerDragStartEvent extends _PositionedMapEvent<MarkerId> {
+  /// Build a MarkerDragStart Event triggered from the map represented by `mapId`.
+  ///
+  /// The `position` on this event is the [LatLng] on which the Marker was picked up from.
+  /// The `value` of this event is a [MarkerId] object that represents the Marker.
+  MarkerDragStartEvent(int mapId, LatLng position, MarkerId markerId)
+      : super(mapId, position, markerId);
+}
+
+/// An event fired when a [Marker] is being dragged to a new [LatLng].
+class MarkerDragEvent extends _PositionedMapEvent<MarkerId> {
+  /// Build a MarkerDrag Event triggered from the map represented by `mapId`.
+  ///
+  /// The `position` on this event is the [LatLng] on which the Marker was dragged to.
+  /// The `value` of this event is a [MarkerId] object that represents the Marker.
+  MarkerDragEvent(int mapId, LatLng position, MarkerId markerId)
+      : super(mapId, position, markerId);
+}
+
 /// An event fired when a [Marker] is dragged to a new [LatLng].
 class MarkerDragEndEvent extends _PositionedMapEvent<MarkerId> {
   /// Build a MarkerDragEnd Event triggered from the map represented by `mapId`.
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart
index 2b9c71e..99f4fdd 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart
@@ -125,6 +125,16 @@
   }
 
   @override
+  Stream<MarkerDragStartEvent> onMarkerDragStart({required int mapId}) {
+    return _events(mapId).whereType<MarkerDragStartEvent>();
+  }
+
+  @override
+  Stream<MarkerDragEvent> onMarkerDrag({required int mapId}) {
+    return _events(mapId).whereType<MarkerDragEvent>();
+  }
+
+  @override
   Stream<MarkerDragEndEvent> onMarkerDragEnd({required int mapId}) {
     return _events(mapId).whereType<MarkerDragEndEvent>();
   }
@@ -174,6 +184,20 @@
           MarkerId(call.arguments['markerId']),
         ));
         break;
+      case 'marker#onDragStart':
+        _mapEventStreamController.add(MarkerDragStartEvent(
+          mapId,
+          LatLng.fromJson(call.arguments['position'])!,
+          MarkerId(call.arguments['markerId']),
+        ));
+        break;
+      case 'marker#onDrag':
+        _mapEventStreamController.add(MarkerDragEvent(
+          mapId,
+          LatLng.fromJson(call.arguments['position'])!,
+          MarkerId(call.arguments['markerId']),
+        ));
+        break;
       case 'marker#onDragEnd':
         _mapEventStreamController.add(MarkerDragEndEvent(
           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 2bb0ab2..08b4872 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
@@ -304,6 +304,16 @@
   }
 
   /// A [Marker] has been dragged to a different [LatLng] position.
+  Stream<MarkerDragStartEvent> onMarkerDragStart({required int mapId}) {
+    throw UnimplementedError('onMarkerDragEnd() has not been implemented.');
+  }
+
+  /// A [Marker] has been dragged to a different [LatLng] position.
+  Stream<MarkerDragEvent> onMarkerDrag({required int mapId}) {
+    throw UnimplementedError('onMarkerDragEnd() has not been implemented.');
+  }
+
+  /// A [Marker] has been dragged to a different [LatLng] position.
   Stream<MarkerDragEndEvent> onMarkerDragEnd({required int mapId}) {
     throw UnimplementedError('onMarkerDragEnd() has not been implemented.');
   }
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker.dart
index 0d1b780..52255f8 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker.dart
@@ -147,6 +147,8 @@
     this.visible = true,
     this.zIndex = 0.0,
     this.onTap,
+    this.onDrag,
+    this.onDragStart,
     this.onDragEnd,
   }) : assert(alpha == null || (0.0 <= alpha && alpha <= 1.0));
 
@@ -207,9 +209,15 @@
   /// Callbacks to receive tap events for markers placed on this map.
   final VoidCallback? onTap;
 
+  /// Signature reporting the new [LatLng] at the start of a drag event.
+  final ValueChanged<LatLng>? onDragStart;
+
   /// Signature reporting the new [LatLng] at the end of a drag event.
   final ValueChanged<LatLng>? onDragEnd;
 
+  /// Signature reporting the new [LatLng] during the drag event.
+  final ValueChanged<LatLng>? onDrag;
+
   /// Creates a new [Marker] object whose values are the same as this instance,
   /// unless overwritten by the specified parameters.
   Marker copyWith({
@@ -225,6 +233,8 @@
     bool? visibleParam,
     double? zIndexParam,
     VoidCallback? onTapParam,
+    ValueChanged<LatLng>? onDragStartParam,
+    ValueChanged<LatLng>? onDragParam,
     ValueChanged<LatLng>? onDragEndParam,
   }) {
     return Marker(
@@ -241,6 +251,8 @@
       visible: visibleParam ?? visible,
       zIndex: zIndexParam ?? zIndex,
       onTap: onTapParam ?? onTap,
+      onDragStart: onDragStartParam ?? onDragStart,
+      onDrag: onDragParam ?? onDrag,
       onDragEnd: onDragEndParam ?? onDragEnd,
     );
   }
@@ -300,6 +312,7 @@
     return 'Marker{markerId: $markerId, alpha: $alpha, anchor: $anchor, '
         'consumeTapEvents: $consumeTapEvents, draggable: $draggable, flat: $flat, '
         'icon: $icon, infoWindow: $infoWindow, position: $position, rotation: $rotation, '
-        'visible: $visible, zIndex: $zIndex, onTap: $onTap}';
+        'visible: $visible, zIndex: $zIndex, onTap: $onTap, onDragStart: $onDragStart, '
+        'onDrag: $onDrag, onDragEnd: $onDragEnd}';
   }
 }
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 1dc73f4..2a2c9cf 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 @@
 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.1.1
+version: 2.1.2
 
 environment:
   sdk: '>=2.12.0 <3.0.0'
@@ -19,6 +19,7 @@
   stream_transform: ^2.0.0
 
 dev_dependencies:
+  async: ^2.5.0
   flutter_test:
     sdk: flutter
   mockito: ^5.0.0
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/method_channel/method_channel_google_maps_flutter_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/method_channel/method_channel_google_maps_flutter_test.dart
index 19e81c9..176f702 100644
--- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/method_channel/method_channel_google_maps_flutter_test.dart
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/method_channel/method_channel_google_maps_flutter_test.dart
@@ -5,8 +5,12 @@
 import 'package:flutter/services.dart';
 import 'package:flutter_test/flutter_test.dart';
 
+import 'package:google_maps_flutter_platform_interface/src/events/map_event.dart';
 import 'package:google_maps_flutter_platform_interface/src/method_channel/method_channel_google_maps_flutter.dart';
 import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart';
+import 'dart:async';
+
+import 'package:async/async.dart';
 
 void main() {
   TestWidgetsFlutterBinding.ensureInitialized();
@@ -33,6 +37,15 @@
       });
     }
 
+    Future<void> sendPlatformMessage(
+        int mapId, String method, Map<dynamic, dynamic> data) async {
+      final ByteData byteData = const StandardMethodCodec()
+          .encodeMethodCall(MethodCall(method, data));
+      await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger
+          .handlePlatformMessage(
+              "plugins.flutter.io/google_maps_$mapId", byteData, (data) {});
+    }
+
     // Calls each method that uses invokeMethod with a return type other than
     // void to ensure that the casting/nullability handling succeeds.
     //
@@ -68,5 +81,46 @@
         'map#takeSnapshot',
       ]);
     });
+    test('markers send drag event to correct streams', () async {
+      const int mapId = 1;
+      final jsonMarkerDragStartEvent = <dynamic, dynamic>{
+        "mapId": mapId,
+        "markerId": "drag-start-marker",
+        "position": <double>[1.0, 1.0]
+      };
+      final jsonMarkerDragEvent = <dynamic, dynamic>{
+        "mapId": mapId,
+        "markerId": "drag-marker",
+        "position": <double>[1.0, 1.0]
+      };
+      final jsonMarkerDragEndEvent = <dynamic, dynamic>{
+        "mapId": mapId,
+        "markerId": "drag-end-marker",
+        "position": <double>[1.0, 1.0]
+      };
+
+      final MethodChannelGoogleMapsFlutter maps =
+          MethodChannelGoogleMapsFlutter();
+      maps.ensureChannelInitialized(mapId);
+
+      final StreamQueue<MarkerDragStartEvent> markerDragStartStream =
+          StreamQueue(maps.onMarkerDragStart(mapId: mapId));
+      final StreamQueue<MarkerDragEvent> markerDragStream =
+          StreamQueue(maps.onMarkerDrag(mapId: mapId));
+      final StreamQueue<MarkerDragEndEvent> markerDragEndStream =
+          StreamQueue(maps.onMarkerDragEnd(mapId: mapId));
+
+      await sendPlatformMessage(
+          mapId, "marker#onDragStart", jsonMarkerDragStartEvent);
+      await sendPlatformMessage(mapId, "marker#onDrag", jsonMarkerDragEvent);
+      await sendPlatformMessage(
+          mapId, "marker#onDragEnd", jsonMarkerDragEndEvent);
+
+      expect((await markerDragStartStream.next).value.value,
+          equals("drag-start-marker"));
+      expect((await markerDragStream.next).value.value, equals("drag-marker"));
+      expect((await markerDragEndStream.next).value.value,
+          equals("drag-end-marker"));
+    });
   });
 }
diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/marker_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/marker_test.dart
new file mode 100644
index 0000000..c8f6fa5
--- /dev/null
+++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/marker_test.dart
@@ -0,0 +1,167 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter/cupertino.dart';
+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('$Marker', () {
+    test('constructor defaults', () {
+      final Marker marker = Marker(markerId: MarkerId("ABC123"));
+
+      expect(marker.alpha, equals(1.0));
+      expect(marker.anchor, equals(const Offset(0.5, 1.0)));
+      expect(marker.consumeTapEvents, equals(false));
+      expect(marker.draggable, equals(false));
+      expect(marker.flat, equals(false));
+      expect(marker.icon, equals(BitmapDescriptor.defaultMarker));
+      expect(marker.infoWindow, equals(InfoWindow.noText));
+      expect(marker.position, equals(const LatLng(0.0, 0.0)));
+      expect(marker.rotation, equals(0.0));
+      expect(marker.visible, equals(true));
+      expect(marker.zIndex, equals(0.0));
+      expect(marker.onTap, equals(null));
+      expect(marker.onDrag, equals(null));
+      expect(marker.onDragStart, equals(null));
+      expect(marker.onDragEnd, equals(null));
+    });
+    test('constructor alpha is >= 0.0 and <= 1.0', () {
+      final ValueSetter<double> initWithAlpha = (double alpha) {
+        Marker(markerId: MarkerId("ABC123"), alpha: alpha);
+      };
+      expect(() => initWithAlpha(-0.5), throwsAssertionError);
+      expect(() => initWithAlpha(0.0), isNot(throwsAssertionError));
+      expect(() => initWithAlpha(0.5), isNot(throwsAssertionError));
+      expect(() => initWithAlpha(1.0), isNot(throwsAssertionError));
+      expect(() => initWithAlpha(100), throwsAssertionError);
+    });
+
+    test('toJson', () {
+      final BitmapDescriptor testDescriptor =
+          BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueCyan);
+      final Marker marker = Marker(
+        markerId: MarkerId("ABC123"),
+        alpha: 0.12345,
+        anchor: Offset(100, 100),
+        consumeTapEvents: true,
+        draggable: true,
+        flat: true,
+        icon: testDescriptor,
+        infoWindow: InfoWindow(
+          title: "Test title",
+          snippet: "Test snippet",
+          anchor: Offset(100, 200),
+        ),
+        position: LatLng(50, 50),
+        rotation: 100,
+        visible: false,
+        zIndex: 100,
+        onTap: () {},
+        onDragStart: (LatLng latLng) {},
+        onDrag: (LatLng latLng) {},
+        onDragEnd: (LatLng latLng) {},
+      );
+
+      final Map<String, Object> json = marker.toJson() as Map<String, Object>;
+
+      expect(json, <String, Object>{
+        'markerId': "ABC123",
+        'alpha': 0.12345,
+        'anchor': <double>[100, 100],
+        'consumeTapEvents': true,
+        'draggable': true,
+        'flat': true,
+        'icon': testDescriptor.toJson(),
+        'infoWindow': <String, Object>{
+          'title': "Test title",
+          'snippet': "Test snippet",
+          'anchor': <Object>[100.0, 200.0],
+        },
+        'position': <double>[50, 50],
+        'rotation': 100.0,
+        'visible': false,
+        'zIndex': 100.0,
+      });
+    });
+    test('clone', () {
+      final Marker marker = Marker(markerId: MarkerId("ABC123"));
+      final Marker clone = marker.clone();
+
+      expect(identical(clone, marker), isFalse);
+      expect(clone, equals(marker));
+    });
+    test('copyWith', () {
+      final Marker marker = Marker(markerId: MarkerId("ABC123"));
+
+      final BitmapDescriptor testDescriptor =
+          BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueCyan);
+      final double testAlphaParam = 0.12345;
+      final Offset testAnchorParam = Offset(100, 100);
+      final bool testConsumeTapEventsParam = !marker.consumeTapEvents;
+      final bool testDraggableParam = !marker.draggable;
+      final bool testFlatParam = !marker.flat;
+      final BitmapDescriptor testIconParam = testDescriptor;
+      final InfoWindow testInfoWindowParam = InfoWindow(title: "Test");
+      final LatLng testPositionParam = LatLng(100, 100);
+      final double testRotationParam = 100;
+      final bool testVisibleParam = !marker.visible;
+      final double testZIndexParam = 100;
+      final List<String> log = [];
+
+      final copy = marker.copyWith(
+        alphaParam: testAlphaParam,
+        anchorParam: testAnchorParam,
+        consumeTapEventsParam: testConsumeTapEventsParam,
+        draggableParam: testDraggableParam,
+        flatParam: testFlatParam,
+        iconParam: testIconParam,
+        infoWindowParam: testInfoWindowParam,
+        positionParam: testPositionParam,
+        rotationParam: testRotationParam,
+        visibleParam: testVisibleParam,
+        zIndexParam: testZIndexParam,
+        onTapParam: () {
+          log.add("onTapParam");
+        },
+        onDragStartParam: (LatLng latLng) {
+          log.add("onDragStartParam");
+        },
+        onDragParam: (LatLng latLng) {
+          log.add("onDragParam");
+        },
+        onDragEndParam: (LatLng latLng) {
+          log.add("onDragEndParam");
+        },
+      );
+
+      expect(copy.alpha, equals(testAlphaParam));
+      expect(copy.anchor, equals(testAnchorParam));
+      expect(copy.consumeTapEvents, equals(testConsumeTapEventsParam));
+      expect(copy.draggable, equals(testDraggableParam));
+      expect(copy.flat, equals(testFlatParam));
+      expect(copy.icon, equals(testIconParam));
+      expect(copy.infoWindow, equals(testInfoWindowParam));
+      expect(copy.position, equals(testPositionParam));
+      expect(copy.rotation, equals(testRotationParam));
+      expect(copy.visible, equals(testVisibleParam));
+      expect(copy.zIndex, equals(testZIndexParam));
+
+      copy.onTap!();
+      expect(log, contains("onTapParam"));
+
+      copy.onDragStart!(LatLng(0, 1));
+      expect(log, contains("onDragStartParam"));
+
+      copy.onDrag!(LatLng(0, 1));
+      expect(log, contains("onDragParam"));
+
+      copy.onDragEnd!(LatLng(0, 1));
+      expect(log, contains("onDragEndParam"));
+    });
+  });
+}