Skip to content

Commit 4138933

Browse files
committed
add moveMarker and addTextMarker
1 parent 2349ab9 commit 4138933

File tree

8 files changed

+779
-2
lines changed

8 files changed

+779
-2
lines changed

android/src/main/java/com/google/android/react/navsdk/MapViewController.java

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@
1515

1616
import android.annotation.SuppressLint;
1717
import android.app.Activity;
18+
import android.graphics.Bitmap;
19+
import android.graphics.Canvas;
1820
import android.graphics.Color;
21+
import android.graphics.Paint;
22+
import android.graphics.Rect;
23+
import android.graphics.Typeface;
1924
import androidx.core.util.Supplier;
2025
import com.facebook.react.bridge.UiThreadUtil;
2126
import com.google.android.gms.maps.CameraUpdateFactory;
@@ -42,9 +47,12 @@
4247
import java.net.HttpURLConnection;
4348
import java.net.URL;
4449
import java.util.ArrayList;
50+
import java.util.HashMap;
4551
import java.util.List;
4652
import java.util.Map;
4753
import java.util.concurrent.Executors;
54+
import android.animation.ValueAnimator;
55+
import android.view.animation.AccelerateDecelerateInterpolator;
4856

4957
public class MapViewController {
5058
private GoogleMap mGoogleMap;
@@ -304,6 +312,62 @@ public GroundOverlay addGroundOverlay(Map<String, Object> map) {
304312
return groundOverlay;
305313
}
306314

315+
/**
316+
* Animates a marker to a new position with smooth movement.
317+
*
318+
* @param markerId The ID of the marker to move
319+
* @param newPosition The new position to move the marker to
320+
* @param duration The duration of the animation in milliseconds
321+
*/
322+
public void moveMarker(String markerId, Map<String, Object> newPosition, int duration) {
323+
if (mGoogleMap == null) {
324+
return;
325+
}
326+
327+
UiThreadUtil.runOnUiThread(() -> {
328+
// Find the marker
329+
Marker markerToMove = null;
330+
for (Marker marker : markerList) {
331+
if (marker.getId().equals(markerId)) {
332+
markerToMove = marker;
333+
break;
334+
}
335+
}
336+
337+
if (markerToMove == null) {
338+
return; // Marker not found
339+
}
340+
341+
final Marker finalMarker = markerToMove;
342+
final LatLng startPosition = finalMarker.getPosition();
343+
final LatLng endPosition = ObjectTranslationUtil.getLatLngFromMap(newPosition);
344+
345+
346+
// Create smooth animation
347+
ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
348+
animator.setDuration(duration > 0 ? duration : 1000); // Default 1 second
349+
animator.setInterpolator(new AccelerateDecelerateInterpolator());
350+
351+
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
352+
@Override
353+
public void onAnimationUpdate(ValueAnimator animation) {
354+
float progress = (float) animation.getAnimatedValue();
355+
356+
// Calculate intermediate position using linear interpolation
357+
double lat = startPosition.latitude + (endPosition.latitude - startPosition.latitude) * progress;
358+
double lng = startPosition.longitude + (endPosition.longitude - startPosition.longitude) * progress;
359+
LatLng intermediatePosition = new LatLng(lat, lng);
360+
361+
// Update marker position
362+
finalMarker.setPosition(intermediatePosition);
363+
}
364+
});
365+
366+
animator.start();
367+
});
368+
}
369+
370+
307371
public void removeMarker(String id) {
308372
UiThreadUtil.runOnUiThread(
309373
() -> {
@@ -357,6 +421,73 @@ public void removeGroundOverlay(String id) {
357421
}
358422
}
359423

424+
/**
425+
* Adds a text marker on the map using a custom bitmap with text and background.
426+
*
427+
* @param optionsMap Configuration map containing text, position, styling options
428+
* @return The created Marker containing the text with background
429+
*/
430+
public Marker addTextMarker(Map<String, Object> optionsMap) {
431+
if (mGoogleMap == null) {
432+
return null;
433+
}
434+
435+
// Extract text marker parameters
436+
String text = CollectionUtil.getString("text", optionsMap);
437+
if (text == null || text.isEmpty()) {
438+
return null; // Text is required
439+
}
440+
441+
// Position parameters
442+
LatLng position = ObjectTranslationUtil.getLatLngFromMap((Map<String, Object>) optionsMap.get("position"));
443+
if (position == null) {
444+
return null; // Position is required
445+
}
446+
447+
// Styling parameters with defaults
448+
float fontSize = (float) CollectionUtil.getDouble("fontSize", optionsMap, 14.0);
449+
String textColor = CollectionUtil.getString("textColor", optionsMap);
450+
int textColorInt = textColor != null ? Color.parseColor(textColor) : Color.BLACK;
451+
452+
String backgroundColor = CollectionUtil.getString("backgroundColor", optionsMap);
453+
int bgColor = backgroundColor != null ? Color.parseColor(backgroundColor) : Color.WHITE;
454+
455+
float padding = (float) CollectionUtil.getDouble("padding", optionsMap, 8.0);
456+
457+
// Border styling parameters
458+
String borderColorStr = CollectionUtil.getString("borderColor", optionsMap);
459+
int borderColor = borderColorStr != null ? Color.parseColor(borderColorStr) : Color.BLACK;
460+
461+
// Label text (optional)
462+
String label = CollectionUtil.getString("label", optionsMap);
463+
464+
// Generate bitmap with text and circle background
465+
BitmapDescriptor bitmapDescriptor = createTextBitmap(text, textColorInt, bgColor, fontSize, padding, borderColor, label);
466+
if (bitmapDescriptor == null) {
467+
return null;
468+
}
469+
470+
// Calculate anchor point to position circle center at the specified coordinates
471+
float anchorU = 0.5f; // Center horizontally
472+
float anchorV = calculateAnchorV(text, fontSize, padding, label);
473+
474+
// Create marker options with custom text bitmap
475+
MarkerOptions options = new MarkerOptions();
476+
options.icon(bitmapDescriptor);
477+
options.position(position);
478+
options.anchor(anchorU, anchorV);
479+
480+
// Add optional title if provided
481+
String title = CollectionUtil.getString("title", optionsMap);
482+
if (title != null) {
483+
options.title(title);
484+
}
485+
486+
Marker textMarker = mGoogleMap.addMarker(options);
487+
markerList.add(textMarker);
488+
return textMarker;
489+
}
490+
360491
public void setMapStyle(String url) {
361492
Executors.newSingleThreadExecutor()
362493
.execute(
@@ -518,6 +649,7 @@ public void clearMapView() {
518649
return;
519650
}
520651

652+
521653
mGoogleMap.clear();
522654
}
523655

@@ -579,4 +711,171 @@ private LatLng createLatLng(Map<String, Object> map) {
579711

580712
return new LatLng(lat, lng);
581713
}
714+
715+
716+
717+
718+
719+
/**
720+
* Calculates the vertical anchor point to position the circle center at the marker position.
721+
*
722+
* @param text The text to render in the circle
723+
* @param fontSize The font size in pixels
724+
* @param padding The padding around the text
725+
* @param label Optional label text (can be null)
726+
* @return The vertical anchor value (0-1) where 0 is top and 1 is bottom of the bitmap
727+
*/
728+
private float calculateAnchorV(String text, float fontSize, float padding, String label) {
729+
try {
730+
// Create paint to measure text
731+
Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
732+
textPaint.setTextSize(fontSize);
733+
textPaint.setTypeface(Typeface.DEFAULT_BOLD);
734+
735+
// Measure text dimensions
736+
Rect textBounds = new Rect();
737+
textPaint.getTextBounds(text, 0, text.length(), textBounds);
738+
int textWidth = textBounds.width();
739+
int textHeight = textBounds.height();
740+
741+
// Calculate circle dimensions
742+
int circleDiameter = (int) ((textWidth > textHeight ? textWidth : textHeight) + (padding * 2));
743+
float borderWidth = circleDiameter * 0.08f;
744+
745+
// Calculate label height if label exists
746+
int labelRectHeight = 0;
747+
if (label != null && !label.isEmpty()) {
748+
Paint labelPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
749+
labelPaint.setTextSize(fontSize * 0.6f);
750+
Rect labelBounds = new Rect();
751+
labelPaint.getTextBounds(label, 0, label.length(), labelBounds);
752+
labelRectHeight = (int) (labelBounds.height() + (padding * 0.8f));
753+
}
754+
755+
// Calculate total bitmap height and circle center position
756+
float bitmapHeight = circleDiameter + (borderWidth * 2) + labelRectHeight;
757+
float circleCenterY = (circleDiameter + (borderWidth * 2)) / 2f;
758+
759+
// Return anchor V (fraction of bitmap height where circle center is)
760+
return circleCenterY / bitmapHeight;
761+
} catch (Exception e) {
762+
// Default to center if calculation fails
763+
return 0.5f;
764+
}
765+
}
766+
767+
/**
768+
* Creates a bitmap with text on a background circle with border.
769+
* Optionally draws a label text on a rectangle below the circle.
770+
* The border width is automatically calculated as 8% of the circle diameter.
771+
*
772+
* @param text The text to render in the circle
773+
* @param textColor The color of the text
774+
* @param bgColor The background circle color
775+
* @param fontSize The font size in pixels
776+
* @param padding The padding around the text
777+
* @param borderColor The border circle color
778+
* @param label Optional label text to display below the circle (can be null)
779+
* @return BitmapDescriptor containing the rendered text with circle background
780+
*/
781+
private BitmapDescriptor createTextBitmap(String text, int textColor, int bgColor,
782+
float fontSize, float padding, int borderColor, String label) {
783+
try {
784+
// Create paint for text
785+
Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
786+
textPaint.setColor(textColor);
787+
textPaint.setTextSize(fontSize);
788+
textPaint.setTypeface(Typeface.DEFAULT_BOLD);
789+
790+
// Create paint for background
791+
Paint bgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
792+
bgPaint.setColor(bgColor);
793+
794+
// Measure main text dimensions
795+
Rect textBounds = new Rect();
796+
textPaint.getTextBounds(text, 0, text.length(), textBounds);
797+
798+
int textWidth = textBounds.width();
799+
int textHeight = textBounds.height();
800+
801+
// Calculate circle dimensions
802+
int circleDiameter = (int) ((textWidth > textHeight ? textWidth : textHeight) + (padding * 2));
803+
float borderWidth = circleDiameter * 0.08f;
804+
805+
// Calculate label dimensions if label exists
806+
boolean hasLabel = label != null && !label.isEmpty();
807+
int labelRectHeight = 0;
808+
int labelRectWidth = 0;
809+
Rect labelBounds = new Rect();
810+
Paint labelPaint = null;
811+
812+
if (hasLabel) {
813+
// Create paint for label text (smaller font size)
814+
labelPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
815+
labelPaint.setColor(textColor);
816+
labelPaint.setTextSize(fontSize * 0.6f); // 60% of main text size
817+
labelPaint.setTypeface(Typeface.DEFAULT);
818+
labelPaint.getTextBounds(label, 0, label.length(), labelBounds);
819+
820+
labelRectHeight = (int) (labelBounds.height() + (padding * 0.8f)); // Smaller padding for label
821+
labelRectWidth = (int) Math.max(circleDiameter + (borderWidth * 2), labelBounds.width() + padding);
822+
}
823+
824+
// Calculate total bitmap dimensions
825+
int bitmapWidth = hasLabel ? labelRectWidth : (int) (circleDiameter + (borderWidth * 2));
826+
int bitmapHeight = (int) (circleDiameter + (borderWidth * 2) + labelRectHeight);
827+
828+
// Create bitmap and canvas
829+
Bitmap bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888);
830+
Canvas canvas = new Canvas(bitmap);
831+
832+
// Calculate circle center (at top of bitmap)
833+
float centerX = bitmapWidth / 2f;
834+
float centerY = (circleDiameter + (borderWidth * 2)) / 2f;
835+
float radius = (circleDiameter - borderWidth) / 2f;
836+
837+
// Draw background circle
838+
canvas.drawCircle(centerX, centerY, radius, bgPaint);
839+
840+
// Draw border circle
841+
if (borderWidth > 0) {
842+
Paint borderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
843+
borderPaint.setColor(borderColor);
844+
borderPaint.setStyle(Paint.Style.STROKE);
845+
borderPaint.setStrokeWidth(borderWidth);
846+
canvas.drawCircle(centerX, centerY, radius, borderPaint);
847+
}
848+
849+
// Draw text centered on the circle
850+
float textX = centerX - (textBounds.width() / 2f);
851+
float textY = centerY + (textBounds.height() / 2f);
852+
canvas.drawText(text, textX, textY, textPaint);
853+
854+
// Draw label rectangle and text if label exists
855+
if (hasLabel) {
856+
float rectTop = circleDiameter + (borderWidth * 2);
857+
float rectLeft = (bitmapWidth - labelRectWidth) / 2f;
858+
float rectRight = rectLeft + labelRectWidth;
859+
float rectBottom = rectTop + labelRectHeight;
860+
float cornerRadius = padding * 0.5f;
861+
862+
// Draw rounded rectangle for label
863+
Paint rectPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
864+
rectPaint.setColor(bgColor);
865+
canvas.drawRoundRect(rectLeft, rectTop, rectRight, rectBottom, cornerRadius, cornerRadius, rectPaint);
866+
867+
// Draw label text centered on rectangle
868+
float labelX = centerX - (labelBounds.width() / 2f);
869+
float labelY = rectTop + (labelRectHeight / 2f) - labelBounds.exactCenterY();
870+
canvas.drawText(label, labelX, labelY, labelPaint);
871+
}
872+
873+
874+
return BitmapDescriptorFactory.fromBitmap(bitmap);
875+
} catch (Exception e) {
876+
// Return null if bitmap creation fails
877+
return null;
878+
}
879+
}
880+
582881
}

android/src/main/java/com/google/android/react/navsdk/NavViewModule.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,25 @@ public void addMarker(int viewId, ReadableMap markerOptionsMap, final Promise pr
171171
});
172172
}
173173

174+
175+
176+
@ReactMethod
177+
public void moveMarker(int viewId, String markerId, ReadableMap newPosition, int duration,
178+
final Promise promise) {
179+
UiThreadUtil.runOnUiThread(
180+
() -> {
181+
if (mNavViewManager.getGoogleMap(viewId) != null) {
182+
mNavViewManager
183+
.getFragmentForViewId(viewId)
184+
.getMapController()
185+
.moveMarker(markerId, newPosition.toHashMap(), duration);
186+
promise.resolve(true);
187+
} else {
188+
promise.reject("NO_MAP", "Map not found for viewId: " + viewId);
189+
}
190+
});
191+
}
192+
174193
@ReactMethod
175194
public void addPolyline(int viewId, ReadableMap polylineOptionsMap, final Promise promise) {
176195
UiThreadUtil.runOnUiThread(
@@ -233,6 +252,28 @@ public void addGroundOverlay(int viewId, ReadableMap overlayOptionsMap, final Pr
233252
});
234253
}
235254

255+
@ReactMethod
256+
public void addTextMarker(int viewId, ReadableMap textMarkerOptionsMap, final Promise promise) {
257+
UiThreadUtil.runOnUiThread(
258+
() -> {
259+
if (mNavViewManager.getGoogleMap(viewId) == null) {
260+
promise.reject(JsErrors.NO_MAP_ERROR_CODE, JsErrors.NO_MAP_ERROR_MESSAGE);
261+
return;
262+
}
263+
Marker textMarker =
264+
mNavViewManager
265+
.getFragmentForViewId(viewId)
266+
.getMapController()
267+
.addTextMarker(textMarkerOptionsMap.toHashMap());
268+
269+
if (textMarker != null) {
270+
promise.resolve(ObjectTranslationUtil.getMapFromMarker(textMarker));
271+
} else {
272+
promise.reject("TEXT_MARKER_ERROR", "Failed to create text marker");
273+
}
274+
});
275+
}
276+
236277
@Override
237278
public boolean canOverrideExistingModule() {
238279
return true;

0 commit comments

Comments
 (0)