diff options
author | Mikhail Pozdnyakov <mikhail.pozdnyakov@mapbox.com> | 2019-01-18 13:12:04 +0200 |
---|---|---|
committer | Langston Smith <langston.smith@mapbox.com> | 2019-07-29 23:18:03 -0700 |
commit | 87ec08d992ed8dd6407989bbf3d1638bf7108197 (patch) | |
tree | 51e3153a0820ec951580fe4f1c58e86244887df8 | |
parent | 1c9f11a171807ebd5a20b2c40bc13c33d0b88b06 (diff) | |
download | qtlocation-mapboxgl-87ec08d992ed8dd6407989bbf3d1638bf7108197.tar.gz |
[android] initial additions to add a pulsing locationComponent circle
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 6337287770..4c3ecc4d03 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) { @@ -135,6 +139,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); @@ -294,6 +321,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 5b2dcd8554..cab27ba47d 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, @@ -1145,6 +1154,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) { @@ -1170,6 +1187,9 @@ public final class LocationComponent { } } setCameraMode(locationCameraController.getCameraMode()); + if (options.pulseEnabled()) { + startPulsingLocationCircle(); + } setLastLocation(); updateCompassListenerState(true); setLastCompassHeading(); @@ -1223,7 +1243,7 @@ public final class LocationComponent { locationAnimatorCoordinator = new LocationAnimatorCoordinator( mapboxMap.getProjection(), MapboxAnimatorSetProvider.getInstance(), - MapboxAnimatorProvider.getInstance() + MapboxAnimatorProvider.getInstance(), options ); locationAnimatorCoordinator.setTrackingAnimationDurationMultiplier(options .trackingAnimationDurationMultiplier()); @@ -1337,6 +1357,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 8ab03e7acd..5b16c4dc9f 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; } /** @@ -1671,11 +1904,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 = ""; @@ -1769,7 +2074,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 c0c6017cd6..13f269ad89 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; @@ -130,6 +138,7 @@ final class LocationLayerController { styleBearing(options); styleAccuracy(options.accuracyAlpha(), options.accuracyColor()); styleScaling(options); + stylePulsingCircle(options); determineIconsSource(options); } @@ -189,6 +198,13 @@ final class LocationLayerController { setRenderMode(renderMode); } + /** + * 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) { @@ -245,6 +261,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) { @@ -262,6 +279,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); @@ -279,6 +304,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 // @@ -368,6 +417,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); @@ -447,6 +514,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)); @@ -462,6 +544,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 3e7124a414..9daa1e4c4c 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/res/values/attrs.xml +++ b/platform/android/MapboxGLAndroidSDK/src/main/res/values/attrs.xml @@ -180,5 +180,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 aa0a07b73e..14c6c52551 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 @@ -628,6 +646,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 03e530e174..a9d0aa5fb3 100644 --- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml @@ -880,6 +880,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 082eb39256..44e57c9bb7 100644 --- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/descriptions.xml +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/descriptions.xml @@ -77,6 +77,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 94566ea995..fcd6f3fd15 100644 --- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/titles.xml +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/titles.xml @@ -76,6 +76,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> |