summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMikhail Pozdnyakov <mikhail.pozdnyakov@mapbox.com>2019-01-18 13:12:04 +0200
committerlangsmith <langstonlmcs@gmail.com>2019-09-30 18:54:18 -0700
commit96a98bdf3ab79fcd3d5bd3f21232d3a45613570c (patch)
tree16e72b17ed7f58cc5169d882fc938210b201d4c8
parentb0f3875be269c8523381e72c3abc79ce3a598cf6 (diff)
downloadqtlocation-mapboxgl-96a98bdf3ab79fcd3d5bd3f21232d3a45613570c.tar.gz
[android] initial additions to add a pulsing locationComponent circle
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerSourceProvider.java15
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinator.java48
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponent.java27
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentConstants.java7
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentOptions.java320
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationLayerController.java86
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimator.java13
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimatorProvider.java30
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimatorSetProvider.java11
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/PulsingLocationCircleAnimator.java42
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/modes/PulseMode.java50
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/res-public/values/public.xml10
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/res/values/attrs.xml9
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/res/values/styles.xml8
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinatorTest.kt20
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationLayerControllerTest.java28
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/location/LocationLayerControllerTest.kt205
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/location/utils/StyleChangeIdlingResource.kt2
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml22
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/location/BasicLocationPulsingCircleActivity.java205
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/location/CustomizedLocationPulsingCircleActivity.java359
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_location_layer_basic_pulsing_circle.xml13
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_location_layer_customized_pulsing_circle.xml78
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_pulsing_location_mode.xml21
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/descriptions.xml2
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/titles.xml2
26 files changed, 1618 insertions, 15 deletions
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerSourceProvider.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerSourceProvider.java
index cac513c2f9..1ff3583b5c 100644
--- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerSourceProvider.java
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerSourceProvider.java
@@ -27,6 +27,7 @@ import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_
import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_GPS_BEARING;
import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_LOCATION_STALE;
import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_SHADOW_ICON_OFFSET;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PULSING_CIRCLE_LAYER;
import static com.mapbox.mapboxsdk.location.LocationComponentConstants.SHADOW_ICON;
import static com.mapbox.mapboxsdk.location.LocationComponentConstants.SHADOW_LAYER;
import static com.mapbox.mapboxsdk.style.expressions.Expression.get;
@@ -108,4 +109,18 @@ class LayerSourceProvider {
circlePitchAlignment(Property.CIRCLE_PITCH_ALIGNMENT_MAP)
);
}
+
+ /**
+ * Adds a {@link CircleLayer} to the map to support the {@link LocationComponent} pulsing UI functionality.
+ *
+ * @return a {@link CircleLayer} with the correct data-driven styling. Tilting the map will keep the pulsing
+ * layer aligned with the map plane.
+ */
+ @NonNull
+ Layer generatePulsingCircleLayer() {
+ return new CircleLayer(PULSING_CIRCLE_LAYER, LOCATION_SOURCE)
+ .withProperties(
+ circlePitchAlignment(Property.CIRCLE_PITCH_ALIGNMENT_MAP)
+ );
+ }
}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinator.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinator.java
index 5aad038a28..a522fbaaa1 100644
--- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinator.java
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinator.java
@@ -30,6 +30,7 @@ import static com.mapbox.mapboxsdk.location.MapboxAnimator.ANIMATOR_LAYER_ACCURA
import static com.mapbox.mapboxsdk.location.MapboxAnimator.ANIMATOR_LAYER_COMPASS_BEARING;
import static com.mapbox.mapboxsdk.location.MapboxAnimator.ANIMATOR_LAYER_GPS_BEARING;
import static com.mapbox.mapboxsdk.location.MapboxAnimator.ANIMATOR_LAYER_LATLNG;
+import static com.mapbox.mapboxsdk.location.MapboxAnimator.ANIMATOR_PULSING_CIRCLE;
import static com.mapbox.mapboxsdk.location.MapboxAnimator.ANIMATOR_TILT;
import static com.mapbox.mapboxsdk.location.MapboxAnimator.ANIMATOR_ZOOM;
import static com.mapbox.mapboxsdk.location.Utils.immediateAnimation;
@@ -51,6 +52,7 @@ final class LocationAnimatorCoordinator {
private final MapboxAnimatorSetProvider animatorSetProvider;
private boolean compassAnimationEnabled;
private boolean accuracyAnimationEnabled;
+ private LocationComponentOptions locationComponentOptions;
@VisibleForTesting
int maxAnimationFps = Integer.MAX_VALUE;
@@ -59,10 +61,12 @@ final class LocationAnimatorCoordinator {
final SparseArray<MapboxAnimator.AnimationsValueChangeListener> listeners = new SparseArray<>();
LocationAnimatorCoordinator(@NonNull Projection projection, @NonNull MapboxAnimatorSetProvider animatorSetProvider,
- @NonNull MapboxAnimatorProvider animatorProvider) {
+ @NonNull MapboxAnimatorProvider animatorProvider,
+ @NonNull LocationComponentOptions locationComponentOptions) {
this.projection = projection;
this.animatorProvider = animatorProvider;
this.animatorSetProvider = animatorSetProvider;
+ this.locationComponentOptions = locationComponentOptions;
}
void updateAnimatorListenerHolders(@NonNull Set<AnimatorListenerHolder> listenerHolders) {
@@ -150,6 +154,29 @@ final class LocationAnimatorCoordinator {
this.previousAccuracyRadius = targetAccuracyRadius;
}
+ /**
+ * Initializes the {@link PulsingLocationCircleAnimator}, which is a type of {@link MapboxAnimator}.
+ * This method also adds the animator to this class' animator array.
+ *
+ * @param options the {@link LocationComponentOptions} passed to this class upstream from the
+ * {@link LocationComponent}.
+ */
+ void startLocationComponentCirclePulsing(LocationComponentOptions options) {
+ cancelAnimator(ANIMATOR_PULSING_CIRCLE);
+ MapboxAnimator.AnimationsValueChangeListener listener = listeners.get(ANIMATOR_PULSING_CIRCLE);
+ locationComponentOptions = options;
+ PulsingLocationCircleAnimator pulsingLocationCircleAnimator = animatorProvider.pulsingCircleAnimator(
+ listener,
+ maxAnimationFps,
+ locationComponentOptions.pulseSingleDuration(),
+ locationComponentOptions.pulseMaxRadius(),
+ locationComponentOptions.pulseInterpolator());
+ if (listener != null) {
+ animatorArray.put(ANIMATOR_PULSING_CIRCLE, pulsingLocationCircleAnimator);
+ }
+ playPulsingAnimator();
+ }
+
void feedNewZoomLevel(double targetZoomLevel, @NonNull CameraPosition currentCameraPosition, long animationDuration,
@Nullable MapboxMap.CancelableCallback callback) {
updateZoomAnimator((float) targetZoomLevel, (float) currentCameraPosition.zoom, callback);
@@ -292,6 +319,18 @@ final class LocationAnimatorCoordinator {
animatorSetProvider.startAnimation(animators, new LinearInterpolator(), duration);
}
+ /**
+ * Starts the {@link PulsingLocationCircleAnimator} in the animator array. This method is separate
+ * from {@link #playAnimators(long, int...)} because the MapboxAnimatorSetProvider has many more
+ * customizable animation parameters than the other {@link MapboxAnimator}s.
+ */
+ private void playPulsingAnimator() {
+ Animator animator = animatorArray.get(ANIMATOR_PULSING_CIRCLE);
+ if (animator != null) {
+ animatorSetProvider.startSingleAnimation(animator);
+ }
+ }
+
void resetAllCameraAnimations(@NonNull CameraPosition currentCameraPosition, boolean isGpsNorth) {
resetCameraCompassAnimation(currentCameraPosition);
boolean snap = resetCameraLocationAnimations(currentCameraPosition, isGpsNorth);
@@ -383,6 +422,13 @@ final class LocationAnimatorCoordinator {
cancelAnimator(ANIMATOR_TILT);
}
+ /**
+ * Cancel's the pulsing circle location animator.
+ */
+ void cancelPulsingCircleAnimation() {
+ cancelAnimator(ANIMATOR_PULSING_CIRCLE);
+ }
+
void cancelAllAnimations() {
for (int i = 0; i < animatorArray.size(); i++) {
@MapboxAnimator.Type int animatorType = animatorArray.keyAt(i);
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponent.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponent.java
index 8b014b0e9c..5bd2268e43 100644
--- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponent.java
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponent.java
@@ -710,17 +710,26 @@ public final class LocationComponent {
LocationComponent.this.options = options;
if (mapboxMap.getStyle() != null) {
locationLayerController.applyStyle(options);
- locationCameraController.initializeOptions(options);
staleStateManager.setEnabled(options.enableStaleState());
staleStateManager.setDelayTime(options.staleStateTimeout());
locationAnimatorCoordinator.setTrackingAnimationDurationMultiplier(options.trackingAnimationDurationMultiplier());
locationAnimatorCoordinator.setCompassAnimationEnabled(options.compassAnimationEnabled());
locationAnimatorCoordinator.setAccuracyAnimationEnabled(options.accuracyAnimationEnabled());
+ if (options.pulseEnabled()) {
+ startPulsingLocationCircle();
+ }
updateMapWithOptions(options);
}
}
/**
+ * Starts the LocationComponent's pulsing circle UI.
+ */
+ public void startPulsingLocationCircle() {
+ locationAnimatorCoordinator.startLocationComponentCirclePulsing(options);
+ }
+
+ /**
* Zooms to the desired zoom level.
* This API can only be used in pair with camera modes other than {@link CameraMode#NONE}.
* If you are not using any of {@link CameraMode} modes,
@@ -1177,6 +1186,14 @@ public final class LocationComponent {
}
}
+ /**
+ * Available to cancel the specific pulsing circle animation.
+ */
+ public void cancelPulsingLocationCircle() {
+ locationAnimatorCoordinator.cancelPulsingCircleAnimation();
+ locationLayerController.adjustPulsingCircleLayerVisibility(false);
+ }
+
@SuppressLint("MissingPermission")
private void onLocationLayerStart() {
if (!isComponentInitialized || !isComponentStarted || mapboxMap.getStyle() == null) {
@@ -1202,6 +1219,9 @@ public final class LocationComponent {
}
}
setCameraMode(locationCameraController.getCameraMode());
+ if (options.pulseEnabled()) {
+ startPulsingLocationCircle();
+ }
setLastLocation();
updateCompassListenerState(true);
setLastCompassHeading();
@@ -1254,7 +1274,7 @@ public final class LocationComponent {
locationAnimatorCoordinator = new LocationAnimatorCoordinator(
mapboxMap.getProjection(),
MapboxAnimatorSetProvider.getInstance(),
- MapboxAnimatorProvider.getInstance()
+ MapboxAnimatorProvider.getInstance(), options
);
locationAnimatorCoordinator.setTrackingAnimationDurationMultiplier(options
.trackingAnimationDurationMultiplier());
@@ -1369,6 +1389,9 @@ public final class LocationComponent {
boolean isLocationLayerHidden = locationLayerController.isHidden();
if (isEnabled && isComponentStarted && isLocationLayerHidden) {
locationLayerController.show();
+ if (options.pulseEnabled()) {
+ locationLayerController.adjustPulsingCircleLayerVisibility(true);
+ }
}
}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentConstants.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentConstants.java
index f2158584c7..baba5eaca4 100644
--- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentConstants.java
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentConstants.java
@@ -52,6 +52,8 @@ public final class LocationComponentConstants {
static final String PROPERTY_FOREGROUND_STALE_ICON = "mapbox-property-foreground-stale-icon";
static final String PROPERTY_BACKGROUND_STALE_ICON = "mapbox-property-background-stale-icon";
static final String PROPERTY_BEARING_ICON = "mapbox-property-shadow-icon";
+ static final String PROPERTY_PULSING_RADIUS = "mapbox-property-pulsing-circle-radius";
+ static final String PROPERTY_PULSING_OPACITY = "mapbox-property-pulsing-circle-opacity";
// Layers
@@ -80,6 +82,11 @@ public final class LocationComponentConstants {
*/
public static final String BEARING_LAYER = "mapbox-location-bearing-layer";
+ /**
+ * Layer ID of the location pulsing circle.
+ */
+ public static final String PULSING_CIRCLE_LAYER = "mapbox-location-pulsing-circle-layer";
+
// Icons
static final String FOREGROUND_ICON = "mapbox-location-icon";
static final String BACKGROUND_ICON = "mapbox-location-stroke-icon";
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentOptions.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentOptions.java
index 48c89622cd..dfadd9f0ae 100644
--- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentOptions.java
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentOptions.java
@@ -14,6 +14,7 @@ import android.support.annotation.StyleRes;
import com.mapbox.android.gestures.AndroidGesturesManager;
import com.mapbox.mapboxsdk.R;
+import com.mapbox.mapboxsdk.location.modes.PulseMode;
import com.mapbox.mapboxsdk.style.layers.Layer;
import java.util.Arrays;
@@ -69,6 +70,21 @@ public class LocationComponentOptions implements Parcelable {
*/
private static final float TRACKING_ANIMATION_DURATION_MULTIPLIER_DEFAULT = 1.1f;
+ /**
+ * Default duration of a single LocationComponent circle pulse.
+ */
+ private static final long CIRCLE_PULSING_DURATION_DEFAULT_MS = 2300;
+
+ /**
+ * Default opacity of the LocationComponent circle when it ends a single pulse.
+ */
+ private static final float CIRCLE_PULSING_ALPHA_DEFAULT = 1f;
+
+ /**
+ * Default maximum radius of the LocationComponent circle when it's pulsing.
+ */
+ public static final float CIRCLE_PULSING_MAX_RADIUS_DEFAULT = 35f;
+
private float accuracyAlpha;
private int accuracyColor;
private int backgroundDrawableStale;
@@ -114,6 +130,13 @@ public class LocationComponentOptions implements Parcelable {
private float trackingAnimationDurationMultiplier;
private boolean compassAnimationEnabled;
private boolean accuracyAnimationEnabled;
+ private Boolean pulsingCircleEnabled;
+ private Boolean pulsingCircleFadeEnabled;
+ private Integer pulseColor;
+ private float pulseSingleDuration;
+ private float pulsingCircleMaxRadius;
+ private float pulseAlpha;
+ private String pulseInterpolator;
public LocationComponentOptions(
float accuracyAlpha,
@@ -148,7 +171,14 @@ public class LocationComponentOptions implements Parcelable {
String layerBelow,
float trackingAnimationDurationMultiplier,
boolean compassAnimationEnabled,
- boolean accuracyAnimationEnabled) {
+ boolean accuracyAnimationEnabled,
+ Boolean pulsingCircleEnabled,
+ Boolean pulsingCircleFadeEnabled,
+ Integer pulsingCircleColor,
+ float pulsingCircleDuration,
+ float pulsingCircleMaxRadius,
+ float pulsingCircleAlpha,
+ String pulsingCircleInterpolator) {
this.accuracyAlpha = accuracyAlpha;
this.accuracyColor = accuracyColor;
this.backgroundDrawableStale = backgroundDrawableStale;
@@ -185,6 +215,13 @@ public class LocationComponentOptions implements Parcelable {
this.trackingAnimationDurationMultiplier = trackingAnimationDurationMultiplier;
this.compassAnimationEnabled = compassAnimationEnabled;
this.accuracyAnimationEnabled = accuracyAnimationEnabled;
+ this.pulsingCircleEnabled = pulsingCircleEnabled;
+ this.pulsingCircleFadeEnabled = pulsingCircleFadeEnabled;
+ this.pulseColor = pulsingCircleColor;
+ this.pulseSingleDuration = pulsingCircleDuration;
+ this.pulsingCircleMaxRadius = pulsingCircleMaxRadius;
+ this.pulseAlpha = pulsingCircleAlpha;
+ this.pulseInterpolator = pulsingCircleInterpolator;
}
/**
@@ -302,6 +339,35 @@ public class LocationComponentOptions implements Parcelable {
R.styleable.mapbox_LocationComponent_mapbox_accuracyAnimationEnabled, true
);
+ builder.pulsingCircleEnabled = typedArray.getBoolean(
+ R.styleable.mapbox_LocationComponent_mapbox_pulsingLocationCircleEnabled, false
+ );
+
+ builder.pulsingCircleFadeEnabled = typedArray.getBoolean(
+ R.styleable.mapbox_LocationComponent_mapbox_pulsingLocationCircleFadeEnabled, true
+ );
+
+ if (typedArray.hasValue(R.styleable.mapbox_LocationComponent_mapbox_pulsingLocationCircleColor)) {
+ builder.pulsingCircleColor(typedArray.getColor(
+ R.styleable.mapbox_LocationComponent_mapbox_pulsingLocationCircleColor,
+ -1));
+ }
+
+ builder.pulsingCircleDuration = typedArray.getFloat(
+ R.styleable.mapbox_LocationComponent_mapbox_pulsingLocationCircleDuration, CIRCLE_PULSING_DURATION_DEFAULT_MS
+ );
+
+ builder.pulsingCircleMaxRadius = typedArray.getFloat(
+ R.styleable.mapbox_LocationComponent_mapbox_pulsingLocationCircleRadius, CIRCLE_PULSING_MAX_RADIUS_DEFAULT
+ );
+
+ builder.pulsingCircleAlpha = typedArray.getFloat(
+ R.styleable.mapbox_LocationComponent_mapbox_pulsingLocationCircleAlpha, CIRCLE_PULSING_ALPHA_DEFAULT
+ );
+
+ builder.pulsingCircleInterpolator = typedArray.getString(
+ R.styleable.mapbox_LocationComponent_mapbox_pulsingLocationCircleInterpolator);
+
typedArray.recycle();
return builder.build();
@@ -740,6 +806,70 @@ public class LocationComponentOptions implements Parcelable {
return accuracyAnimationEnabled;
}
+ /**
+ * Enable or disable the LocationComponent's pulsing circle.
+ *
+ * @return whether the LocationComponent's pulsing circle is enabled
+ */
+ public Boolean pulseEnabled() {
+ return pulsingCircleEnabled;
+ }
+
+ /**
+ * Enable or disable fading of the LocationComponent's pulsing circle. If it fades, the circle's
+ * opacity decreases as its radius increases.
+ *
+ * @return whether fading of the LocationComponent's pulsing circle is enabled
+ */
+ public Boolean pulsingCircleFadeEnabled() {
+ return pulsingCircleFadeEnabled;
+ }
+
+ /**
+ * Color of the LocationComponent's pulsing circle as it pulses.
+ *
+ * @return the current set color of the circle
+ */
+ public Integer pulseColor() {
+ return pulseColor;
+ }
+
+ /**
+ * The number of milliseconds it takes for a single pulse of the LocationComponent's pulsing circle.
+ *
+ * @return the current set length of time for a single pulse
+ */
+ public float pulseSingleDuration() {
+ return pulseSingleDuration;
+ }
+
+ /**
+ * The maximum radius that a single pulse should expand the LocationComponent's pulsing circle to.
+ *
+ * @return the maximum radius that the pulsing circle will expand to.
+ */
+ public float pulseMaxRadius() {
+ return pulsingCircleMaxRadius;
+ }
+
+ /**
+ * The opacity of the LocationComponent's circle as it pulses.
+ *
+ * @return the current set opacity of the LocationComponent's circle
+ */
+ public float pulseAlpha() {
+ return pulseAlpha;
+ }
+
+ /**
+ * The interpolator type of animation for the movement of the LocationComponent's circle
+ *
+ * @return the current set type of animation interpolator for the pulsing circle
+ */
+ public String pulseInterpolator() {
+ return pulseInterpolator;
+ }
+
@NonNull
@Override
public String toString() {
@@ -775,6 +905,13 @@ public class LocationComponentOptions implements Parcelable {
+ "layerAbove=" + layerAbove
+ "layerBelow=" + layerBelow
+ "trackingAnimationDurationMultiplier=" + trackingAnimationDurationMultiplier
+ + "pulsingCircleEnabled=" + pulsingCircleEnabled
+ + "pulsingCircleFadeEnabled=" + pulsingCircleFadeEnabled
+ + "pulseColor=" + pulseColor
+ + "pulseSingleDuration=" + pulseSingleDuration
+ + "pulsingCircleMaxRadius=" + pulsingCircleMaxRadius
+ + "pulseAlpha=" + pulseAlpha
+ + "pulseInterpolator=" + pulseInterpolator
+ "}";
}
@@ -892,6 +1029,36 @@ public class LocationComponentOptions implements Parcelable {
if (layerAbove != null ? !layerAbove.equals(options.layerAbove) : options.layerAbove != null) {
return false;
}
+
+ if (pulsingCircleEnabled != options.pulsingCircleEnabled) {
+ return false;
+ }
+
+ if (pulsingCircleFadeEnabled != options.pulsingCircleFadeEnabled) {
+ return false;
+ }
+
+ if (pulseColor != null ? !pulseColor.equals(options.pulseColor) :
+ options.pulseColor() != null) {
+ return false;
+ }
+ if (Float.compare(options.pulseSingleDuration, pulseSingleDuration) != 0) {
+ return false;
+ }
+
+ if (Float.compare(options.pulsingCircleMaxRadius, pulsingCircleMaxRadius) != 0) {
+ return false;
+ }
+
+ if (Float.compare(options.pulseAlpha, pulseAlpha) != 0) {
+ return false;
+ }
+
+ if (pulseInterpolator != null ? !pulseInterpolator.equals(options.pulseInterpolator)
+ : options.pulseInterpolator != null) {
+ return false;
+ }
+
return layerBelow != null ? layerBelow.equals(options.layerBelow) : options.layerBelow == null;
}
@@ -933,6 +1100,13 @@ public class LocationComponentOptions implements Parcelable {
? Float.floatToIntBits(trackingAnimationDurationMultiplier) : 0);
result = 31 * result + (compassAnimationEnabled ? 1 : 0);
result = 31 * result + (accuracyAnimationEnabled ? 1 : 0);
+ result = 31 * result + (pulsingCircleEnabled ? 1 : 0);
+ result = 31 * result + (pulsingCircleFadeEnabled ? 1 : 0);
+ result = 31 * result + (pulseColor != null ? pulseColor.hashCode() : 0);
+ result = 31 * result + (pulseSingleDuration != +0.0f ? Float.floatToIntBits(pulseSingleDuration) : 0);
+ result = 31 * result + (pulsingCircleMaxRadius != +0.0f ? Float.floatToIntBits(pulsingCircleMaxRadius) : 0);
+ result = 31 * result + (pulseAlpha != +0.0f ? Float.floatToIntBits(pulseAlpha) : 0);
+ result = 31 * result + (pulseInterpolator != null ? pulseInterpolator.hashCode() : 0);
return result;
}
@@ -973,7 +1147,14 @@ public class LocationComponentOptions implements Parcelable {
in.readString(),
in.readFloat(),
in.readInt() == 1,
- in.readInt() == 1
+ in.readInt() == 1,
+ in.readInt() == 1,
+ in.readInt() == 1,
+ in.readInt() == 0 ? in.readInt() : null,
+ in.readFloat(),
+ in.readFloat(),
+ in.readFloat(),
+ in.readString()
);
}
@@ -1073,6 +1254,18 @@ public class LocationComponentOptions implements Parcelable {
dest.writeFloat(trackingAnimationDurationMultiplier);
dest.writeInt(compassAnimationEnabled() ? 1 : 0);
dest.writeInt(accuracyAnimationEnabled() ? 1 : 0);
+ dest.writeInt(pulseEnabled() ? 1 : 0);
+ dest.writeInt(pulsingCircleFadeEnabled() ? 1 : 0);
+ if (pulseColor() == null) {
+ dest.writeInt(1);
+ } else {
+ dest.writeInt(0);
+ dest.writeInt(pulseColor());
+ }
+ dest.writeFloat(pulseSingleDuration());
+ dest.writeFloat(pulseMaxRadius());
+ dest.writeFloat(pulseAlpha());
+ dest.writeString(pulseInterpolator());
}
@Override
@@ -1108,6 +1301,32 @@ public class LocationComponentOptions implements Parcelable {
+ "Choose one or the other.");
}
+ if (locationComponentOptions.pulseEnabled() == null) {
+ String pulsingSetupError = "";
+ if (locationComponentOptions.pulsingCircleFadeEnabled() != null) {
+ pulsingSetupError += " pulsingCircleFadeEnabled";
+ }
+ if (locationComponentOptions.pulseColor() != null) {
+ pulsingSetupError += " pulsingCircleColor";
+ }
+ if (locationComponentOptions.pulseSingleDuration() > 0) {
+ pulsingSetupError += " pulsingCircleDuration";
+ }
+ if (locationComponentOptions.pulseMaxRadius() > 0) {
+ pulsingSetupError += " pulsingCircleMaxRadius";
+ }
+ if (locationComponentOptions.pulseAlpha() >= 0 && locationComponentOptions.pulseAlpha() <= 1) {
+ pulsingSetupError += " pulsingCircleAlpha";
+ }
+ if (locationComponentOptions.pulseInterpolator() != null) {
+ pulsingSetupError += " pulsingCircleInterpolator";
+ }
+ if (!pulsingSetupError.isEmpty()) {
+ throw new IllegalStateException("You've set up the following pulsing circle options but have not enabled"
+ + " the pulsing circle via the LocationComponentOptions builder:" + pulsingSetupError
+ + ". Enable the pulsing circle if you're going to set pulsing options.");
+ }
+ }
return locationComponentOptions;
}
@@ -1156,6 +1375,13 @@ public class LocationComponentOptions implements Parcelable {
private Float trackingAnimationDurationMultiplier;
private Boolean compassAnimationEnabled;
private Boolean accuracyAnimationEnabled;
+ private Boolean pulsingCircleEnabled;
+ private Boolean pulsingCircleFadeEnabled;
+ private int pulsingCircleColor;
+ private float pulsingCircleDuration;
+ private float pulsingCircleMaxRadius;
+ private float pulsingCircleAlpha;
+ private String pulsingCircleInterpolator;
Builder() {
}
@@ -1194,6 +1420,13 @@ public class LocationComponentOptions implements Parcelable {
this.trackingAnimationDurationMultiplier = source.trackingAnimationDurationMultiplier();
this.compassAnimationEnabled = source.compassAnimationEnabled();
this.accuracyAnimationEnabled = source.accuracyAnimationEnabled();
+ this.pulsingCircleEnabled = source.pulsingCircleEnabled;
+ this.pulsingCircleFadeEnabled = source.pulsingCircleFadeEnabled;
+ this.pulsingCircleColor = source.pulseColor;
+ this.pulsingCircleDuration = source.pulseSingleDuration;
+ this.pulsingCircleMaxRadius = source.pulsingCircleMaxRadius;
+ this.pulsingCircleAlpha = source.pulseAlpha;
+ this.pulsingCircleInterpolator = source.pulseInterpolator;
}
/**
@@ -1674,11 +1907,83 @@ public class LocationComponentOptions implements Parcelable {
*
* @return whether smooth animation of the accuracy circle is enabled
*/
- public Builder accuracyAnimationEnabled(Boolean accuracyAnimationEnabled) {
+ public LocationComponentOptions.Builder accuracyAnimationEnabled(Boolean accuracyAnimationEnabled) {
this.accuracyAnimationEnabled = accuracyAnimationEnabled;
return this;
}
+ /**
+ * Enable or disable the LocationComponent's pulsing circle.
+ *
+ * @return whether the LocationComponent's pulsing circle is enabled
+ */
+ public LocationComponentOptions.Builder pulsingCircleEnabled(Boolean pulsingCircleEnabled) {
+ this.pulsingCircleEnabled = pulsingCircleEnabled;
+ return this;
+ }
+
+ /**
+ * Enable or disable fading of the LocationComponent's pulsing circle. If it fades, the circle's
+ * opacity decreases as its radius increases.
+ *
+ * @return whether fading of the LocationComponent's pulsing circle is enabled
+ */
+ public LocationComponentOptions.Builder pulsingCircleFadeEnabled(Boolean pulsingCircleFadeEnabled) {
+ this.pulsingCircleFadeEnabled = pulsingCircleFadeEnabled;
+ return this;
+ }
+
+ /**
+ * Sets the color of the LocationComponent's pulsing circle.
+ *
+ * @return the current set color of the circle
+ */
+ public LocationComponentOptions.Builder pulsingCircleColor(int pulsingCircleColor) {
+ this.pulsingCircleColor = pulsingCircleColor;
+ return this;
+ }
+
+ /**
+ * Sets the number of milliseconds it takes for a single pulse of the LocationComponent's pulsing circle.
+ *
+ * @return the current set length of time for a single pulse
+ */
+ public LocationComponentOptions.Builder pulsingCircleDuration(float pulsingCircleDuration) {
+ this.pulsingCircleDuration = pulsingCircleDuration;
+ return this;
+ }
+
+ /**
+ * The maximum radius that a single pulse should expand the LocationComponent's pulsing circle to.
+ *
+ * @return the maximum radius that the pulsing circle will expand to.
+ */
+ public LocationComponentOptions.Builder pulsingCircleMaxRadius(float pulsingCircleMaxRadius) {
+ this.pulsingCircleMaxRadius = pulsingCircleMaxRadius;
+ return this;
+ }
+
+ /**
+ * Sets the opacity of the LocationComponent's pulsing circle.
+ *
+ * @return the current set opacity of the LocationComponent's circle
+ */
+ public LocationComponentOptions.Builder pulsingCircleAlpha(float pulsingCircleAlpha) {
+ this.pulsingCircleAlpha = pulsingCircleAlpha;
+ return this;
+ }
+
+ /**
+ * Sets the pulsing circle's interpolator animation. Pass through a mode constant via the
+ * {@link PulseMode} class.
+ *
+ * @return a String which represents the interpolator animation that the pulsing circle will use.
+ */
+ public LocationComponentOptions.Builder pulsingCircleInterpolator(String pulsingCircleInterpolator) {
+ this.pulsingCircleInterpolator = pulsingCircleInterpolator;
+ return this;
+ }
+
@Nullable
LocationComponentOptions autoBuild() {
String missing = "";
@@ -1772,7 +2077,14 @@ public class LocationComponentOptions implements Parcelable {
this.layerBelow,
this.trackingAnimationDurationMultiplier,
this.compassAnimationEnabled,
- this.accuracyAnimationEnabled);
+ this.accuracyAnimationEnabled,
+ this.pulsingCircleEnabled,
+ this.pulsingCircleFadeEnabled,
+ this.pulsingCircleColor,
+ this.pulsingCircleDuration,
+ this.pulsingCircleMaxRadius,
+ this.pulsingCircleAlpha,
+ this.pulsingCircleInterpolator);
}
}
}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationLayerController.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationLayerController.java
index 54f8ee6d1a..53a6b74e4f 100644
--- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationLayerController.java
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationLayerController.java
@@ -45,15 +45,23 @@ import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_
import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_FOREGROUND_STALE_ICON;
import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_GPS_BEARING;
import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_LOCATION_STALE;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PULSING_CIRCLE_LAYER;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_PULSING_OPACITY;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_PULSING_RADIUS;
import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_SHADOW_ICON_OFFSET;
import static com.mapbox.mapboxsdk.location.LocationComponentConstants.SHADOW_ICON;
import static com.mapbox.mapboxsdk.location.LocationComponentConstants.SHADOW_LAYER;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.get;
import static com.mapbox.mapboxsdk.style.expressions.Expression.interpolate;
import static com.mapbox.mapboxsdk.style.expressions.Expression.linear;
import static com.mapbox.mapboxsdk.style.expressions.Expression.stop;
import static com.mapbox.mapboxsdk.style.expressions.Expression.zoom;
import static com.mapbox.mapboxsdk.style.layers.Property.NONE;
import static com.mapbox.mapboxsdk.style.layers.Property.VISIBLE;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.circleColor;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.circleOpacity;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.circleRadius;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.circleStrokeColor;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconSize;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.visibility;
import static com.mapbox.mapboxsdk.utils.ColorUtils.colorToRgbaString;
@@ -127,6 +135,7 @@ final class LocationLayerController {
styleBearing(options);
styleAccuracy(options.accuracyAlpha(), options.accuracyColor());
styleScaling(options);
+ stylePulsingCircle(options);
determineIconsSource(options);
if (!isHidden) {
@@ -186,6 +195,13 @@ final class LocationLayerController {
}
}
+ /**
+ * Adjust the visibility of the pulsing LocationComponent circle.
+ */
+ void adjustPulsingCircleLayerVisibility(boolean visible) {
+ setLayerVisibility(PULSING_CIRCLE_LAYER, visible);
+ }
+
void hide() {
isHidden = true;
for (String layerId : layerSet) {
@@ -242,6 +258,7 @@ final class LocationLayerController {
addSymbolLayer(BACKGROUND_LAYER, FOREGROUND_LAYER);
addSymbolLayer(SHADOW_LAYER, BACKGROUND_LAYER);
addAccuracyLayer();
+ addPulsingCircleLayerToMap();
}
private void addSymbolLayer(@NonNull String layerId, @NonNull String beforeLayerId) {
@@ -259,6 +276,14 @@ final class LocationLayerController {
layerSet.add(layer.getId());
}
+ /**
+ * Add the pulsing LocationComponent circle to the map for future use, if need be.updatePulsingLocationCircleRadius
+ */
+ private void addPulsingCircleLayerToMap() {
+ Layer pulsingCircleLayer = layerSourceProvider.generatePulsingCircleLayer();
+ addLayerToMap(pulsingCircleLayer, ACCURACY_LAYER);
+ }
+
private void removeLayers() {
for (String layerId : layerSet) {
style.removeLayer(layerId);
@@ -276,6 +301,30 @@ final class LocationLayerController {
refreshSource();
}
+ /**
+ * Updates the {@link LocationComponentConstants#PROPERTY_PULSING_RADIUS} property value and refreshes
+ * the LocationComponent source. This leads to a smooth update to the visuals of the pulsing
+ * LocationComponent circle.
+ *
+ * @param radius The new radius in the animation.
+ */
+ private void updatePulsingLocationCircleRadius(float radius) {
+ locationFeature.addNumberProperty(PROPERTY_PULSING_RADIUS, radius);
+ refreshSource();
+ }
+
+ /**
+ * Updates the {@link LocationComponentConstants#PROPERTY_PULSING_OPACITY} property value and refreshes
+ * the LocationComponent source. This leads to a smooth update to the visuals of the pulsing
+ * LocationComponent circle. This is used if the fade option is set to true while setting pulsing options.
+ *
+ * @param opacity The new opacity in the animation.
+ */
+ private void updatePulsingLocationCircleOpacity(float opacity) {
+ locationFeature.addNumberProperty(PROPERTY_PULSING_OPACITY, opacity);
+ refreshSource();
+ }
+
//
// Source actions
//
@@ -365,6 +414,24 @@ final class LocationLayerController {
}
}
+ /**
+ * Use the Maps SDK's data-driven styling properties to set the pulsing circle location UI.
+ *
+ * @param options The {@link LocationComponentOptions} set upstream during LocationComponent
+ * initialization.
+ */
+ private void stylePulsingCircle(LocationComponentOptions options) {
+ if (style.getLayer(PULSING_CIRCLE_LAYER) != null) {
+ setLayerVisibility(PULSING_CIRCLE_LAYER, true);
+ style.getLayer(PULSING_CIRCLE_LAYER).setProperties(
+ circleRadius(get(PROPERTY_PULSING_RADIUS)),
+ circleColor(options.pulseColor()),
+ circleStrokeColor(options.pulseColor()),
+ circleOpacity(get(PROPERTY_PULSING_OPACITY))
+ );
+ }
+ }
+
private void determineIconsSource(LocationComponentOptions options) {
String foregroundIconString = buildIconString(
renderMode == RenderMode.GPS ? options.gpsName() : options.foregroundName(), FOREGROUND_ICON);
@@ -444,6 +511,21 @@ final class LocationLayerController {
}
};
+ /**
+ * The listener that handles the updating of the pulsing circle's radius and opacity.
+ */
+ private final MapboxAnimator.AnimationsValueChangeListener<Float> pulsingCircleRadiusListener =
+ new MapboxAnimator.AnimationsValueChangeListener<Float>() {
+ @Override
+ public void onNewAnimationValue(Float newPulsingRadiusValue) {
+ updatePulsingLocationCircleRadius(newPulsingRadiusValue);
+ if (options.pulsingCircleFadeEnabled()) {
+ double newPulsingOpacityValue = 1 - ((newPulsingRadiusValue / 100) * 3);
+ updatePulsingLocationCircleOpacity((float) newPulsingOpacityValue);
+ }
+ }
+ };
+
Set<AnimatorListenerHolder> getAnimationListeners() {
Set<AnimatorListenerHolder> holders = new HashSet<>();
holders.add(new AnimatorListenerHolder(MapboxAnimator.ANIMATOR_LAYER_LATLNG, latLngValueListener));
@@ -459,6 +541,10 @@ final class LocationLayerController {
holders.add(new AnimatorListenerHolder(MapboxAnimator.ANIMATOR_LAYER_ACCURACY, accuracyValueListener));
}
+ if (options.pulseEnabled()) {
+ holders.add(new AnimatorListenerHolder(MapboxAnimator.ANIMATOR_PULSING_CIRCLE,
+ pulsingCircleRadiusListener));
+ }
return holders;
}
} \ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimator.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimator.java
index dff7369cd5..2ffaceb507 100644
--- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimator.java
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimator.java
@@ -26,7 +26,8 @@ abstract class MapboxAnimator<K> extends ValueAnimator implements ValueAnimator.
ANIMATOR_CAMERA_COMPASS_BEARING,
ANIMATOR_LAYER_ACCURACY,
ANIMATOR_ZOOM,
- ANIMATOR_TILT
+ ANIMATOR_TILT,
+ ANIMATOR_PULSING_CIRCLE
})
@interface Type {
}
@@ -40,6 +41,7 @@ abstract class MapboxAnimator<K> extends ValueAnimator implements ValueAnimator.
static final int ANIMATOR_LAYER_ACCURACY = 6;
static final int ANIMATOR_ZOOM = 7;
static final int ANIMATOR_TILT = 8;
+ static final int ANIMATOR_PULSING_CIRCLE = 9;
private final AnimationsValueChangeListener<K> updateListener;
private final K target;
@@ -59,6 +61,15 @@ abstract class MapboxAnimator<K> extends ValueAnimator implements ValueAnimator.
addListener(new AnimatorListener());
}
+ public MapboxAnimator(AnimationsValueChangeListener<K> updateListener, K target, K animatedValue,
+ double minUpdateInterval, long timeElapsed) {
+ this.updateListener = updateListener;
+ this.target = target;
+ this.animatedValue = animatedValue;
+ this.minUpdateInterval = minUpdateInterval;
+ this.timeElapsed = timeElapsed;
+ }
+
@Override
public void onAnimationUpdate(ValueAnimator animation) {
animatedValue = (K) animation.getAnimatedValue();
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimatorProvider.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimatorProvider.java
index 938f4ec74a..36cce25d2b 100644
--- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimatorProvider.java
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimatorProvider.java
@@ -1,5 +1,6 @@
package com.mapbox.mapboxsdk.location;
+import android.animation.ValueAnimator;
import android.support.annotation.Nullable;
import com.mapbox.mapboxsdk.geometry.LatLng;
@@ -37,4 +38,33 @@ final class MapboxAnimatorProvider {
@Nullable MapboxMap.CancelableCallback cancelableCallback) {
return new MapboxCameraAnimatorAdapter(previous, target, updateListener, cancelableCallback);
}
+
+ /**
+ * This animator is for the LocationComponent pulsing circle.
+ *
+ * @param updateListener the listener that is found in the {@link LocationAnimatorCoordinator}'s
+ * listener array.
+ * @param maxAnimationFps the max frames per second of the pulsing animation
+ * @param pulseSingleDuration the number of milliseconds it takes for the animator to create
+ * a single pulse.
+ * @param pulseMaxRadius the max radius when the circle is finished with a single pulse.
+ * @param desiredInterpolatorFromOptions the type of Android-system interpolator to use for
+ * the pulsing animation (linear, accelerate, bounce, etc.)
+ * @return a built {@link PulsingLocationCircleAnimator} object.
+ */
+ PulsingLocationCircleAnimator pulsingCircleAnimator(MapboxAnimator.AnimationsValueChangeListener updateListener,
+ int maxAnimationFps,
+ float pulseSingleDuration,
+ float pulseMaxRadius,
+ String desiredInterpolatorFromOptions) {
+ PulsingLocationCircleAnimator pulsingLocationCircleAnimator =
+ new PulsingLocationCircleAnimator(updateListener, maxAnimationFps, pulseMaxRadius);
+ pulsingLocationCircleAnimator.setDuration((long) pulseSingleDuration);
+ pulsingLocationCircleAnimator.setRepeatMode(ValueAnimator.RESTART);
+ pulsingLocationCircleAnimator.setRepeatCount(ValueAnimator.INFINITE);
+ pulsingLocationCircleAnimator.retrievePulseInterpolator(desiredInterpolatorFromOptions);
+ pulsingLocationCircleAnimator.setInterpolator(
+ pulsingLocationCircleAnimator.retrievePulseInterpolator(desiredInterpolatorFromOptions));
+ return pulsingLocationCircleAnimator;
+ }
}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimatorSetProvider.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimatorSetProvider.java
index 1d09f8ae71..da7c666ae9 100644
--- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimatorSetProvider.java
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimatorSetProvider.java
@@ -29,4 +29,15 @@ class MapboxAnimatorSetProvider {
locationAnimatorSet.setDuration(duration);
locationAnimatorSet.start();
}
+
+ /**
+ * Starts a single animator rather than playing multliple animators all at once.
+ *
+ * @param singleAnimation the {@link Animator} to run.
+ */
+ void startSingleAnimation(@NonNull Animator singleAnimation) {
+ AnimatorSet locationAnimatorSet = new AnimatorSet();
+ locationAnimatorSet.play(singleAnimation);
+ locationAnimatorSet.start();
+ }
}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/PulsingLocationCircleAnimator.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/PulsingLocationCircleAnimator.java
new file mode 100644
index 0000000000..bfae5102ab
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/PulsingLocationCircleAnimator.java
@@ -0,0 +1,42 @@
+package com.mapbox.mapboxsdk.location;
+
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.BounceInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.view.animation.LinearInterpolator;
+
+import com.mapbox.mapboxsdk.location.modes.PulseMode;
+
+/**
+ * Manages the logic of the interpolated animation which is applied to the LocationComponent's pulsing circle
+ */
+public class PulsingLocationCircleAnimator extends MapboxFloatAnimator {
+
+ /**
+ *
+ * @param updateListener the {@link AnimationsValueChangeListener} associated with this animator.
+ * @param maxAnimationFps the maximum frames per second that the animator should use. Default
+ * is the {@link LocationAnimatorCoordinator#maxAnimationFps} variable.
+ */
+ public PulsingLocationCircleAnimator(AnimationsValueChangeListener updateListener,
+ int maxAnimationFps,
+ float circleMaxRadius) {
+ super(0f, circleMaxRadius, updateListener, maxAnimationFps);
+ }
+
+ public Interpolator retrievePulseInterpolator(String desiredInterpolatorFromOptions) {
+ switch (desiredInterpolatorFromOptions) {
+ case PulseMode.LINEAR:
+ return new LinearInterpolator();
+ case PulseMode.ACCELERATE:
+ return new AccelerateInterpolator();
+ case PulseMode.DECELERATE:
+ return new DecelerateInterpolator();
+ case PulseMode.BOUNCE:
+ return new BounceInterpolator();
+ default:
+ return new DecelerateInterpolator();
+ }
+ }
+} \ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/modes/PulseMode.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/modes/PulseMode.java
new file mode 100644
index 0000000000..f8713ffe86
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/modes/PulseMode.java
@@ -0,0 +1,50 @@
+package com.mapbox.mapboxsdk.location.modes;
+
+import android.support.annotation.StringDef;
+
+import com.mapbox.mapboxsdk.location.LocationComponentOptions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A convenience class for setting the {@link com.mapbox.mapboxsdk.location.LocationComponent}'s
+ * pulsing circle UI functionality. Use with
+ */
+public final class PulseMode {
+
+ private PulseMode() {
+ // Class should not be initialized
+ }
+
+ /**
+ * An interpolator defines the rate of change of an animation.
+ *
+ * One of these constants should be used with {@link LocationComponentOptions#pulseInterpolator}.
+ *
+ */
+ @StringDef( {LINEAR, ACCELERATE, DECELERATE, BOUNCE})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Mode {
+ }
+
+ /**
+ * An interpolator where the rate of change is constant.
+ */
+ public static final String LINEAR = "linear";
+
+ /**
+ * An interpolator where the rate of change starts out slowly and and then accelerates.
+ */
+ public static final String ACCELERATE = "accelerate";
+
+ /**
+ * An interpolator where the rate of change starts out quickly and and then decelerates.
+ */
+ public static final String DECELERATE = "decelerate";
+
+ /**
+ * An interpolator where the change bounces at the end.
+ */
+ public static final String BOUNCE = "bounce";
+}
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/res-public/values/public.xml b/platform/android/MapboxGLAndroidSDK/src/main/res-public/values/public.xml
index 60a1efc771..731ec8a0e7 100644
--- a/platform/android/MapboxGLAndroidSDK/src/main/res-public/values/public.xml
+++ b/platform/android/MapboxGLAndroidSDK/src/main/res-public/values/public.xml
@@ -159,4 +159,12 @@
<public name="mapbox_layer_above" format="string" type="attr" />
<public name="mapbox_layer_below" format="string" type="attr" />
-</resources>
+ <!-- Pulsing circle -->
+ <public name="mapbox_pulsingLocationCircleEnabled" format="boolean" type="attr" />
+ <public name="mapbox_pulsingLocationCircleFadeEnabled" format="boolean" type="attr" />
+ <public name="mapbox_pulsingLocationCircleColor" format="color" type="attr" />
+ <public name="mapbox_pulsingLocationCircleDuration" format="float" type="attr" />
+ <public name="mapbox_pulsingLocationCircleRadius" format="float" type="attr" />
+ <public name="mapbox_pulsingLocationCircleAlpha" format="float" type="attr" />
+ <public name="mapbox_pulsingLocationCircleInterpolator" format="string" type="attr" />
+</resources> \ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/res/values/attrs.xml b/platform/android/MapboxGLAndroidSDK/src/main/res/values/attrs.xml
index ff8a32ac64..a8703e81d1 100644
--- a/platform/android/MapboxGLAndroidSDK/src/main/res/values/attrs.xml
+++ b/platform/android/MapboxGLAndroidSDK/src/main/res/values/attrs.xml
@@ -182,5 +182,14 @@
<!-- Accuracy animation-->
<attr name="mapbox_accuracyAnimationEnabled" format="boolean" />
+ <!-- Pulsing circle -->
+ <attr name="mapbox_pulsingLocationCircleEnabled" format="boolean" />
+ <attr name="mapbox_pulsingLocationCircleFadeEnabled" format="boolean" />
+ <attr name="mapbox_pulsingLocationCircleColor" format="color" />
+ <attr name="mapbox_pulsingLocationCircleDuration" format="float" />
+ <attr name="mapbox_pulsingLocationCircleRadius" format="float" />
+ <attr name="mapbox_pulsingLocationCircleAlpha" format="float" />
+ <attr name="mapbox_pulsingLocationCircleInterpolator" format="string" />
+
</declare-styleable>
</resources>
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/res/values/styles.xml b/platform/android/MapboxGLAndroidSDK/src/main/res/values/styles.xml
index c6c2d3fc7b..32429e2b62 100644
--- a/platform/android/MapboxGLAndroidSDK/src/main/res/values/styles.xml
+++ b/platform/android/MapboxGLAndroidSDK/src/main/res/values/styles.xml
@@ -33,5 +33,13 @@
<item name="mapbox_trackingInitialMoveThreshold">@dimen/mapbox_locationComponentTrackingInitialMoveThreshold</item>
<item name="mapbox_trackingMultiFingerMoveThreshold">@dimen/mapbox_locationComponentTrackingMultiFingerMoveThreshold</item>
+ <!-- Location pulsing circle -->
+ <item name="mapbox_pulsingLocationCircleEnabled">false</item>
+ <item name="mapbox_pulsingLocationCircleFadeEnabled">true</item>
+ <item name="mapbox_pulsingLocationCircleColor">@color/mapbox_location_layer_blue</item>
+ <item name="mapbox_pulsingLocationCircleDuration">2300</item>
+ <item name="mapbox_pulsingLocationCircleRadius">35</item>
+ <item name="mapbox_pulsingLocationCircleAlpha">0.4</item>
+ <item name="mapbox_pulsingLocationCircleInterpolator">decelerate</item>
</style>
</resources> \ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinatorTest.kt b/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinatorTest.kt
index 1b927d213a..9039f8bb69 100644
--- a/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinatorTest.kt
+++ b/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinatorTest.kt
@@ -21,11 +21,13 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito
import org.robolectric.RobolectricTestRunner
+import org.robolectric.RuntimeEnvironment
@RunWith(RobolectricTestRunner::class)
class LocationAnimatorCoordinatorTest {
private lateinit var locationAnimatorCoordinator: LocationAnimatorCoordinator
+ private lateinit var locationComponentOptions: LocationComponentOptions
private val cameraPosition: CameraPosition = CameraPosition.DEFAULT
private val animatorProvider: MapboxAnimatorProvider = mockk()
@@ -35,7 +37,14 @@ class LocationAnimatorCoordinatorTest {
@Before
fun setUp() {
- locationAnimatorCoordinator = LocationAnimatorCoordinator(projection, animatorSetProvider, animatorProvider)
+ locationComponentOptions = LocationComponentOptions.builder(
+ RuntimeEnvironment.systemContext)
+ .pulsingCircleEnabled(true)
+ .build()
+
+ locationAnimatorCoordinator = LocationAnimatorCoordinator(projection,
+ animatorSetProvider,
+ animatorProvider, locationComponentOptions)
configureAnimatorProvider()
every { projection.getMetersPerPixelAtLatitude(any()) } answers { 1.0 }
every { animatorSetProvider.startAnimation(any(), any(), any()) } answers {}
@@ -48,7 +57,8 @@ class LocationAnimatorCoordinatorTest {
ANIMATOR_CAMERA_COMPASS_BEARING,
ANIMATOR_LAYER_ACCURACY,
ANIMATOR_ZOOM,
- ANIMATOR_TILT
+ ANIMATOR_TILT,
+ ANIMATOR_PULSING_CIRCLE
))
}
@@ -347,6 +357,12 @@ class LocationAnimatorCoordinatorTest {
}
@Test
+ fun startPulsingCircle_animatorCreated() {
+ locationAnimatorCoordinator.startLocationComponentCirclePulsing(locationComponentOptions)
+ assertTrue(locationAnimatorCoordinator.animatorArray[ANIMATOR_PULSING_CIRCLE] != null)
+ }
+
+ @Test
fun feedNewTiltLevel_animatorValue() {
val tilt = 30.0f
locationAnimatorCoordinator.feedNewTilt(
diff --git a/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationLayerControllerTest.java b/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationLayerControllerTest.java
index ed2d015d85..4fbf6f98c3 100644
--- a/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationLayerControllerTest.java
+++ b/platform/android/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationLayerControllerTest.java
@@ -33,6 +33,7 @@ import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_
import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_FOREGROUND_ICON_OFFSET;
import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_GPS_BEARING;
import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_SHADOW_ICON_OFFSET;
+import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PULSING_CIRCLE_LAYER;
import static com.mapbox.mapboxsdk.location.LocationComponentConstants.SHADOW_ICON;
import static com.mapbox.mapboxsdk.location.LocationComponentConstants.SHADOW_LAYER;
import static com.mapbox.mapboxsdk.location.MapboxAnimator.ANIMATOR_LAYER_ACCURACY;
@@ -161,6 +162,23 @@ public class LocationLayerControllerTest {
}
@Test
+ public void onInitialization_pulsingCircleLayerIsAdded() {
+ OnRenderModeChangedListener internalRenderModeChangedListener = mock(OnRenderModeChangedListener.class);
+ LayerSourceProvider sourceProvider = buildLayerProvider();
+ Layer pulsingCircleLayer = mock(Layer.class);
+ when(sourceProvider.generatePulsingCircleLayer()).thenReturn(pulsingCircleLayer);
+ GeoJsonSource locationSource = mock(GeoJsonSource.class);
+ when(sourceProvider.generateSource(any(Feature.class))).thenReturn(locationSource);
+ LayerBitmapProvider bitmapProvider = mock(LayerBitmapProvider.class);
+ LocationComponentOptions options = mock(LocationComponentOptions.class);
+
+ new LocationLayerController(mapboxMap, mapboxMap.getStyle(), sourceProvider, buildFeatureProvider(options),
+ bitmapProvider, options, internalRenderModeChangedListener);
+
+ verify(style).addLayerBelow(pulsingCircleLayer, ACCURACY_LAYER);
+ }
+
+ @Test
public void onInitialization_numberOfCachedLayerIdsIsConstant() {
OnRenderModeChangedListener internalRenderModeChangedListener = mock(OnRenderModeChangedListener.class);
LayerSourceProvider sourceProvider = buildLayerProvider();
@@ -175,7 +193,7 @@ public class LocationLayerControllerTest {
controller.initializeComponents(mapboxMap.getStyle(), options);
- assertEquals(5, controller.layerSet.size());
+ assertEquals(6, controller.layerSet.size());
}
@Test
@@ -379,7 +397,7 @@ public class LocationLayerControllerTest {
layerController.applyStyle(options);
verify(style, times(0)).removeLayer(any(String.class));
- verify(style, times(5)).addLayerBelow(any(Layer.class), any(String.class));
+ verify(style, times(6)).addLayerBelow(any(Layer.class), any(String.class));
}
@Test
@@ -401,7 +419,7 @@ public class LocationLayerControllerTest {
verify(style, times(0)).removeLayer(any(String.class));
verify(style, times(1)).addLayer(any(Layer.class));
- verify(style, times(4)).addLayerBelow(any(Layer.class), Mockito.<String>any());
+ verify(style, times(5)).addLayerBelow(any(Layer.class), Mockito.<String>any());
}
@Test
@@ -701,6 +719,10 @@ public class LocationLayerControllerTest {
Layer accuracyLayer = mock(Layer.class);
when(accuracyLayer.getId()).thenReturn(ACCURACY_LAYER);
when(layerSourceProvider.generateAccuracyLayer()).thenReturn(accuracyLayer);
+
+ Layer pulsingCircleLayer = mock(Layer.class);
+ when(pulsingCircleLayer.getId()).thenReturn(PULSING_CIRCLE_LAYER);
+ when(layerSourceProvider.generatePulsingCircleLayer()).thenReturn(pulsingCircleLayer);
return layerSourceProvider;
}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/location/LocationLayerControllerTest.kt b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/location/LocationLayerControllerTest.kt
index ab70f188c3..9343ce51fb 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/location/LocationLayerControllerTest.kt
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/location/LocationLayerControllerTest.kt
@@ -2,6 +2,7 @@ package com.mapbox.mapboxsdk.location
import android.Manifest
import android.content.Context
+import android.graphics.Color
import android.location.Location
import android.support.test.annotation.UiThreadTest
import android.support.test.espresso.Espresso.onView
@@ -289,6 +290,45 @@ class LocationLayerControllerTest : EspressoTest() {
assertThat(mapboxMap.isLayerVisible(SHADOW_LAYER), `is`(true))
assertThat(mapboxMap.isLayerVisible(ACCURACY_LAYER), `is`(true))
assertThat(mapboxMap.isLayerVisible(BEARING_LAYER), `is`(false))
+ assertThat(mapboxMap.isLayerVisible(PULSING_CIRCLE_LAYER), `is`(false))
+ }
+ }
+ executeComponentTest(componentAction)
+ }
+
+ @Test
+ fun onMapChange_locationComponentPulsingCircleLayerGetsRedrawn() {
+ val componentAction = object : LocationComponentAction.OnPerformLocationComponentAction {
+ override fun onLocationComponentAction(
+ component: LocationComponent,
+ mapboxMap: MapboxMap,
+ style: Style,
+ uiController: UiController,
+ context: Context
+ ) {
+ component.activateLocationComponent(
+ LocationComponentActivationOptions
+ .builder(context, style)
+ .locationComponentOptions(LocationComponentOptions.builder(context)
+ .pulsingCircleEnabled(true)
+ .build())
+ .useDefaultLocationEngine(false)
+ .build()
+ )
+ component.isLocationComponentEnabled = true
+ component.renderMode = RenderMode.NORMAL
+ component.forceLocationUpdate(location)
+ styleChangeIdlingResource.waitForStyle(mapboxMap, Style.LIGHT)
+ TestingAsyncUtils.waitForLayer(uiController, mapView)
+
+ assertThat(component.renderMode, `is`(equalTo(RenderMode.NORMAL)))
+
+ // Check that the Source has been re-added to the new map style
+ val source: GeoJsonSource? = mapboxMap.style!!.getSourceAs(LOCATION_SOURCE)
+ assertThat(source, notNullValue())
+
+ // Check that the pulsing circle layer visibilities is set to visible
+ assertThat(mapboxMap.isLayerVisible(PULSING_CIRCLE_LAYER), `is`(true))
}
}
executeComponentTest(componentAction)
@@ -558,6 +598,171 @@ class LocationLayerControllerTest : EspressoTest() {
}
@Test
+ fun pulsingCircle_enableLocationComponent_pulsingLayerVisibility() {
+ val componentAction = object : LocationComponentAction.OnPerformLocationComponentAction {
+ override fun onLocationComponentAction(
+ component: LocationComponent,
+ mapboxMap: MapboxMap,
+ style: Style,
+ uiController: UiController,
+ context: Context
+ ) {
+ component.activateLocationComponent(
+ LocationComponentActivationOptions
+ .builder(context, style)
+ .useDefaultLocationEngine(false)
+ .build()
+ )
+ component.isLocationComponentEnabled = true
+ component.forceLocationUpdate(location)
+ TestingAsyncUtils.waitForLayer(uiController, mapView)
+
+ component.applyStyle(LocationComponentOptions.builder(context)
+ .pulsingCircleEnabled(true).build())
+
+ assertThat(mapboxMap.isLayerVisible(PULSING_CIRCLE_LAYER), `is`(true))
+ }
+ }
+ executeComponentTest(componentAction)
+ }
+
+ @Test
+ fun pulsingCircle_disableLocationComponent_pulsingLayerVisibility() {
+ val componentAction = object : LocationComponentAction.OnPerformLocationComponentAction {
+ override fun onLocationComponentAction(
+ component: LocationComponent,
+ mapboxMap: MapboxMap,
+ style: Style,
+ uiController: UiController,
+ context: Context
+ ) {
+ component.activateLocationComponent(
+ LocationComponentActivationOptions
+ .builder(context, style)
+ .useDefaultLocationEngine(false)
+ .build()
+ )
+ component.isLocationComponentEnabled = false
+ component.forceLocationUpdate(location)
+ TestingAsyncUtils.waitForLayer(uiController, mapView)
+
+ component.applyStyle(LocationComponentOptions.builder(context)
+ .pulsingCircleEnabled(true).build())
+
+ assertThat(mapboxMap.isLayerVisible(PULSING_CIRCLE_LAYER), `is`(false))
+ }
+ }
+ executeComponentTest(componentAction)
+ }
+
+ @Test
+ fun pulsingCircle_cancelLocationComponent_pulsingLayerVisibility() {
+ val componentAction = object : LocationComponentAction.OnPerformLocationComponentAction {
+ override fun onLocationComponentAction(
+ component: LocationComponent,
+ mapboxMap: MapboxMap,
+ style: Style,
+ uiController: UiController,
+ context: Context
+ ) {
+ component.activateLocationComponent(
+ LocationComponentActivationOptions
+ .builder(context, style)
+ .useDefaultLocationEngine(false)
+ .build()
+ )
+ component.isLocationComponentEnabled = true
+ component.forceLocationUpdate(location)
+ TestingAsyncUtils.waitForLayer(uiController, mapView)
+
+ component.applyStyle(LocationComponentOptions.builder(context)
+ .pulsingCircleEnabled(true).build())
+
+ component.cancelPulsingLocationCircle()
+
+ assertThat(mapboxMap.isLayerVisible(PULSING_CIRCLE_LAYER), `is`(false))
+ }
+ }
+ executeComponentTest(componentAction)
+ }
+
+ @Test
+ fun pulsingCircle_changeColorCheck() {
+ val componentAction = object : LocationComponentAction.OnPerformLocationComponentAction {
+ override fun onLocationComponentAction(
+ component: LocationComponent,
+ mapboxMap: MapboxMap,
+ style: Style,
+ uiController: UiController,
+ context: Context
+ ) {
+ component.activateLocationComponent(
+ LocationComponentActivationOptions
+ .builder(context, style)
+ .useDefaultLocationEngine(false)
+ .build()
+ )
+ component.isLocationComponentEnabled = true
+ component.forceLocationUpdate(location)
+ TestingAsyncUtils.waitForLayer(uiController, mapView)
+
+ component.applyStyle(LocationComponentOptions.builder(context)
+ .pulsingCircleEnabled(true)
+ .pulsingCircleColor(Color.RED)
+ .build())
+
+ component.applyStyle(LocationComponentOptions.builder(context)
+ .pulsingCircleEnabled(true)
+ .pulsingCircleColor(Color.BLUE)
+ .build())
+
+ mapboxMap.style.apply {
+ assertThat(component.locationComponentOptions.pulseColor(), `is`(Color.BLUE))
+ }
+ }
+ }
+ executeComponentTest(componentAction)
+ }
+
+ @Test
+ fun pulsingCircle_changeSpeedCheck() {
+ val componentAction = object : LocationComponentAction.OnPerformLocationComponentAction {
+ override fun onLocationComponentAction(
+ component: LocationComponent,
+ mapboxMap: MapboxMap,
+ style: Style,
+ uiController: UiController,
+ context: Context
+ ) {
+ component.activateLocationComponent(
+ LocationComponentActivationOptions
+ .builder(context, style)
+ .useDefaultLocationEngine(false)
+ .build()
+ )
+ component.isLocationComponentEnabled = true
+ component.forceLocationUpdate(location)
+ TestingAsyncUtils.waitForLayer(uiController, mapView)
+
+ component.applyStyle(LocationComponentOptions.builder(context)
+ .pulsingCircleEnabled(true)
+ .pulsingCircleDuration(8000f)
+ .build())
+
+ component.applyStyle(LocationComponentOptions.builder(context)
+ .pulsingCircleEnabled(true)
+ .pulsingCircleDuration(400f)
+ .build())
+
+ mapboxMap.style.apply {
+ assertThat(component.locationComponentOptions.pulseSingleDuration(), `is`(400f))
+ }
+ }
+ }
+ executeComponentTest(componentAction)
+ }
+
+ @Test
fun applyStyle_layerBelow_restoreLayerVisibility() {
val componentAction = object : LocationComponentAction.OnPerformLocationComponentAction {
override fun onLocationComponentAction(
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/location/utils/StyleChangeIdlingResource.kt b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/location/utils/StyleChangeIdlingResource.kt
index 050535f6df..0cfc5a3488 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/location/utils/StyleChangeIdlingResource.kt
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/location/utils/StyleChangeIdlingResource.kt
@@ -33,7 +33,7 @@ class StyleChangeIdlingResource : IdlingResource {
fun waitForStyle(mapboxMap: MapboxMap, styleUrl: String) {
isIdle = false
- mapboxMap.setStyle(Style.Builder().fromUrl(styleUrl)) {
+ mapboxMap.setStyle(Style.Builder().fromUri(styleUrl)) {
setIdle()
}
}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml
index 73c9b97f20..7644e3603d 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml
@@ -889,6 +889,28 @@
android:value=".activity.FeatureOverviewActivity" />
</activity>
<activity
+ android:name=".activity.location.BasicLocationPulsingCircleActivity"
+ android:description="@string/description_location_basic_pulsing_circle"
+ android:label="@string/activity_basic_location_pulsing_circle">
+ <meta-data
+ android:name="@string/category"
+ android:value="@string/category_location" />
+ <meta-data
+ android:name="android.support.PARENT_ACTIVITY"
+ android:value=".activity.FeatureOverviewActivity" />
+ </activity>
+ <activity
+ android:name=".activity.location.CustomizedLocationPulsingCircleActivity"
+ android:description="@string/description_location_customized_pulsing_circle"
+ android:label="@string/activity_customized_location_pulsing_circle">
+ <meta-data
+ android:name="@string/category"
+ android:value="@string/category_location" />
+ <meta-data
+ android:name="android.support.PARENT_ACTIVITY"
+ android:value=".activity.FeatureOverviewActivity" />
+ </activity>
+ <activity
android:name=".activity.location.LocationFragmentActivity"
android:description="@string/description_location_fragment"
android:label="@string/activity_location_fragment">
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/location/BasicLocationPulsingCircleActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/location/BasicLocationPulsingCircleActivity.java
new file mode 100644
index 0000000000..213c607f79
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/location/BasicLocationPulsingCircleActivity.java
@@ -0,0 +1,205 @@
+package com.mapbox.mapboxsdk.testapp.activity.location;
+
+import android.annotation.SuppressLint;
+import android.location.Location;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v7.app.AppCompatActivity;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.Toast;
+
+import com.mapbox.android.core.location.LocationEngineRequest;
+import com.mapbox.android.core.permissions.PermissionsListener;
+import com.mapbox.android.core.permissions.PermissionsManager;
+import com.mapbox.mapboxsdk.location.LocationComponent;
+import com.mapbox.mapboxsdk.location.LocationComponentActivationOptions;
+import com.mapbox.mapboxsdk.location.LocationComponentOptions;
+import com.mapbox.mapboxsdk.location.modes.CameraMode;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
+import com.mapbox.mapboxsdk.maps.Style;
+import com.mapbox.mapboxsdk.testapp.R;
+
+import java.util.List;
+
+public class BasicLocationPulsingCircleActivity extends AppCompatActivity implements OnMapReadyCallback {
+
+ private static final String SAVED_STATE_LOCATION = "saved_state_location";
+ private static final String TAG = "Mbgl-BasicLocationPulsingCircleActivity";
+
+ private Location lastLocation;
+ private MapView mapView;
+ private PermissionsManager permissionsManager;
+ private LocationComponent locationComponent;
+ private MapboxMap mapboxMap;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_location_layer_basic_pulsing_circle);
+
+ mapView = findViewById(R.id.mapView);
+
+ if (savedInstanceState != null) {
+ lastLocation = savedInstanceState.getParcelable(SAVED_STATE_LOCATION);
+ }
+ mapView.onCreate(savedInstanceState);
+
+ checkPermissions();
+ }
+
+ @SuppressLint("MissingPermission")
+ @Override
+ public void onMapReady(@NonNull MapboxMap mapboxMap) {
+ this.mapboxMap = mapboxMap;
+
+ mapboxMap.setStyle(Style.MAPBOX_STREETS, style -> {
+ locationComponent = mapboxMap.getLocationComponent();
+
+ LocationComponentOptions locationComponentOptions =
+ LocationComponentOptions.builder(BasicLocationPulsingCircleActivity.this)
+ .pulsingCircleEnabled(true)
+ .build();
+
+ LocationComponentActivationOptions locationComponentActivationOptions =
+ buildLocationComponentActivationOptions(style,locationComponentOptions);
+
+ locationComponent.activateLocationComponent(locationComponentActivationOptions);
+ locationComponent.setLocationComponentEnabled(true);
+ locationComponent.setCameraMode(CameraMode.TRACKING);
+ locationComponent.forceLocationUpdate(lastLocation);
+ });
+ }
+
+ private LocationComponentActivationOptions buildLocationComponentActivationOptions(@NonNull Style style,
+ @NonNull LocationComponentOptions
+ locationComponentOptions) {
+ return LocationComponentActivationOptions
+ .builder(this, style)
+ .locationComponentOptions(locationComponentOptions)
+ .useDefaultLocationEngine(true)
+ .locationEngineRequest(new LocationEngineRequest.Builder(750)
+ .setFastestInterval(750)
+ .setPriority(LocationEngineRequest.PRIORITY_HIGH_ACCURACY)
+ .build())
+ .build();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_pulsing_location_mode, menu);
+ return true;
+ }
+
+ @SuppressLint("MissingPermission")
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (locationComponent == null) {
+ return super.onOptionsItemSelected(item);
+ }
+
+ int id = item.getItemId();
+ if (id == R.id.action_map_style_change) {
+ toggleMapStyle();
+ return true;
+ } else if (id == R.id.action_component_disable) {
+ locationComponent.setLocationComponentEnabled(false);
+ return true;
+ } else if (id == R.id.action_component_enabled) {
+ locationComponent.setLocationComponentEnabled(true);
+ return true;
+ } else if (id == R.id.action_cancel_pulsing) {
+ locationComponent.cancelPulsingLocationCircle();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void toggleMapStyle() {
+ if (locationComponent == null) {
+ return;
+ }
+
+ mapboxMap.setStyle(Style.LIGHT);
+ }
+
+ private void checkPermissions() {
+ if (PermissionsManager.areLocationPermissionsGranted(this)) {
+ mapView.getMapAsync(this);
+ } else {
+ permissionsManager = new PermissionsManager(new PermissionsListener() {
+ @Override
+ public void onExplanationNeeded(List<String> permissionsToExplain) {
+ Toast.makeText(BasicLocationPulsingCircleActivity.this, "You need to accept location permissions.",
+ Toast.LENGTH_SHORT).show();
+ }
+
+ @Override
+ public void onPermissionResult(boolean granted) {
+ if (granted) {
+ mapView.getMapAsync(BasicLocationPulsingCircleActivity.this);
+ } else {
+ finish();
+ }
+ }
+ });
+ permissionsManager.requestLocationPermissions(this);
+ }
+ }
+
+
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ permissionsManager.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @SuppressLint("MissingPermission")
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ if (locationComponent != null) {
+ outState.putParcelable(SAVED_STATE_LOCATION, locationComponent.getLastKnownLocation());
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+} \ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/location/CustomizedLocationPulsingCircleActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/location/CustomizedLocationPulsingCircleActivity.java
new file mode 100644
index 0000000000..b71d3fbdd3
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/location/CustomizedLocationPulsingCircleActivity.java
@@ -0,0 +1,359 @@
+package com.mapbox.mapboxsdk.testapp.activity.location;
+
+import android.annotation.SuppressLint;
+import android.graphics.Color;
+import android.location.Location;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.ListPopupWindow;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.Toast;
+
+import com.mapbox.android.core.location.LocationEngineRequest;
+import com.mapbox.android.core.permissions.PermissionsListener;
+import com.mapbox.android.core.permissions.PermissionsManager;
+import com.mapbox.mapboxsdk.location.LocationComponent;
+import com.mapbox.mapboxsdk.location.LocationComponentActivationOptions;
+import com.mapbox.mapboxsdk.location.LocationComponentOptions;
+import com.mapbox.mapboxsdk.location.modes.CameraMode;
+import com.mapbox.mapboxsdk.location.modes.PulseMode;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
+import com.mapbox.mapboxsdk.maps.Style;
+import com.mapbox.mapboxsdk.testapp.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class CustomizedLocationPulsingCircleActivity extends AppCompatActivity implements OnMapReadyCallback {
+
+
+ //region
+
+ // Adjust these variables to customize the example's pulsing circle UI
+ private static final float DEFAULT_LOCATION_CIRCLE_PULSE_DURATION_MS = 2300;
+ private static final float SECOND_LOCATION_CIRCLE_PULSE_DURATION_MS = 800;
+ private static final float THIRD_LOCATION_CIRCLE_PULSE_DURATION_MS = 8000;
+ private static final float DEFAULT_LOCATION_CIRCLE_PULSE_RADIUS = 35;
+ private static final float DEFAULT_LOCATION_CIRCLE_PULSE_ALPHA = .55f;
+ private static final String DEFAULT_LOCATION_CIRCLE_INTERPOLATOR_PULSE_MODE = PulseMode.DECELERATE;
+ private static final boolean DEFAULT_LOCATION_CIRCLE_PULSE_FADE_MODE = true;
+ //endregion
+
+ //region
+ private static int LOCATION_CIRCLE_PULSE_COLOR;
+ private static float LOCATION_CIRCLE_PULSE_DURATION = DEFAULT_LOCATION_CIRCLE_PULSE_DURATION_MS;
+ private static final String SAVED_STATE_LOCATION = "saved_state_location";
+ private static final String SAVED_STATE_LOCATION_CIRCLE_PULSE_COLOR = "saved_state_color";
+ private static final String SAVED_STATE_LOCATION_CIRCLE_PULSE_DURATION = "saved_state_duration";
+ private static final String TAG = "Mbgl-CustomizedLocationPulsingCircleActivity";
+
+ private Location lastLocation;
+ private MapView mapView;
+ private Button pulsingCircleDurationButton;
+ private Button pulsingCircleColorButton;
+ private PermissionsManager permissionsManager;
+ private LocationComponent locationComponent;
+ private MapboxMap mapboxMap;
+ private float currentPulseDuration;
+ //endregion
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_location_layer_customized_pulsing_circle);
+
+ LOCATION_CIRCLE_PULSE_COLOR = Color.BLUE;
+
+ mapView = findViewById(R.id.mapView);
+
+ if (savedInstanceState != null) {
+ lastLocation = savedInstanceState.getParcelable(SAVED_STATE_LOCATION);
+ LOCATION_CIRCLE_PULSE_COLOR = savedInstanceState.getInt(SAVED_STATE_LOCATION_CIRCLE_PULSE_COLOR);
+ LOCATION_CIRCLE_PULSE_DURATION = savedInstanceState.getFloat(SAVED_STATE_LOCATION_CIRCLE_PULSE_DURATION);
+ }
+
+ pulsingCircleDurationButton = findViewById(R.id.button_location_circle_duration);
+ pulsingCircleDurationButton.setText(String.format("%sms",
+ String.valueOf(LOCATION_CIRCLE_PULSE_DURATION)));
+ pulsingCircleDurationButton.setOnClickListener(v -> {
+ if (locationComponent == null) {
+ return;
+ }
+ showDurationListDialog();
+ });
+
+ pulsingCircleColorButton = findViewById(R.id.button_location_circle_color);
+ pulsingCircleColorButton.setOnClickListener(v -> {
+ if (locationComponent == null) {
+ return;
+ }
+ showColorListDialog();
+ });
+
+ mapView.onCreate(savedInstanceState);
+
+ checkPermissions();
+ }
+
+ @SuppressLint("MissingPermission")
+ @Override
+ public void onMapReady(@NonNull MapboxMap mapboxMap) {
+ this.mapboxMap = mapboxMap;
+
+ mapboxMap.setStyle(Style.MAPBOX_STREETS, style -> {
+ locationComponent = mapboxMap.getLocationComponent();
+
+ LocationComponentOptions locationComponentOptions =
+ buildLocationComponentOptions(
+ "waterway-label",
+ LOCATION_CIRCLE_PULSE_COLOR,
+ DEFAULT_LOCATION_CIRCLE_PULSE_ALPHA,
+ LOCATION_CIRCLE_PULSE_DURATION,
+ DEFAULT_LOCATION_CIRCLE_PULSE_RADIUS);
+
+ LocationComponentActivationOptions locationComponentActivationOptions =
+ buildLocationComponentActivationOptions(style,locationComponentOptions);
+
+ locationComponent.activateLocationComponent(locationComponentActivationOptions);
+ locationComponent.setLocationComponentEnabled(true);
+ locationComponent.setCameraMode(CameraMode.TRACKING);
+ locationComponent.forceLocationUpdate(lastLocation);
+ });
+ }
+
+ private LocationComponentOptions buildLocationComponentOptions(@Nullable String idOfLayerBelow,
+ @Nullable int pulsingCircleColor,
+ @Nullable float pulsingCircleAlpha,
+ @Nullable float pulsingCircleDuration,
+ @Nullable float pulsingCircleMaxRadius
+ ) {
+ currentPulseDuration = pulsingCircleDuration;
+ return LocationComponentOptions.builder(this)
+ .layerBelow(idOfLayerBelow)
+ .pulsingCircleEnabled(true)
+ .pulsingCircleFadeEnabled(DEFAULT_LOCATION_CIRCLE_PULSE_FADE_MODE)
+ .pulsingCircleInterpolator(DEFAULT_LOCATION_CIRCLE_INTERPOLATOR_PULSE_MODE)
+ .pulsingCircleColor(pulsingCircleColor)
+ .pulsingCircleAlpha(pulsingCircleAlpha)
+ .pulsingCircleDuration(pulsingCircleDuration)
+ .pulsingCircleMaxRadius(pulsingCircleMaxRadius)
+ .build();
+ }
+
+ @SuppressLint("MissingPermission")
+ private void setNewLocationComponentOptions(@Nullable float newPulsingDuration,
+ @Nullable int newPulsingColor) {
+ mapboxMap.getStyle(new Style.OnStyleLoaded() {
+ @Override
+ public void onStyleLoaded(@NonNull Style style) {
+ locationComponent.applyStyle(
+ buildLocationComponentOptions(
+ "waterway-label",
+ newPulsingColor, DEFAULT_LOCATION_CIRCLE_PULSE_ALPHA,
+ newPulsingDuration,
+ DEFAULT_LOCATION_CIRCLE_PULSE_RADIUS));
+ }
+ });
+ }
+
+ private LocationComponentActivationOptions buildLocationComponentActivationOptions(
+ @NonNull Style style,
+ @NonNull LocationComponentOptions locationComponentOptions) {
+ return LocationComponentActivationOptions
+ .builder(this, style)
+ .locationComponentOptions(locationComponentOptions)
+ .useDefaultLocationEngine(true)
+ .locationEngineRequest(new LocationEngineRequest.Builder(750)
+ .setFastestInterval(750)
+ .setPriority(LocationEngineRequest.PRIORITY_HIGH_ACCURACY)
+ .build())
+ .build();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_pulsing_location_mode, menu);
+ return true;
+ }
+
+ @SuppressLint("MissingPermission")
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (locationComponent == null) {
+ return super.onOptionsItemSelected(item);
+ }
+
+ int id = item.getItemId();
+ if (id == R.id.action_map_style_change) {
+ toggleMapStyle();
+ return true;
+ } else if (id == R.id.action_component_disable) {
+ locationComponent.setLocationComponentEnabled(false);
+ return true;
+ } else if (id == R.id.action_component_enabled) {
+ locationComponent.setLocationComponentEnabled(true);
+ return true;
+ } else if (id == R.id.action_cancel_pulsing) {
+ locationComponent.cancelPulsingLocationCircle();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void toggleMapStyle() {
+ if (locationComponent == null) {
+ return;
+ }
+
+ mapboxMap.setStyle(Style.LIGHT);
+ }
+
+ private void checkPermissions() {
+ if (PermissionsManager.areLocationPermissionsGranted(this)) {
+ mapView.getMapAsync(this);
+ } else {
+ permissionsManager = new PermissionsManager(new PermissionsListener() {
+ @Override
+ public void onExplanationNeeded(List<String> permissionsToExplain) {
+ Toast.makeText(CustomizedLocationPulsingCircleActivity.this, "You need to accept location permissions.",
+ Toast.LENGTH_SHORT).show();
+ }
+
+ @Override
+ public void onPermissionResult(boolean granted) {
+ if (granted) {
+ mapView.getMapAsync(CustomizedLocationPulsingCircleActivity.this);
+ } else {
+ finish();
+ }
+ }
+ });
+ permissionsManager.requestLocationPermissions(this);
+ }
+ }
+
+ private void showDurationListDialog() {
+ List<String> modes = new ArrayList<>();
+ modes.add(String.format("%sms", String.valueOf(DEFAULT_LOCATION_CIRCLE_PULSE_DURATION_MS)));
+ modes.add(String.format("%sms", String.valueOf(SECOND_LOCATION_CIRCLE_PULSE_DURATION_MS)));
+ modes.add(String.format("%sms", String.valueOf(THIRD_LOCATION_CIRCLE_PULSE_DURATION_MS)));
+ ArrayAdapter<String> profileAdapter = new ArrayAdapter<>(this,
+ android.R.layout.simple_list_item_1, modes);
+ ListPopupWindow listPopup = new ListPopupWindow(this);
+ listPopup.setAdapter(profileAdapter);
+ listPopup.setAnchorView(pulsingCircleDurationButton);
+ listPopup.setOnItemClickListener((parent, itemView, position, id) -> {
+ String selectedMode = modes.get(position);
+ pulsingCircleDurationButton.setText(selectedMode);
+ if (selectedMode.contentEquals(String.format("%sms",
+ String.valueOf(DEFAULT_LOCATION_CIRCLE_PULSE_DURATION_MS)))) {
+ LOCATION_CIRCLE_PULSE_DURATION = DEFAULT_LOCATION_CIRCLE_PULSE_DURATION_MS;
+ setNewLocationComponentOptions(DEFAULT_LOCATION_CIRCLE_PULSE_DURATION_MS, LOCATION_CIRCLE_PULSE_COLOR);
+ } else if (selectedMode.contentEquals(String.format("%sms",
+ String.valueOf(SECOND_LOCATION_CIRCLE_PULSE_DURATION_MS)))) {
+ LOCATION_CIRCLE_PULSE_DURATION = SECOND_LOCATION_CIRCLE_PULSE_DURATION_MS;
+ setNewLocationComponentOptions(SECOND_LOCATION_CIRCLE_PULSE_DURATION_MS, LOCATION_CIRCLE_PULSE_COLOR);
+ } else if (selectedMode.contentEquals(String.format("%sms",
+ String.valueOf(THIRD_LOCATION_CIRCLE_PULSE_DURATION_MS)))) {
+ LOCATION_CIRCLE_PULSE_DURATION = THIRD_LOCATION_CIRCLE_PULSE_DURATION_MS;
+ setNewLocationComponentOptions(THIRD_LOCATION_CIRCLE_PULSE_DURATION_MS, LOCATION_CIRCLE_PULSE_COLOR);
+ }
+ listPopup.dismiss();
+ });
+ listPopup.show();
+ }
+
+ private void showColorListDialog() {
+ List<String> trackingTypes = new ArrayList<>();
+ trackingTypes.add("Blue");
+ trackingTypes.add("Red");
+ trackingTypes.add("Green");
+ trackingTypes.add("Gray");
+ ArrayAdapter<String> profileAdapter = new ArrayAdapter<>(this,
+ android.R.layout.simple_list_item_1, trackingTypes);
+ ListPopupWindow listPopup = new ListPopupWindow(this);
+ listPopup.setAdapter(profileAdapter);
+ listPopup.setAnchorView(pulsingCircleColorButton);
+ listPopup.setOnItemClickListener((parent, itemView, position, id) -> {
+ String selectedTrackingType = trackingTypes.get(position);
+ pulsingCircleColorButton.setText(selectedTrackingType);
+ if (selectedTrackingType.contentEquals("Blue")) {
+ LOCATION_CIRCLE_PULSE_COLOR = Color.BLUE;
+ setNewLocationComponentOptions(currentPulseDuration, Color.BLUE);
+ } else if (selectedTrackingType.contentEquals("Red")) {
+ LOCATION_CIRCLE_PULSE_COLOR = Color.RED;
+ setNewLocationComponentOptions(currentPulseDuration, Color.RED);
+ } else if (selectedTrackingType.contentEquals("Green")) {
+ LOCATION_CIRCLE_PULSE_COLOR = Color.GREEN;
+ setNewLocationComponentOptions(currentPulseDuration, Color.GREEN);
+ } else if (selectedTrackingType.contentEquals("Gray")) {
+ LOCATION_CIRCLE_PULSE_COLOR = Color.parseColor("#4a4a4a");
+ setNewLocationComponentOptions(currentPulseDuration, Color.parseColor("#4a4a4a"));
+ }
+ listPopup.dismiss();
+ });
+ listPopup.show();
+ }
+
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ permissionsManager.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @SuppressLint("MissingPermission")
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ if (locationComponent != null) {
+ outState.putParcelable(SAVED_STATE_LOCATION, locationComponent.getLastKnownLocation());
+ outState.putInt(SAVED_STATE_LOCATION_CIRCLE_PULSE_COLOR, LOCATION_CIRCLE_PULSE_COLOR);
+ outState.putFloat(SAVED_STATE_LOCATION_CIRCLE_PULSE_DURATION, LOCATION_CIRCLE_PULSE_DURATION);
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+} \ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_location_layer_basic_pulsing_circle.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_location_layer_basic_pulsing_circle.xml
new file mode 100644
index 0000000000..2b5243b2da
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_location_layer_basic_pulsing_circle.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <com.mapbox.mapboxsdk.maps.MapView
+ android:id="@+id/mapView"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:mapbox_uiAttribution="false"/>
+
+</FrameLayout> \ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_location_layer_customized_pulsing_circle.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_location_layer_customized_pulsing_circle.xml
new file mode 100644
index 0000000000..ed86efa885
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_location_layer_customized_pulsing_circle.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <com.mapbox.mapboxsdk.maps.MapView
+ android:id="@+id/mapView"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginBottom="0dp"
+ app:layout_constraintBottom_toTopOf="@+id/linearLayout"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintRight_toRightOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:mapbox_uiAttribution="false" />
+
+ <LinearLayout
+ android:id="@+id/linearLayout"
+ style="?android:attr/buttonBarStyle"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:background="@color/primary"
+ android:orientation="horizontal"
+ android:weightSum="4"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintRight_toRightOf="parent"
+ tools:layout_constraintBottom_creator="1"
+ tools:layout_constraintLeft_creator="1"
+ tools:layout_constraintRight_creator="1">
+
+ <TextView
+ android:id="@+id/tv_frequency"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight=".75"
+ android:gravity="center"
+ android:text="Duration:"
+ android:textColor="@color/white"
+ android:textSize="18sp"
+ android:textStyle="bold" />
+
+ <Button
+ android:id="@+id/button_location_circle_duration"
+ style="?android:attr/buttonBarButtonStyle"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1.25"
+ android:gravity="center"
+ tools:text="400ms"
+ android:textColor="@android:color/white" />
+
+ <TextView
+ android:id="@+id/tv_color"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight=".85"
+ android:gravity="center"
+ android:text="Color:"
+ android:textColor="@color/white"
+ android:textSize="18sp"
+ android:textStyle="bold" />
+
+ <Button
+ android:id="@+id/button_location_circle_color"
+ style="?android:attr/buttonBarButtonStyle"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1.15"
+ android:gravity="center"
+ android:text="None"
+ android:textColor="@android:color/white" />
+
+ </LinearLayout>
+
+</android.support.constraint.ConstraintLayout> \ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_pulsing_location_mode.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_pulsing_location_mode.xml
new file mode 100644
index 0000000000..da44915a6c
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_pulsing_location_mode.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <item android:id="@+id/action_map_style_change"
+ android:title="Toggle custom Map style"
+ app:showAsAction="never"/>
+
+ <item android:id="@+id/action_component_disable"
+ android:title="Disable Component"
+ app:showAsAction="never"/>
+
+ <item android:id="@+id/action_component_enabled"
+ android:title="Enable Component"
+ app:showAsAction="never"/>
+
+ <item android:id="@+id/action_cancel_pulsing"
+ android:title="Cancel pulsing"
+ app:showAsAction="never"/>
+
+</menu> \ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/descriptions.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/descriptions.xml
index 7f018a43ff..fe49c54a9a 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/descriptions.xml
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/descriptions.xml
@@ -78,6 +78,8 @@
<string name="description_draggable_marker">Click to add a marker, long-click to drag</string>
<string name="description_location_map_change">Change map\'s style while location is displayed</string>
<string name="description_location_modes">Showcases location render and tracking modes</string>
+ <string name="description_location_basic_pulsing_circle">Display the LocationComponent\'s default pulsing circle</string>
+ <string name="description_location_customized_pulsing_circle">Display and customize the LocationComponent\'s pulsing circle</string>
<string name="description_location_fragment">Uses LocationComponent in a Fragment</string>
<string name="description_location_manual">Force location updates and don\'t rely on the engine</string>
<string name="description_location_activation_builder">Use LocationComponentActivationOptions to set options</string>
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/titles.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/titles.xml
index 90baaed284..39c9b0f77a 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/titles.xml
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/titles.xml
@@ -77,6 +77,8 @@
<string name="activity_draggable_maker">Draggable marker</string>
<string name="activity_location_map_change">Simple Location Activity</string>
<string name="activity_location_modes">Location Modes Activity</string>
+ <string name="activity_basic_location_pulsing_circle">Basic Pulsing Circle Activity</string>
+ <string name="activity_customized_location_pulsing_circle">Customized Pulsing Circle Activity</string>
<string name="activity_location_fragment">Location Fragment</string>
<string name="activity_location_manual">Manual Location updates</string>
<string name="activity_location_activation_builder">Build Location Activation</string>