Skip to content

Commit 3c83fd9

Browse files
authored
Nearbybus enhancement (#27)
* feat(android): marker press haptic feedback and custom visibility * marker movemment check * enhance: callout border thickness and anchor changes * enhance: added callout config
1 parent ab981d1 commit 3c83fd9

1 file changed

Lines changed: 199 additions & 37 deletions

File tree

android/src/main/java/com/rnmaps/maps/MapView.java

Lines changed: 199 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
import android.graphics.Paint;
1818
import android.graphics.Rect;
1919
import android.graphics.RectF;
20+
import android.graphics.drawable.Drawable;
21+
import androidx.core.content.ContextCompat;
2022

2123

2224
import androidx.lifecycle.DefaultLifecycleObserver;
@@ -25,6 +27,7 @@
2527
import android.util.Log;
2628
import android.view.GestureDetector;
2729
import android.view.MotionEvent;
30+
import android.view.HapticFeedbackConstants;
2831
import android.view.View;
2932
import android.view.ViewGroup;
3033
import android.view.animation.LinearInterpolator;
@@ -505,6 +508,7 @@ public boolean onMarkerClick(@NonNull Marker marker) {
505508
String id = null;
506509
String action = "marker-press";
507510
String actionType = null;
511+
boolean isPressFeedbackEnabled = false;
508512

509513
if (tag instanceof java.util.Map) {
510514
java.util.Map tagMap = (java.util.Map) tag;
@@ -520,9 +524,16 @@ public boolean onMarkerClick(@NonNull Marker marker) {
520524
if (actionTypeObj instanceof String) {
521525
actionType = (String) actionTypeObj;
522526
}
527+
Object isPressFeedbackEnabledObj = tagMap.get("isPressFeedbackEnabled");
528+
if (isPressFeedbackEnabledObj instanceof Boolean) {
529+
isPressFeedbackEnabled = (Boolean) isPressFeedbackEnabledObj;
530+
}
523531
}
524532

525533
if (id != null) {
534+
if (isPressFeedbackEnabled) {
535+
performMarkerPressFeedback(marker);
536+
}
526537
WritableMap mapEventData = makeClickEventData(marker.getPosition());
527538
mapEventData.putString("action", "marker-press");
528539
mapEventData.putString("actionType", actionType);
@@ -1958,54 +1969,168 @@ private boolean safeRemoveFeatureFromAttacherGroup(MapFeature feature) {
19581969
}
19591970
return false;
19601971
}
1961-
private Bitmap createSimpleLabel(String title) {
1962-
// Text paint
1972+
1973+
private Bitmap createSimpleLabel(
1974+
String title,
1975+
List<String> iconNames,
1976+
org.json.JSONObject calloutConfig
1977+
) {
1978+
1979+
Context context = getContext();
1980+
float density = context.getResources().getDisplayMetrics().density;
1981+
1982+
// ---------- CONFIG ----------
1983+
int padding = (int) (8 * density);
1984+
int iconSize = (int) (14 * density);
1985+
int iconSpacing = (int) (4 * density);
1986+
float textSize = 13 * density;
1987+
float cornerRadius = 12 * density;
1988+
float borderWidth = 0.5f * density;
1989+
1990+
if (calloutConfig != null) {
1991+
padding = (int) (calloutConfig.optDouble("padding", 8) * density);
1992+
iconSize = (int) (calloutConfig.optDouble("iconSize", 14) * density);
1993+
iconSpacing = (int) (calloutConfig.optDouble("iconSpacing", 4) * density);
1994+
textSize = (float) (calloutConfig.optDouble("textSize", 13) * density);
1995+
cornerRadius = (float) (calloutConfig.optDouble("cornerRadius", 12) * density);
1996+
borderWidth = (float) (calloutConfig.optDouble("borderWidth", 0.5) * density);
1997+
}
1998+
// ----------------------------
1999+
19632000
Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
19642001
textPaint.setColor(Color.BLACK);
1965-
textPaint.setTextSize(36f);
2002+
textPaint.setTextSize(textSize);
19662003

1967-
// Measure text
1968-
Rect bounds = new Rect();
1969-
textPaint.getTextBounds(title, 0, title.length(), bounds);
2004+
Paint.FontMetrics fm = textPaint.getFontMetrics();
2005+
float textHeight = fm.bottom - fm.top;
2006+
float textWidth = textPaint.measureText(title);
19702007

1971-
int padding = 20;
1972-
int width = bounds.width() + padding * 2;
1973-
int height = bounds.height() + padding * 2;
2008+
int iconCount = (iconNames != null) ? iconNames.size() : 0;
2009+
2010+
int totalIconsWidth = 0;
2011+
if (iconCount > 0) {
2012+
totalIconsWidth =
2013+
(iconSize * iconCount)
2014+
+ (iconSpacing * (iconCount - 1));
2015+
}
2016+
2017+
int width = (int) (
2018+
padding * 2
2019+
+ textWidth
2020+
+ (iconCount > 0 ? padding : 0)
2021+
+ totalIconsWidth
2022+
);
2023+
2024+
int height = (int) (
2025+
Math.max(iconSize, textHeight)
2026+
+ padding * 2
2027+
);
19742028

1975-
// Create bitmap
19762029
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
19772030
Canvas canvas = new Canvas(bitmap);
19782031

1979-
// Background paint
2032+
// Background
19802033
Paint bgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
19812034
bgPaint.setColor(Color.WHITE);
19822035

1983-
// Rounded rectangle bounds
1984-
android.graphics.RectF rectF = new android.graphics.RectF(0, 0, width, height);
1985-
float cornerRadius = 15f;
1986-
1987-
// Draw white background rounded rectangle
2036+
RectF rectF = new RectF(0, 0, width, height);
19882037
canvas.drawRoundRect(rectF, cornerRadius, cornerRadius, bgPaint);
19892038

1990-
// --- BORDER ---
2039+
// Border
19912040
Paint borderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
1992-
borderPaint.setColor(Color.BLACK); // border color
2041+
borderPaint.setColor(Color.BLACK);
19932042
borderPaint.setStyle(Paint.Style.STROKE);
1994-
borderPaint.setStrokeWidth(1f); // thickness (px)
1995-
2043+
borderPaint.setStrokeWidth(borderWidth);
19962044
canvas.drawRoundRect(rectF, cornerRadius, cornerRadius, borderPaint);
1997-
// --------------
19982045

2046+
// Draw Text
2047+
float textX = padding;
2048+
float textY = height / 2f - (fm.ascent + fm.descent) / 2f;
2049+
canvas.drawText(title, textX, textY, textPaint);
2050+
2051+
// Draw Icons (RIGHT)
2052+
if (iconCount > 0) {
2053+
2054+
int startX = (int) (padding + textWidth + padding);
2055+
2056+
for (int i = 0; i < iconCount; i++) {
19992057

2000-
// Draw black text
2001-
float x = padding;
2002-
float y = padding - bounds.top; // align text baseline
2003-
canvas.drawText(title, x, y, textPaint);
2058+
String assetName = iconNames.get(i);
2059+
2060+
int resId = getResources().getIdentifier(
2061+
assetName,
2062+
"drawable",
2063+
context.getPackageName()
2064+
);
2065+
2066+
if (resId != 0) {
2067+
2068+
Drawable drawable =
2069+
ContextCompat.getDrawable(context, resId);
2070+
2071+
if (drawable != null) {
2072+
2073+
int left = startX + i * (iconSize + iconSpacing);
2074+
int top = (height - iconSize) / 2;
2075+
int right = left + iconSize;
2076+
int bottom = top + iconSize;
2077+
2078+
drawable.setBounds(left, top, right, bottom);
2079+
drawable.draw(canvas);
2080+
}
2081+
}
2082+
}
2083+
}
20042084

20052085
return bitmap;
20062086
}
20072087

20082088

2089+
private void performMarkerPressFeedback(final Marker marker) {
2090+
this.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
2091+
2092+
marker.setAlpha(0.6f);
2093+
2094+
this.postDelayed(new Runnable() {
2095+
@Override
2096+
public void run() {
2097+
marker.setAlpha(1.0f);
2098+
}
2099+
}, 120);
2100+
}
2101+
2102+
private final Double POS_EPS_DEG = 0.00001;
2103+
2104+
private boolean hasPositionChanged(LatLng p1, LatLng p2) {
2105+
return Math.abs(p1.latitude - p2.latitude) >= POS_EPS_DEG
2106+
&& Math.abs(p1.longitude - p2.longitude) >= POS_EPS_DEG;
2107+
}
2108+
2109+
private float clamp(float value, float min, float max) {
2110+
return Math.max(min, Math.min(max, value));
2111+
}
2112+
2113+
2114+
private float getAdaptiveCalloutAnchorV(float rotationDegrees) {
2115+
float r = (rotationDegrees + 360f) % 360f;
2116+
2117+
final float BASE_ANCHOR = 2.4f;
2118+
2119+
final float EAST_WEST_ANCHOR = 1.9f;
2120+
2121+
if (r >= 50f && r <= 130f) {
2122+
return EAST_WEST_ANCHOR;
2123+
}
2124+
2125+
if (r >= 230f && r <= 310f) {
2126+
return EAST_WEST_ANCHOR;
2127+
}
2128+
2129+
return BASE_ANCHOR;
2130+
}
2131+
2132+
2133+
20092134
// Native nearby markers management
20102135
public void updateNearbyMarkersFromProcessedData(org.json.JSONArray processedMarkers) {
20112136

@@ -2035,7 +2160,27 @@ public void updateNearbyMarkersFromProcessedData(org.json.JSONArray processedMar
20352160
boolean rotationEnabled = markerData.optBoolean("rotationEnabled", false);
20362161
String routeCode = markerData.optString("routeCode", "");
20372162
String action = markerData.optString("action", "marker-press");
2163+
boolean isVisible = markerData.optBoolean("isVisible", true);
2164+
boolean isPressFeedbackEnabled = markerData.optBoolean("isPressFeedbackEnabled", false);
2165+
org.json.JSONArray iconsArray = markerData.optJSONArray("icons");
2166+
org.json.JSONObject calloutConfig = markerData.optJSONObject("calloutConfig");
2167+
2168+
List<String> labelIcons = new ArrayList<>();
2169+
2170+
2171+
2172+
if (iconsArray != null) {
2173+
2174+
for (int j = 0; j < iconsArray.length(); j++) {
2175+
2176+
labelIcons.add(iconsArray.getString(j));
2177+
}
2178+
}
2179+
20382180

2181+
float calloutAnchorV = 2.4f;
2182+
float calloutAnchorU = 0.5f;
2183+
20392184

20402185
if (markerAnimators.containsKey(driverId)) {
20412186
ValueAnimator animator = markerAnimators.get(driverId);
@@ -2044,36 +2189,52 @@ public void updateNearbyMarkersFromProcessedData(org.json.JSONArray processedMar
20442189
}
20452190
markerAnimators.remove(driverId);
20462191
}
2047-
2192+
20482193
seenDrivers.add(driverId);
20492194

20502195
Marker existingMarker = nearbyMarkersCache.get(driverId);
20512196
Marker existingCalloutMarker = nearbyMarkersCalloutCache.get(driverId);
20522197

20532198
if (existingMarker != null) {
2054-
// Calculate bearing if rotation is missing and position changed
2055-
2199+
existingMarker.setVisible(isVisible);
2200+
if(existingCalloutMarker != null){
2201+
existingCalloutMarker.setVisible(isVisible);
2202+
}
20562203
LatLng start = existingMarker.getPosition();
20572204
LatLng end = new LatLng(lat, lon);
2205+
20582206

20592207
if(!isCluster){
2060-
if (start.latitude != end.latitude || start.longitude != end.longitude) {
2208+
if (hasPositionChanged(start, end)) {
20612209
Location locStart = new Location("start");
20622210
locStart.setLatitude(start.latitude);
20632211
locStart.setLongitude(start.longitude);
20642212

20652213
Location locEnd = new Location("end");
20662214
locEnd.setLatitude(end.latitude);
20672215
locEnd.setLongitude(end.longitude);
2068-
20692216
rotation = locStart.bearingTo(locEnd);
20702217
} else {
20712218
rotation = existingMarker.getRotation();
20722219
}
2220+
2221+
2222+
}
2223+
2224+
calloutAnchorV = getAdaptiveCalloutAnchorV((float) rotation);
2225+
if(existingCalloutMarker != null) {
2226+
existingCalloutMarker.setAnchor(calloutAnchorU, calloutAnchorV);
20732227
}
20742228

2229+
if(rotationEnabled){
2230+
existingMarker.setRotation((float) rotation);
2231+
} else {
2232+
existingMarker.setIcon(getIconFromAssets(vehicleVariant, rotation, isCluster, clusterCount, size,rotationEnabled));
2233+
}
2234+
20752235
// Update existing marker
20762236
if (shouldAnimate) {
2237+
if(hasPositionChanged(start, end)){
20772238
ValueAnimator animator = animateMarkers(existingMarker,existingCalloutMarker, new com.google.android.gms.maps.model.LatLng(lat, lon), animationDuration);
20782239
markerAnimators.put(driverId, animator);
20792240
animator.addListener(new AnimatorListenerAdapter() {
@@ -2084,13 +2245,13 @@ public void onAnimationEnd(Animator animation) {
20842245
}
20852246
}
20862247
});
2248+
};
20872249
} else {
20882250
existingMarker.setPosition(new com.google.android.gms.maps.model.LatLng(lat, lon));
20892251
if(existingCalloutMarker != null) existingCalloutMarker.setPosition(new com.google.android.gms.maps.model.LatLng(lat, lon));
20902252
}
20912253
existingMarker.setZIndex(zIndex);
2092-
if(rotationEnabled) existingMarker.setRotation((float) rotation);
2093-
else existingMarker.setIcon(getIconFromAssets(vehicleVariant, rotation, isCluster, clusterCount, size,rotationEnabled));
2254+
20942255

20952256
// Update icon if vehicle variant changed
20962257

@@ -2101,7 +2262,7 @@ public void onAnimationEnd(Animator animation) {
21012262
.anchor(0.5f, 0.5f)
21022263
.zIndex(zIndex)
21032264
.flat(true)
2104-
.visible(true);
2265+
.visible(isVisible);
21052266

21062267

21072268
// Use custom marker icon based on vehicle variant and rotation
@@ -2112,23 +2273,24 @@ public void onAnimationEnd(Animator animation) {
21122273

21132274
if (newMarker != null) {
21142275
if(!isCluster && !routeCode.isEmpty()) {
2115-
java.util.Map<String, String> tagMap = new java.util.HashMap<>();
2276+
java.util.Map<String, Object> tagMap = new java.util.HashMap<>();
21162277
tagMap.put("id", routeCode);
21172278
tagMap.put("action", "marker-press");
21182279
tagMap.put("actionType", action);
2280+
tagMap.put("isPressFeedbackEnabled", isPressFeedbackEnabled);
21192281
newMarker.setTag(tagMap);
21202282
}
21212283
nearbyMarkersCache.put(driverId, newMarker);
21222284
}
21232285
if(!title.isEmpty()) {
2124-
Bitmap labelBitmap = createSimpleLabel(title);
2286+
Bitmap labelBitmap = createSimpleLabel(title, labelIcons, calloutConfig);
21252287
MarkerOptions calloutMarkerOptions = new MarkerOptions()
21262288
.position(new com.google.android.gms.maps.model.LatLng(lat, lon))
21272289
.icon(com.google.android.gms.maps.model.BitmapDescriptorFactory.fromBitmap(labelBitmap))
2128-
.anchor(0.5f, 2.4f)
2290+
.anchor(calloutAnchorU, calloutAnchorV)
21292291
.zIndex(600)
21302292
.flat(true)
2131-
.visible(true);
2293+
.visible(isVisible);
21322294
Marker newCalloutMarker = markerCollection.addMarker(calloutMarkerOptions);
21332295
if(newCalloutMarker != null) nearbyMarkersCalloutCache.put(driverId, newCalloutMarker);
21342296
}

0 commit comments

Comments
 (0)