From 679da7767d495edfff082646de9598dc791a99c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josu=C3=A9=20Moreno?= Date: Mon, 19 May 2025 06:38:02 -0500 Subject: [PATCH] Add: animate marker --- README.md | 15 ++++++ plugin/README.md | 17 +++++++ .../plugins/googlemaps/CapacitorGoogleMap.kt | 48 +++++++++++++++++++ .../googlemaps/CapacitorGoogleMapsPlugin.kt | 34 +++++++++++++ plugin/ios/Plugin/CapacitorGoogleMapsPlugin.m | 1 + .../Plugin/CapacitorGoogleMapsPlugin.swift | 31 ++++++++++++ plugin/ios/Plugin/Map.swift | 16 +++++++ plugin/src/definitions.ts | 8 ++++ plugin/src/implementation.ts | 10 ++++ plugin/src/map.ts | 19 ++++++++ plugin/src/web.ts | 31 ++++++++++++ 11 files changed, 230 insertions(+) diff --git a/README.md b/README.md index 99b1116d..ab648e28 100644 --- a/README.md +++ b/README.md @@ -301,6 +301,7 @@ export default MyMap; * [`enableClustering(...)`](#enableclustering) * [`disableClustering()`](#disableclustering) * [`addMarker(...)`](#addmarker) +* [`animateMarker(...)`](#animatemarker) * [`addMarkers(...)`](#addmarkers) * [`removeMarker(...)`](#removemarker) * [`removeMarkers(...)`](#removemarkers) @@ -416,6 +417,20 @@ addMarker(marker: Marker) => Promise -------------------- +### animateMarker(...) + +```typescript +animateMarker(markerId: string, lat: number, lng: number, duration?: number | undefined) => Promise +``` + +| Param | Type | +| -------------- | ------------------- | +| **`markerId`** | string | +| **`lat`** | number | +| **`lng`** | number | +| **`duration`** | number | + +-------------------- ### addMarkers(...) diff --git a/plugin/README.md b/plugin/README.md index 99b1116d..20a584c0 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -301,6 +301,7 @@ export default MyMap; * [`enableClustering(...)`](#enableclustering) * [`disableClustering()`](#disableclustering) * [`addMarker(...)`](#addmarker) +* [`animateMarker(...)`](#animatemarker) * [`addMarkers(...)`](#addmarkers) * [`removeMarker(...)`](#removemarker) * [`removeMarkers(...)`](#removemarkers) @@ -417,6 +418,22 @@ addMarker(marker: Marker) => Promise -------------------- +### animateMarker(...) + +```typescript +animateMarker(markerId: string, lat: number, lng: number, duration?: number | undefined) => Promise +``` + +| Param | Type | +| -------------- | ------------------- | +| **`markerId`** | string | +| **`lat`** | number | +| **`lng`** | number | +| **`duration`** | number | + +-------------------- + + ### addMarkers(...) ```typescript diff --git a/plugin/android/src/main/java/com/capacitorjs/plugins/googlemaps/CapacitorGoogleMap.kt b/plugin/android/src/main/java/com/capacitorjs/plugins/googlemaps/CapacitorGoogleMap.kt index a22aa220..f095c28d 100644 --- a/plugin/android/src/main/java/com/capacitorjs/plugins/googlemaps/CapacitorGoogleMap.kt +++ b/plugin/android/src/main/java/com/capacitorjs/plugins/googlemaps/CapacitorGoogleMap.kt @@ -20,6 +20,13 @@ import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel import java.io.InputStream import java.net.URL +import android.animation.ValueAnimator +import android.animation.AnimatorListenerAdapter +import android.animation.Animator +import com.google.android.gms.maps.model.LatLng +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch class CapacitorGoogleMap( @@ -244,6 +251,47 @@ class CapacitorGoogleMap( } } + fun animateMarker(markerId: String,lat: Double,lng: Double,duration: Long, callback: (Result) -> Unit + ) { + try { + googleMap ?: throw GoogleMapNotAvailable() + + CoroutineScope(Dispatchers.Main).launch { + try { + val wrapper = markers[markerId] ?: throw MarkerNotFoundError() + val marker = wrapper.googleMapMarker + ?: throw GoogleMapsError("GoogleMap Marker not available") + + val startPos = marker.position + val endPos = LatLng(lat, lng) + + ValueAnimator.ofFloat(0f, 1f).apply { + this.duration = duration + addUpdateListener { anim -> + val fraction = anim.animatedValue as Float + val newLat = startPos.latitude + + fraction * (endPos.latitude - startPos.latitude) + val newLng = startPos.longitude + + fraction * (endPos.longitude - startPos.longitude) + marker.position = LatLng(newLat, newLng) + } + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + callback(Result.success(Unit)) + } + }) + }.start() + } catch (e: GoogleMapsError) { + callback(Result.failure(e)) + } catch (e: Exception) { + callback(Result.failure(GoogleMapsError(e.localizedMessage ?: "Animation error"))) + } + } + } catch (e: GoogleMapsError) { + callback(Result.failure(e)) + } + } + fun addPolygons(newPolygons: List, callback: (ids: Result>) -> Unit) { try { googleMap ?: throw GoogleMapNotAvailable() diff --git a/plugin/android/src/main/java/com/capacitorjs/plugins/googlemaps/CapacitorGoogleMapsPlugin.kt b/plugin/android/src/main/java/com/capacitorjs/plugins/googlemaps/CapacitorGoogleMapsPlugin.kt index 3a73f590..13c158f8 100644 --- a/plugin/android/src/main/java/com/capacitorjs/plugins/googlemaps/CapacitorGoogleMapsPlugin.kt +++ b/plugin/android/src/main/java/com/capacitorjs/plugins/googlemaps/CapacitorGoogleMapsPlugin.kt @@ -237,6 +237,40 @@ class CapacitorGoogleMapsPlugin : Plugin(), OnMapsSdkInitializedCallback { } } + @PluginMethod + fun animateMarker(call: PluginCall) { + try { + val id = call.getString("id") + id ?: throw InvalidMapIdError() + + val markerId = call.getString("markerId") + markerId ?: throw InvalidArgumentsError("markerId is missing") + + val lat = call.getDouble("lat") + val lng = call.getDouble("lng") + if (lat == null || lng == null) { + throw InvalidArgumentsError("lat or lng is missing") + } + val duration = call.getLong("duration", 1000L) + + val map = maps[id] ?: throw MapNotFoundError() + + map.animateMarker(markerId, lat, lng, duration) { result -> + result + .onSuccess { + call.resolve() + } + .onFailure { error -> + handleError(call, error) + } + } + } catch (e: GoogleMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + @PluginMethod fun addMarkers(call: PluginCall) { try { diff --git a/plugin/ios/Plugin/CapacitorGoogleMapsPlugin.m b/plugin/ios/Plugin/CapacitorGoogleMapsPlugin.m index 4ae033d0..a3deaea5 100644 --- a/plugin/ios/Plugin/CapacitorGoogleMapsPlugin.m +++ b/plugin/ios/Plugin/CapacitorGoogleMapsPlugin.m @@ -8,6 +8,7 @@ CAP_PLUGIN_METHOD(enableTouch, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(disableTouch, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(addMarker, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(animateMarker, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(addMarkers, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(addPolygons, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(addPolylines, CAPPluginReturnPromise); diff --git a/plugin/ios/Plugin/CapacitorGoogleMapsPlugin.swift b/plugin/ios/Plugin/CapacitorGoogleMapsPlugin.swift index 3d2a32a5..1ed1d755 100644 --- a/plugin/ios/Plugin/CapacitorGoogleMapsPlugin.swift +++ b/plugin/ios/Plugin/CapacitorGoogleMapsPlugin.swift @@ -3,6 +3,7 @@ import Foundation import Capacitor import GoogleMaps import GoogleMapsUtils +import QuartzCore extension GMSMapViewType { static func fromString(mapType: String) -> GMSMapViewType { @@ -208,6 +209,36 @@ public class CapacitorGoogleMapsPlugin: CAPPlugin, GMSMapViewDelegate { } } + @objc func animateMarker(_ call: CAPPluginCall) { + do { + guard let id = call.getString("id") else { + throw GoogleMapErrors.invalidMapId + } + guard let markerId = call.getInt("markerId") else { + throw GoogleMapErrors.invalidArguments("markerId is missing") + } + guard let lat = call.getDouble("lat"), + let lng = call.getDouble("lng") else { + throw GoogleMapErrors.invalidArguments("lat or lng is missing") + } + let duration = call.getDouble("duration") ?? 1.0 + + guard let map = self.maps[id] else { + throw GoogleMapErrors.mapNotFound + } + + try map.animateMarker( + markerId: markerId, + to: LatLng(lat: lat, lng: lng), + duration: duration + ) + + call.resolve() + } catch { + handleError(call, error: error) + } + } + @objc func addMarkers(_ call: CAPPluginCall) { do { guard let id = call.getString("id") else { diff --git a/plugin/ios/Plugin/Map.swift b/plugin/ios/Plugin/Map.swift index 71e4a7de..f2a6c159 100644 --- a/plugin/ios/Plugin/Map.swift +++ b/plugin/ios/Plugin/Map.swift @@ -2,6 +2,7 @@ import Foundation import GoogleMaps import Capacitor import GoogleMapsUtils +import QuartzCore public struct LatLng: Codable { let lat: Double @@ -240,6 +241,21 @@ public class Map { return markerHash } + func animateMarker( markerId: Int, to target: LatLng, duration: Double) throws { + guard let marker = markers[markerId] else { + throw GoogleMapErrors.markerNotFound + } + DispatchQueue.main.async { + CATransaction.begin() + CATransaction.setAnimationDuration(duration) + marker.position = CLLocationCoordinate2D( + latitude: target.lat, + longitude: target.lng + ) + CATransaction.commit() + } + } + func addMarkers(markers: [Marker]) throws -> [Int] { var markerHashes: [Int] = [] diff --git a/plugin/src/definitions.ts b/plugin/src/definitions.ts index 6fa91254..02c891b2 100644 --- a/plugin/src/definitions.ts +++ b/plugin/src/definitions.ts @@ -391,6 +391,14 @@ export interface Marker { zIndex?: number; } +export interface AnimateMarkerOptions { + id: string; + markerId: string; + lat: number; + lng: number; + duration?: number; +} + /** * The callback function to be called when map events are emitted. */ diff --git a/plugin/src/implementation.ts b/plugin/src/implementation.ts index a7ff4535..be7e039d 100644 --- a/plugin/src/implementation.ts +++ b/plugin/src/implementation.ts @@ -2,6 +2,7 @@ import type { Plugin } from '@capacitor/core'; import { registerPlugin } from '@capacitor/core'; import type { + AnimateMarkerOptions, CameraConfig, Circle, GoogleMapConfig, @@ -73,6 +74,14 @@ export interface AddMarkerArgs { marker: Marker; } +export interface AnimateMarkerArgs { + id: string; + markerId: string; + lat: number; + lng: number; + duration?: number; +} + export interface AddPolygonsArgs { id: string; polygons: Polygon[]; @@ -174,6 +183,7 @@ export interface CapacitorGoogleMapsPlugin extends Plugin { enableTouch(args: { id: string }): Promise; disableTouch(args: { id: string }): Promise; addMarker(args: AddMarkerArgs): Promise<{ id: string }>; + animateMarker(options: AnimateMarkerOptions): Promise; addMarkers(args: AddMarkersArgs): Promise<{ ids: string[] }>; removeMarker(args: RemoveMarkerArgs): Promise; removeMarkers(args: RemoveMarkersArgs): Promise; diff --git a/plugin/src/map.ts b/plugin/src/map.ts index 977c032c..d0b6b1b3 100644 --- a/plugin/src/map.ts +++ b/plugin/src/map.ts @@ -36,6 +36,7 @@ export interface GoogleMapInterface { ): Promise; disableClustering(): Promise; addMarker(marker: Marker): Promise; + animateMarker(markerId: string,lat: number,lng: number,duration?: number): Promise; addMarkers(markers: Marker[]): Promise; removeMarker(id: string): Promise; removeMarkers(ids: string[]): Promise; @@ -348,6 +349,24 @@ export class GoogleMap { return res.id; } + /** + * Animation of marker + */ + async animateMarker( + markerId: string, + lat: number, + lng: number, + duration = 1000 + ): Promise { + await CapacitorGoogleMaps.animateMarker({ + id: this.id, + markerId, + lat, + lng, + duration, + }); + } + /** * Adds multiple markers to the map * diff --git a/plugin/src/web.ts b/plugin/src/web.ts index 08fa11ce..cbce92c6 100644 --- a/plugin/src/web.ts +++ b/plugin/src/web.ts @@ -27,6 +27,7 @@ import type { RemoveCirclesArgs, AddPolylinesArgs, RemovePolylinesArgs, + AnimateMarkerArgs, } from './implementation'; export class CapacitorGoogleMapsWeb extends WebPlugin implements CapacitorGoogleMapsPlugin { @@ -291,6 +292,36 @@ export class CapacitorGoogleMapsWeb extends WebPlugin implements CapacitorGoogle return { id: id }; } + + async animateMarker(_args: AnimateMarkerArgs): Promise { + const { id, markerId, lat, lng, duration = 1000 } = _args; + const mapData = this.maps[id]; + if (!mapData) { + throw new Error(`Map with id '${id}' not found`); + } + const marker = mapData.markers[markerId]; + if (!marker) { + throw new Error(`Marker '${markerId}' not found on map '${id}'`); + } + const start = marker.position as google.maps.LatLngLiteral; + const end = { lat, lng }; + const startTime = performance.now(); + + return new Promise(resolve => { + const step = (now: number) => { + const t = Math.min((now - startTime) / duration, 1); + const currLat = start.lat + (end.lat - start.lat) * t; + const currLng = start.lng + (end.lng - start.lng) * t; + marker.position = { lat: currLat, lng: currLng }; + if (t < 1) { + requestAnimationFrame(step); + } else { + resolve(); + } + }; + requestAnimationFrame(step); + }); + } async removeMarkers(_args: RemoveMarkersArgs): Promise { const map = this.maps[_args.id];