fix(android): Add option to move symbol location indicator bearing down the image stack (#4408)

This commit is contained in:
Adrian Cojocaru
2026-08-04 19:21:25 +03:00
committed by GitHub
parent a34723ff4a
commit 25e4f6acbf
7 changed files with 128 additions and 46 deletions

View File

@@ -133,6 +133,7 @@ public class LocationComponentOptions implements Parcelable {
private RectF trackingMultiFingerProtectedMoveArea;
private String layerAbove;
private String layerBelow;
private boolean bearingOnTop;
private float trackingAnimationDurationMultiplier;
private boolean compassAnimationEnabled;
private boolean accuracyAnimationEnabled;
@@ -177,6 +178,7 @@ public class LocationComponentOptions implements Parcelable {
RectF trackingMultiFingerProtectedMoveArea,
String layerAbove,
String layerBelow,
boolean bearingOnTop,
float trackingAnimationDurationMultiplier,
boolean compassAnimationEnabled,
boolean accuracyAnimationEnabled,
@@ -221,6 +223,7 @@ public class LocationComponentOptions implements Parcelable {
this.trackingMultiFingerProtectedMoveArea = trackingMultiFingerProtectedMoveArea;
this.layerAbove = layerAbove;
this.layerBelow = layerBelow;
this.bearingOnTop = bearingOnTop;
this.trackingAnimationDurationMultiplier = trackingAnimationDurationMultiplier;
this.compassAnimationEnabled = compassAnimationEnabled;
this.accuracyAnimationEnabled = accuracyAnimationEnabled;
@@ -327,6 +330,9 @@ public class LocationComponentOptions implements Parcelable {
builder.layerBelow(
typedArray.getString(R.styleable.maplibre_LocationComponent_maplibre_layer_below));
builder.bearingOnTop(
typedArray.getBoolean(R.styleable.maplibre_LocationComponent_maplibre_bearing_on_top, true));
float minScale = typedArray.getFloat(
R.styleable.maplibre_LocationComponent_maplibre_minZoomIconScale, MIN_ZOOM_ICON_SCALE_DEFAULT);
float maxScale = typedArray.getFloat(
@@ -798,6 +804,15 @@ public class LocationComponentOptions implements Parcelable {
return layerBelow;
}
/**
* Whether the bearing icon is rendered on top of the foreground icon in the location layer stack.
*
* @return true if the bearing icon is rendered above the foreground icon, false otherwise.
*/
public boolean bearingOnTop() {
return bearingOnTop;
}
/**
* Get the tracking animation duration multiplier.
*
@@ -927,6 +942,7 @@ public class LocationComponentOptions implements Parcelable {
+ "trackingMultiFingerProtectedMoveArea=" + trackingMultiFingerProtectedMoveArea + ", "
+ "layerAbove=" + layerAbove
+ "layerBelow=" + layerBelow
+ "bearingOnTop=" + bearingOnTop
+ "trackingAnimationDurationMultiplier=" + trackingAnimationDurationMultiplier
+ "pulseEnabled=" + pulseEnabled
+ "pulseFadeEnabled=" + pulseFadeEnabled
@@ -1057,6 +1073,10 @@ public class LocationComponentOptions implements Parcelable {
return false;
}
if (bearingOnTop != options.bearingOnTop) {
return false;
}
if (pulseEnabled != options.pulseEnabled) {
return false;
}
@@ -1120,6 +1140,7 @@ public class LocationComponentOptions implements Parcelable {
? trackingMultiFingerProtectedMoveArea.hashCode() : 0);
result = 31 * result + (layerAbove != null ? layerAbove.hashCode() : 0);
result = 31 * result + (layerBelow != null ? layerBelow.hashCode() : 0);
result = 31 * result + (bearingOnTop ? 1 : 0);
result = 31 * result + (trackingAnimationDurationMultiplier != +0.0f
? Float.floatToIntBits(trackingAnimationDurationMultiplier) : 0);
result = 31 * result + (compassAnimationEnabled ? 1 : 0);
@@ -1171,6 +1192,7 @@ public class LocationComponentOptions implements Parcelable {
dest.writeParcelable(this.trackingMultiFingerProtectedMoveArea, flags);
dest.writeString(this.layerAbove);
dest.writeString(this.layerBelow);
dest.writeByte(this.bearingOnTop ? (byte) 1 : (byte) 0);
dest.writeFloat(this.trackingAnimationDurationMultiplier);
dest.writeByte(this.compassAnimationEnabled ? (byte) 1 : (byte) 0);
dest.writeByte(this.accuracyAnimationEnabled ? (byte) 1 : (byte) 0);
@@ -1214,6 +1236,7 @@ public class LocationComponentOptions implements Parcelable {
this.trackingMultiFingerProtectedMoveArea = in.readParcelable(RectF.class.getClassLoader());
this.layerAbove = in.readString();
this.layerBelow = in.readString();
this.bearingOnTop = in.readByte() != 0;
this.trackingAnimationDurationMultiplier = in.readFloat();
this.compassAnimationEnabled = in.readByte() != 0;
this.accuracyAnimationEnabled = in.readByte() != 0;
@@ -1338,6 +1361,7 @@ public class LocationComponentOptions implements Parcelable {
private RectF trackingMultiFingerProtectedMoveArea;
private String layerAbove;
private String layerBelow;
private boolean bearingOnTop;
private Float trackingAnimationDurationMultiplier;
private Boolean compassAnimationEnabled;
private Boolean accuracyAnimationEnabled;
@@ -1385,6 +1409,7 @@ public class LocationComponentOptions implements Parcelable {
this.trackingMultiFingerProtectedMoveArea = source.trackingMultiFingerProtectedMoveArea();
this.layerAbove = source.layerAbove();
this.layerBelow = source.layerBelow();
this.bearingOnTop = source.bearingOnTop();
this.trackingAnimationDurationMultiplier = source.trackingAnimationDurationMultiplier();
this.compassAnimationEnabled = source.compassAnimationEnabled();
this.accuracyAnimationEnabled = source.accuracyAnimationEnabled();
@@ -1866,6 +1891,19 @@ public class LocationComponentOptions implements Parcelable {
return this;
}
/**
* Sets whether the bearing icon is rendered on top of the foreground icon in the location
* layer stack. When true, the bearing layer is placed above the foreground layer; when false,
* it is rendered below.
*
* @param bearingOnTop true to render the bearing icon above the foreground icon, false for below.
*/
@NonNull
public LocationComponentOptions.Builder bearingOnTop(boolean bearingOnTop) {
this.bearingOnTop = bearingOnTop;
return this;
}
/**
* Sets the tracking animation duration multiplier.
*
@@ -2065,6 +2103,7 @@ public class LocationComponentOptions implements Parcelable {
this.trackingMultiFingerProtectedMoveArea,
this.layerAbove,
this.layerBelow,
this.bearingOnTop,
this.trackingAnimationDurationMultiplier,
this.compassAnimationEnabled,
this.accuracyAnimationEnabled,

View File

@@ -17,22 +17,28 @@ class LocationComponentPositionManager {
@Nullable
private String layerBelow;
LocationComponentPositionManager(@NonNull Style style, @Nullable String layerAbove, @Nullable String layerBelow) {
public boolean bearingOnTop;
LocationComponentPositionManager(@NonNull Style style, @Nullable String layerAbove,
@Nullable String layerBelow, Boolean bearingOnTop) {
this.style = style;
this.layerAbove = layerAbove;
this.layerBelow = layerBelow;
this.bearingOnTop = bearingOnTop;
}
/**
* Returns true whenever layer above/below configuration has changed and requires re-layout.
*/
boolean update(@Nullable String layerAbove, @Nullable String layerBelow) {
boolean update(@Nullable String layerAbove, @Nullable String layerBelow, boolean bearingOnTop) {
boolean requiresUpdate =
!(this.layerAbove == layerAbove || (this.layerAbove != null && this.layerAbove.equals(layerAbove)))
|| !(this.layerBelow == layerBelow || (this.layerBelow != null && this.layerBelow.equals(layerBelow)));
|| !(this.layerBelow == layerBelow || (this.layerBelow != null && this.layerBelow.equals(layerBelow)))
|| (this.bearingOnTop != bearingOnTop);
this.layerAbove = layerAbove;
this.layerBelow = layerBelow;
this.bearingOnTop = bearingOnTop;
return requiresUpdate;
}

View File

@@ -74,7 +74,8 @@ final class LocationLayerController {
}
void initializeComponents(Style style, LocationComponentOptions options) {
this.positionManager = new LocationComponentPositionManager(style, options.layerAbove(), options.layerBelow());
this.positionManager = new LocationComponentPositionManager(style, options.layerAbove(),
options.layerBelow(), options.bearingOnTop());
locationLayerRenderer.initializeComponents(style);
locationLayerRenderer.addLayers(positionManager);
applyStyle(options);
@@ -87,7 +88,7 @@ final class LocationLayerController {
}
void applyStyle(@NonNull LocationComponentOptions options) {
if (positionManager.update(options.layerAbove(), options.layerBelow())) {
if (positionManager.update(options.layerAbove(), options.layerBelow(), options.bearingOnTop())) {
locationLayerRenderer.removeLayers();
locationLayerRenderer.addLayers(positionManager);
if (isHidden) {

View File

@@ -83,15 +83,29 @@ final class SymbolLocationLayerRenderer implements LocationLayerRenderer {
@Override
public void addLayers(LocationComponentPositionManager positionManager) {
// positions the top-most reference layer
Layer layer = layerSourceProvider.generateLayer(BEARING_LAYER);
positionManager.addLayerToMap(layer);
layerSet.add(layer.getId());
// adds remaining layers while keeping the order
addSymbolLayer(FOREGROUND_LAYER, BEARING_LAYER);
addSymbolLayer(BACKGROUND_LAYER, FOREGROUND_LAYER);
addSymbolLayer(SHADOW_LAYER, BACKGROUND_LAYER);
if (positionManager.bearingOnTop) {
// positions the top-most reference layer
Layer layer = layerSourceProvider.generateLayer(BEARING_LAYER);
positionManager.addLayerToMap(layer);
layerSet.add(layer.getId());
// adds remaining layers while keeping the order
addSymbolLayer(FOREGROUND_LAYER, BEARING_LAYER);
addSymbolLayer(BACKGROUND_LAYER, FOREGROUND_LAYER);
addSymbolLayer(SHADOW_LAYER, BACKGROUND_LAYER);
} else {
// positions the top-most reference layer
Layer layer = layerSourceProvider.generateLayer(FOREGROUND_LAYER);
positionManager.addLayerToMap(layer);
layerSet.add(layer.getId());
// adds remaining layers while keeping the order
addSymbolLayer(BACKGROUND_LAYER, FOREGROUND_LAYER);
addSymbolLayer(BEARING_LAYER, BACKGROUND_LAYER);
addSymbolLayer(SHADOW_LAYER, BEARING_LAYER);
}
addAccuracyLayer();
addPulsingCircleLayerToMap();
}

View File

@@ -181,6 +181,7 @@
<!-- Map layer configuration -->
<attr name="maplibre_layer_above" format="string"/>
<attr name="maplibre_layer_below" format="string"/>
<attr name="maplibre_bearing_on_top" format="boolean"/>
<!-- Icon scale based on map zoom levels -->
<attr name="maplibre_maxZoomIconScale" format="float"/>

View File

@@ -26,70 +26,91 @@ class LocationComponentPositionManagerTest : BaseTest() {
@Test
fun update_noChange_null() {
val positionManager = LocationComponentPositionManager(style, null, null)
val requiresUpdate = positionManager.update(null, null)
val positionManager = LocationComponentPositionManager(style, null, null, false)
val requiresUpdate = positionManager.update(null, null, false)
assertFalse(requiresUpdate)
}
@Test
fun update_noChange_above() {
val positionManager = LocationComponentPositionManager(style, "above", null)
val requiresUpdate = positionManager.update("above", null)
val positionManager = LocationComponentPositionManager(style, "above", null, false)
val requiresUpdate = positionManager.update("above", null, false)
assertFalse(requiresUpdate)
}
@Test
fun update_noChange_below() {
val positionManager = LocationComponentPositionManager(style, null, "below")
val requiresUpdate = positionManager.update(null, "below")
val positionManager = LocationComponentPositionManager(style, null, "below", false)
val requiresUpdate = positionManager.update(null, "below", false)
assertFalse(requiresUpdate)
}
@Test
fun update_noChange_bearingOnTop() {
val positionManager = LocationComponentPositionManager(style, null, null, true)
val requiresUpdate = positionManager.update(null, null, true)
assertFalse(requiresUpdate)
}
@Test
fun update_fromNull_above() {
val positionManager = LocationComponentPositionManager(style, null, null)
val requiresUpdate = positionManager.update("above", null)
val positionManager = LocationComponentPositionManager(style, null, null, false)
val requiresUpdate = positionManager.update("above", null, false)
assertTrue(requiresUpdate)
}
@Test
fun update_fromNull_below() {
val positionManager = LocationComponentPositionManager(style, null, null)
val requiresUpdate = positionManager.update(null, "below")
val positionManager = LocationComponentPositionManager(style, null, null, false)
val requiresUpdate = positionManager.update(null, "below", false)
assertTrue(requiresUpdate)
}
@Test
fun update_toNull_above() {
val positionManager = LocationComponentPositionManager(style, "above", null)
val requiresUpdate = positionManager.update(null, null)
val positionManager = LocationComponentPositionManager(style, "above", null, false)
val requiresUpdate = positionManager.update(null, null, false)
assertTrue(requiresUpdate)
}
@Test
fun update_toNull_below() {
val positionManager = LocationComponentPositionManager(style, null, "below")
val requiresUpdate = positionManager.update(null, null)
val positionManager = LocationComponentPositionManager(style, null, "below", false)
val requiresUpdate = positionManager.update(null, null, false)
assertTrue(requiresUpdate)
}
@Test
fun update_fromValue_above() {
val positionManager = LocationComponentPositionManager(style, "above1", null)
val requiresUpdate = positionManager.update("above2", null)
val positionManager = LocationComponentPositionManager(style, "above1", null, false)
val requiresUpdate = positionManager.update("above2", null, false)
assertTrue(requiresUpdate)
}
@Test
fun update_fromValue_below() {
val positionManager = LocationComponentPositionManager(style, null, "below1")
val requiresUpdate = positionManager.update(null, "below2")
val positionManager = LocationComponentPositionManager(style, null, "below1", false)
val requiresUpdate = positionManager.update(null, "below2", false)
assertTrue(requiresUpdate)
}
@Test
fun update_bearingOnTop_changed() {
val positionManager = LocationComponentPositionManager(style, null, null, false)
val requiresUpdate = positionManager.update(null, null, true)
assertTrue(requiresUpdate)
}
@Test
fun update_bearingOnTop_changedToFalse() {
val positionManager = LocationComponentPositionManager(style, null, null, true)
val requiresUpdate = positionManager.update(null, null, false)
assertTrue(requiresUpdate)
}
@Test
fun addLayer_noModifier() {
val positionManager = LocationComponentPositionManager(style, null, null)
val positionManager = LocationComponentPositionManager(style, null, null, false)
positionManager.addLayerToMap(layer)
verify { style.addLayer(layer) }
@@ -97,7 +118,7 @@ class LocationComponentPositionManagerTest : BaseTest() {
@Test
fun addLayer_above() {
val positionManager = LocationComponentPositionManager(style, "above", null)
val positionManager = LocationComponentPositionManager(style, "above", null, false)
positionManager.addLayerToMap(layer)
verify { style.addLayerAbove(layer, "above") }
@@ -105,7 +126,7 @@ class LocationComponentPositionManagerTest : BaseTest() {
@Test
fun addLayer_below() {
val positionManager = LocationComponentPositionManager(style, null, "below")
val positionManager = LocationComponentPositionManager(style, null, "below", false)
positionManager.addLayerToMap(layer)
verify { style.addLayerBelow(layer, "below") }
@@ -113,8 +134,8 @@ class LocationComponentPositionManagerTest : BaseTest() {
@Test
fun addLayer_afterUpdate_above() {
val positionManager = LocationComponentPositionManager(style, null, null)
positionManager.update("above", null)
val positionManager = LocationComponentPositionManager(style, null, null, false)
positionManager.update("above", null, false)
positionManager.addLayerToMap(layer)
verify { style.addLayerAbove(layer, "above") }
@@ -122,8 +143,8 @@ class LocationComponentPositionManagerTest : BaseTest() {
@Test
fun addLayer_afterUpdate_below() {
val positionManager = LocationComponentPositionManager(style, null, null)
positionManager.update(null, "below")
val positionManager = LocationComponentPositionManager(style, null, null, false)
positionManager.update(null, "below", false)
positionManager.addLayerToMap(layer)
verify { style.addLayerBelow(layer, "below") }

View File

@@ -102,7 +102,7 @@ class LocationLayerControllerTest : BaseTest() {
false
)
Mockito.verify(style)
.addLayerBelow(shadowLayer, LocationComponentConstants.BACKGROUND_LAYER)
.addLayerBelow(shadowLayer, LocationComponentConstants.BEARING_LAYER)
}
@Test
@@ -181,8 +181,7 @@ class LocationLayerControllerTest : BaseTest() {
internalRenderModeChangedListener,
false
)
Mockito.verify(style)
.addLayerBelow(foregroundLayer, LocationComponentConstants.BEARING_LAYER)
Mockito.verify(style).addLayer(foregroundLayer)
}
@Test
@@ -223,7 +222,8 @@ class LocationLayerControllerTest : BaseTest() {
internalRenderModeChangedListener,
false
)
Mockito.verify(style).addLayerBelow(bearingLayer, layerBelow)
Mockito.verify(style)
.addLayerBelow(bearingLayer, LocationComponentConstants.BACKGROUND_LAYER)
}
@Test
@@ -757,13 +757,13 @@ class LocationLayerControllerTest : BaseTest() {
Mockito.verify(style).removeLayer(LocationComponentConstants.BACKGROUND_LAYER)
Mockito.verify(style).removeLayer(LocationComponentConstants.SHADOW_LAYER)
Mockito.verify(style).removeLayer(LocationComponentConstants.ACCURACY_LAYER)
Mockito.verify(style).addLayerBelow(bearingLayer2, layerBelow)
Mockito.verify(style)
.addLayerBelow(foregroundLayer2, LocationComponentConstants.BEARING_LAYER)
Mockito.verify(style).addLayerBelow(foregroundLayer2, layerBelow)
Mockito.verify(style)
.addLayerBelow(backgroundLayer2, LocationComponentConstants.FOREGROUND_LAYER)
Mockito.verify(style)
.addLayerBelow(shadowLayer2, LocationComponentConstants.BACKGROUND_LAYER)
.addLayerBelow(bearingLayer2, LocationComponentConstants.BACKGROUND_LAYER)
Mockito.verify(style)
.addLayerBelow(shadowLayer2, LocationComponentConstants.BEARING_LAYER)
Mockito.verify(style)
.addLayerBelow(accuracyLayer2, LocationComponentConstants.BACKGROUND_LAYER)
}