1717import android .graphics .Paint ;
1818import android .graphics .Rect ;
1919import android .graphics .RectF ;
20+ import android .graphics .drawable .Drawable ;
21+ import androidx .core .content .ContextCompat ;
2022
2123
2224import androidx .lifecycle .DefaultLifecycleObserver ;
2527import android .util .Log ;
2628import android .view .GestureDetector ;
2729import android .view .MotionEvent ;
30+ import android .view .HapticFeedbackConstants ;
2831import android .view .View ;
2932import android .view .ViewGroup ;
3033import 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