From 9bfe77df403c21357c5d56b9a9757022b3a1fa86 Mon Sep 17 00:00:00 2001 From: Langston Smith Date: Thu, 30 Apr 2020 03:28:36 -0700 Subject: [PATCH] Adding pulsing circle to LocationComponent options (#172) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [location] added pulsing locationcomponent circle * adjusting style switching to roll through all styles * refactored methods and variables for prefix continuity * refactoring to after making start and stop private methods * refactoring to stop pulsing when LocationComponent is disabled and color/speed is then selected * refactoring to swap interpolator string paramete for interpolator interface parameter * LocationLayerControllerTest adjustments * removing unneeded test * more rebase adjustments * [location] added pulsing locationcomponent circle * refactoring to swap interpolator string paramete for interpolator interface parameter * consolidating pulse ui update methods * gl-native vendor bump * refactor to check for activation with locationcomponent * Update MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentOptions.java Co-Authored-By: Łukasz Paczos * Update MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponent.java Co-Authored-By: Łukasz Paczos * Update MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentOptions.java Co-Authored-By: Łukasz Paczos * Update MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentOptions.java Co-Authored-By: Łukasz Paczos * Update MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentOptions.java Co-Authored-By: Łukasz Paczos * Added incompatibility docs * Added pulse alpha range explanation * Do not wrap pulsing circle animator with an animator set to avoid leaks Co-authored-by: Łukasz Paczos --- .../IndicatorLocationLayerRenderer.java | 28 +- .../location/LayerSourceProvider.java | 19 +- .../location/LocationAnimatorCoordinator.java | 43 ++ .../mapboxsdk/location/LocationComponent.java | 33 + .../LocationComponentActivationOptions.java | 8 + .../location/LocationComponentConstants.java | 7 + .../location/LocationComponentOptions.java | 699 +++++++++++++----- .../location/LocationLayerController.java | 32 +- .../location/LocationLayerRenderer.java | 6 + .../mapboxsdk/location/MapboxAnimator.java | 13 +- .../location/MapboxAnimatorProvider.java | 30 + .../location/MapboxAnimatorSetProvider.java | 3 +- .../PulsingLocationCircleAnimator.java | 19 + .../location/SymbolLocationLayerRenderer.java | 53 ++ .../src/main/res-public/values/public.xml | 9 + .../src/main/res/values/attrs.xml | 9 + .../src/main/res/values/styles.xml | 9 + .../LocationAnimatorCoordinatorTest.kt | 4 +- ...ocationComponentActivationOptionsTest.java | 41 + .../LocationComponentOptionsTest.java | 2 +- .../location/LocationLayerControllerTest.java | 27 +- .../location/LocationLayerControllerTest.kt | 170 +++++ .../utils/StyleChangeIdlingResource.kt | 2 +- .../src/main/AndroidManifest.xml | 22 + .../BasicLocationPulsingCircleActivity.java | 215 ++++++ ...stomizedLocationPulsingCircleActivity.java | 360 +++++++++ .../testapp/activity/location/Utils.java | 2 +- ...ty_location_layer_basic_pulsing_circle.xml | 13 + ...cation_layer_customized_pulsing_circle.xml | 78 ++ .../res/menu/menu_pulsing_location_mode.xml | 25 + .../src/main/res/values/descriptions.xml | 2 + .../src/main/res/values/titles.xml | 2 + 32 files changed, 1772 insertions(+), 213 deletions(-) create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/PulsingLocationCircleAnimator.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/location/BasicLocationPulsingCircleActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/location/CustomizedLocationPulsingCircleActivity.java create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_location_layer_basic_pulsing_circle.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_location_layer_customized_pulsing_circle.xml create mode 100644 MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_pulsing_location_mode.xml diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/IndicatorLocationLayerRenderer.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/IndicatorLocationLayerRenderer.java index 5cb1f37df..10c22107b 100644 --- a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/IndicatorLocationLayerRenderer.java +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/IndicatorLocationLayerRenderer.java @@ -2,8 +2,6 @@ package com.mapbox.mapboxsdk.location; import android.graphics.Bitmap; -import androidx.annotation.Nullable; - import com.mapbox.mapboxsdk.geometry.LatLng; import com.mapbox.mapboxsdk.location.modes.RenderMode; import com.mapbox.mapboxsdk.maps.Style; @@ -12,6 +10,8 @@ import com.mapbox.mapboxsdk.style.layers.Layer; import com.mapbox.mapboxsdk.utils.BitmapUtils; import com.mapbox.mapboxsdk.utils.ColorUtils; +import androidx.annotation.Nullable; + import static com.mapbox.mapboxsdk.location.LocationComponentConstants.BACKGROUND_ICON; import static com.mapbox.mapboxsdk.location.LocationComponentConstants.BACKGROUND_STALE_ICON; import static com.mapbox.mapboxsdk.location.LocationComponentConstants.BEARING_ICON; @@ -181,6 +181,30 @@ class IndicatorLocationLayerRenderer implements LocationLayerRenderer { lastBearing = bearing; } + /** + * Adjust the visibility of the pulsing LocationComponent circle. + */ + @Override + public void adjustPulsingCircleLayerVisibility(boolean visible) { + // not supported at this time + } + + /** + * Adjust the the pulsing LocationComponent circle based on the set options. + */ + @Override + public void stylePulsingCircle(LocationComponentOptions options) { + // not supported at this time + } + + /** + * Adjust the visual appearance of the pulsing LocationComponent circle. + */ + @Override + public void updatePulsingUi(float radius, @Nullable Float opacity) { + // not supported at this time + } + private void setImages(@RenderMode.Mode int renderMode, boolean isStale) { String topImage = ""; String bearingImage = ""; diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerSourceProvider.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerSourceProvider.java index 053fdb24f..96d073dac 100644 --- a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerSourceProvider.java +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerSourceProvider.java @@ -32,6 +32,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; @@ -131,9 +132,23 @@ class LayerSourceProvider { LocationIndicatorLayer layer = new LocationIndicatorLayer(FOREGROUND_LAYER); layer.setLocationTransition(new TransitionOptions(0, 0)); layer.setProperties( - LocationPropertyFactory.perspectiveCompensation(0.9f), - LocationPropertyFactory.imageTiltDisplacement(4f) + LocationPropertyFactory.perspectiveCompensation(0.9f), + LocationPropertyFactory.imageTiltDisplacement(4f) ); return layer; } + + /** + * 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/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinator.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinator.java index 50b2d6384..ab8eec709 100644 --- a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinator.java +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinator.java @@ -4,6 +4,7 @@ import android.animation.Animator; import android.location.Location; import android.os.SystemClock; import android.util.SparseArray; +import android.view.animation.DecelerateInterpolator; import android.view.animation.LinearInterpolator; import androidx.annotation.NonNull; @@ -32,6 +33,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; @@ -186,6 +188,28 @@ 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); + if (listener != null) { + PulsingLocationCircleAnimator pulsingLocationCircleAnimator = animatorProvider.pulsingCircleAnimator( + listener, + maxAnimationFps, + options.pulseSingleDuration(), + options.pulseMaxRadius(), + options.pulseInterpolator() == null ? new DecelerateInterpolator() : options.pulseInterpolator()); + 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); @@ -349,6 +373,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) { + animator.start(); + } + } + void resetAllCameraAnimations(@NonNull CameraPosition currentCameraPosition, boolean isGpsNorth) { resetCameraCompassAnimation(currentCameraPosition); boolean snap = resetCameraLocationAnimations(currentCameraPosition, isGpsNorth); @@ -446,6 +482,13 @@ final class LocationAnimatorCoordinator { cancelAnimator(ANIMATOR_TILT); } + /** + * Cancel the pulsing circle location animator. + */ + void stopPulsingCircleAnimation() { + cancelAnimator(ANIMATOR_PULSING_CIRCLE); + } + void cancelAllAnimations() { for (int i = 0; i < animatorArray.size(); i++) { @MapboxAnimator.Type int animatorType = animatorArray.keyAt(i); diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponent.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponent.java index bbb60289d..c7792e692 100644 --- a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponent.java +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponent.java @@ -726,10 +726,25 @@ public final class LocationComponent { locationAnimatorCoordinator.setTrackingAnimationDurationMultiplier(options.trackingAnimationDurationMultiplier()); locationAnimatorCoordinator.setCompassAnimationEnabled(options.compassAnimationEnabled()); locationAnimatorCoordinator.setAccuracyAnimationEnabled(options.accuracyAnimationEnabled()); + if (options.pulseEnabled()) { + startPulsingLocationCircle(); + } else { + stopPulsingLocationCircle(); + } updateMapWithOptions(options); } } + /** + * Starts the LocationComponent's pulsing circle UI. + */ + private void startPulsingLocationCircle() { + if (isEnabled && isLayerReady) { + locationAnimatorCoordinator.startLocationComponentCirclePulsing(options); + locationLayerController.adjustPulsingCircleLayerVisibility(true); + } + } + /** * Zooms to the desired zoom level. * This API can only be used in pair with camera modes other than {@link CameraMode#NONE}. @@ -1215,6 +1230,14 @@ public final class LocationComponent { } } + /** + * Stop the LocationComponent's pulsing circle animation. + */ + private void stopPulsingLocationCircle() { + locationAnimatorCoordinator.stopPulsingCircleAnimation(); + locationLayerController.adjustPulsingCircleLayerVisibility(false); + } + @SuppressLint("MissingPermission") private void onLocationLayerStart() { if (!isComponentInitialized || !isComponentStarted || mapboxMap.getStyle() == null) { @@ -1240,6 +1263,11 @@ public final class LocationComponent { } } setCameraMode(locationCameraController.getCameraMode()); + if (options.pulseEnabled()) { + startPulsingLocationCircle(); + } else { + stopPulsingLocationCircle(); + } setLastLocation(); updateCompassListenerState(true); setLastCompassHeading(); @@ -1256,6 +1284,8 @@ public final class LocationComponent { if (compassEngine != null) { updateCompassListenerState(false); } + + stopPulsingLocationCircle(); locationAnimatorCoordinator.cancelAllAnimations(); if (locationEngine != null) { locationEngine.removeLocationUpdates(currentLocationEngineListener); @@ -1430,6 +1460,9 @@ public final class LocationComponent { boolean isLocationLayerHidden = locationLayerController.isHidden(); if (isEnabled && isComponentStarted && isLocationLayerHidden) { locationLayerController.show(); + if (options.pulseEnabled()) { + locationLayerController.adjustPulsingCircleLayerVisibility(true); + } } } diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentActivationOptions.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentActivationOptions.java index bc295e920..e4c86e655 100644 --- a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentActivationOptions.java +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentActivationOptions.java @@ -131,6 +131,10 @@ public class LocationComponentActivationOptions { * or {@link LocationComponentConstants#LOCATION_SOURCE} are ignored. *
  • All options that alter the image ID, like {@link LocationComponentOptions#foregroundName()}, are ignored. * Use {@link LocationComponentOptions#foregroundDrawable()} to alter the image rendered as the puck. + *
  • The LocationComponent's pulsing effect. Any of the {@link LocationComponentOptions}' + * pulsing methods such as {@link LocationComponentOptions#pulseEnabled()} or + * {@link LocationComponentOptions#pulseColor()}, are useless when using the + * the specialized location layer. * * * @return whether the default specialized location layer is used @@ -244,6 +248,10 @@ public class LocationComponentActivationOptions { * or {@link LocationComponentConstants#LOCATION_SOURCE} are ignored. *
  • All options that alter the image ID, like {@link LocationComponentOptions#foregroundName()}, are ignored. * Use {@link LocationComponentOptions#foregroundDrawable()} to alter the image rendered as the puck. + *
  • The LocationComponent's pulsing effect. Any of the {@link LocationComponentOptions}' + * pulsing methods such as {@link LocationComponentOptions#pulseEnabled()} or + * {@link LocationComponentOptions#pulseColor()}, are useless when using the + * the specialized location layer. * * * @param useSpecializedLocationLayer true if you want to initialize and use the diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentConstants.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentConstants.java index a3b4804b7..596f9c649 100644 --- a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentConstants.java +++ b/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/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentOptions.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentOptions.java index e5b56865d..781d67eb3 100644 --- a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentOptions.java +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentOptions.java @@ -5,12 +5,7 @@ import android.content.res.TypedArray; import android.graphics.Bitmap; import android.os.Parcel; import android.os.Parcelable; -import androidx.annotation.ColorInt; -import androidx.annotation.Dimension; -import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.StyleRes; +import android.view.animation.Interpolator; import com.mapbox.android.gestures.AndroidGesturesManager; import com.mapbox.mapboxsdk.R; @@ -18,6 +13,13 @@ import com.mapbox.mapboxsdk.style.layers.Layer; import java.util.Arrays; +import androidx.annotation.ColorInt; +import androidx.annotation.Dimension; +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StyleRes; + /** * This class exposes options for the Location Component. The options can be set by defining a * style in your apps style.xml file and passing in directly into the {@link LocationComponent} @@ -69,6 +71,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,41 +131,56 @@ public class LocationComponentOptions implements Parcelable { private float trackingAnimationDurationMultiplier; private boolean compassAnimationEnabled; private boolean accuracyAnimationEnabled; + private Boolean pulseEnabled; + private Boolean pulseFadeEnabled; + private Integer pulseColor; + private float pulseSingleDuration; + private float pulseMaxRadius; + private float pulseAlpha; + @Nullable + private Interpolator pulseInterpolator; public LocationComponentOptions( - float accuracyAlpha, - int accuracyColor, - int backgroundDrawableStale, - @Nullable String backgroundStaleName, - int foregroundDrawableStale, - @Nullable String foregroundStaleName, - int gpsDrawable, - @Nullable String gpsName, - int foregroundDrawable, - @Nullable String foregroundName, - int backgroundDrawable, - @Nullable String backgroundName, - int bearingDrawable, - @Nullable String bearingName, - @Nullable Integer bearingTintColor, - @Nullable Integer foregroundTintColor, - @Nullable Integer backgroundTintColor, - @Nullable Integer foregroundStaleTintColor, - @Nullable Integer backgroundStaleTintColor, - float elevation, - boolean enableStaleState, - long staleStateTimeout, - @Nullable int[] padding, - float maxZoomIconScale, - float minZoomIconScale, - boolean trackingGesturesManagement, - float trackingInitialMoveThreshold, - float trackingMultiFingerMoveThreshold, - String layerAbove, - String layerBelow, - float trackingAnimationDurationMultiplier, - boolean compassAnimationEnabled, - boolean accuracyAnimationEnabled) { + float accuracyAlpha, + int accuracyColor, + int backgroundDrawableStale, + @Nullable String backgroundStaleName, + int foregroundDrawableStale, + @Nullable String foregroundStaleName, + int gpsDrawable, + @Nullable String gpsName, + int foregroundDrawable, + @Nullable String foregroundName, + int backgroundDrawable, + @Nullable String backgroundName, + int bearingDrawable, + @Nullable String bearingName, + @Nullable Integer bearingTintColor, + @Nullable Integer foregroundTintColor, + @Nullable Integer backgroundTintColor, + @Nullable Integer foregroundStaleTintColor, + @Nullable Integer backgroundStaleTintColor, + float elevation, + boolean enableStaleState, + long staleStateTimeout, + @Nullable int[] padding, + float maxZoomIconScale, + float minZoomIconScale, + boolean trackingGesturesManagement, + float trackingInitialMoveThreshold, + float trackingMultiFingerMoveThreshold, + String layerAbove, + String layerBelow, + float trackingAnimationDurationMultiplier, + boolean compassAnimationEnabled, + boolean accuracyAnimationEnabled, + Boolean pulseEnabled, + Boolean pulseFadeEnabled, + Integer pulseColor, + float pulseSingleDuration, + float pulseMaxRadius, + float pulseAlpha, + @Nullable Interpolator pulseInterpolator) { this.accuracyAlpha = accuracyAlpha; this.accuracyColor = accuracyColor; this.backgroundDrawableStale = backgroundDrawableStale; @@ -185,6 +217,13 @@ public class LocationComponentOptions implements Parcelable { this.trackingAnimationDurationMultiplier = trackingAnimationDurationMultiplier; this.compassAnimationEnabled = compassAnimationEnabled; this.accuracyAnimationEnabled = accuracyAnimationEnabled; + this.pulseEnabled = pulseEnabled; + this.pulseFadeEnabled = pulseFadeEnabled; + this.pulseColor = pulseColor; + this.pulseSingleDuration = pulseSingleDuration; + this.pulseMaxRadius = pulseMaxRadius; + this.pulseAlpha = pulseAlpha; + this.pulseInterpolator = pulseInterpolator; } /** @@ -202,106 +241,131 @@ public class LocationComponentOptions implements Parcelable { @StyleRes int styleRes) { TypedArray typedArray = context.obtainStyledAttributes( - styleRes, R.styleable.mapbox_LocationComponent); + styleRes, R.styleable.mapbox_LocationComponent); LocationComponentOptions.Builder builder = new LocationComponentOptions.Builder() - .enableStaleState(true) - .staleStateTimeout(STALE_STATE_DELAY_MS) - .maxZoomIconScale(MAX_ZOOM_ICON_SCALE_DEFAULT) - .minZoomIconScale(MIN_ZOOM_ICON_SCALE_DEFAULT) - .padding(PADDING_DEFAULT); + .enableStaleState(true) + .staleStateTimeout(STALE_STATE_DELAY_MS) + .maxZoomIconScale(MAX_ZOOM_ICON_SCALE_DEFAULT) + .minZoomIconScale(MIN_ZOOM_ICON_SCALE_DEFAULT) + .padding(PADDING_DEFAULT); builder.foregroundDrawable(typedArray.getResourceId( - R.styleable.mapbox_LocationComponent_mapbox_foregroundDrawable, -1)); + R.styleable.mapbox_LocationComponent_mapbox_foregroundDrawable, -1)); if (typedArray.hasValue(R.styleable.mapbox_LocationComponent_mapbox_foregroundTintColor)) { builder.foregroundTintColor(typedArray.getColor( - R.styleable.mapbox_LocationComponent_mapbox_foregroundTintColor, -1)); + R.styleable.mapbox_LocationComponent_mapbox_foregroundTintColor, -1)); } builder.backgroundDrawable(typedArray.getResourceId( - R.styleable.mapbox_LocationComponent_mapbox_backgroundDrawable, -1)); + R.styleable.mapbox_LocationComponent_mapbox_backgroundDrawable, -1)); if (typedArray.hasValue(R.styleable.mapbox_LocationComponent_mapbox_backgroundTintColor)) { builder.backgroundTintColor(typedArray.getColor( - R.styleable.mapbox_LocationComponent_mapbox_backgroundTintColor, -1)); + R.styleable.mapbox_LocationComponent_mapbox_backgroundTintColor, -1)); } builder.foregroundDrawableStale(typedArray.getResourceId( - R.styleable.mapbox_LocationComponent_mapbox_foregroundDrawableStale, -1)); + R.styleable.mapbox_LocationComponent_mapbox_foregroundDrawableStale, -1)); if (typedArray.hasValue(R.styleable.mapbox_LocationComponent_mapbox_foregroundStaleTintColor)) { builder.foregroundStaleTintColor(typedArray.getColor( - R.styleable.mapbox_LocationComponent_mapbox_foregroundStaleTintColor, -1)); + R.styleable.mapbox_LocationComponent_mapbox_foregroundStaleTintColor, -1)); } builder.backgroundDrawableStale(typedArray.getResourceId( - R.styleable.mapbox_LocationComponent_mapbox_backgroundDrawableStale, -1)); + R.styleable.mapbox_LocationComponent_mapbox_backgroundDrawableStale, -1)); if (typedArray.hasValue(R.styleable.mapbox_LocationComponent_mapbox_backgroundStaleTintColor)) { builder.backgroundStaleTintColor(typedArray.getColor( - R.styleable.mapbox_LocationComponent_mapbox_backgroundStaleTintColor, -1)); + R.styleable.mapbox_LocationComponent_mapbox_backgroundStaleTintColor, -1)); } builder.bearingDrawable(typedArray.getResourceId( - R.styleable.mapbox_LocationComponent_mapbox_bearingDrawable, -1)); + R.styleable.mapbox_LocationComponent_mapbox_bearingDrawable, -1)); if (typedArray.hasValue(R.styleable.mapbox_LocationComponent_mapbox_bearingTintColor)) { builder.bearingTintColor(typedArray.getColor( - R.styleable.mapbox_LocationComponent_mapbox_bearingTintColor, -1)); + R.styleable.mapbox_LocationComponent_mapbox_bearingTintColor, -1)); } if (typedArray.hasValue(R.styleable.mapbox_LocationComponent_mapbox_enableStaleState)) { builder.enableStaleState(typedArray.getBoolean( - R.styleable.mapbox_LocationComponent_mapbox_enableStaleState, true)); + R.styleable.mapbox_LocationComponent_mapbox_enableStaleState, true)); } if (typedArray.hasValue(R.styleable.mapbox_LocationComponent_mapbox_staleStateTimeout)) { builder.staleStateTimeout(typedArray.getInteger( - R.styleable.mapbox_LocationComponent_mapbox_staleStateTimeout, (int) STALE_STATE_DELAY_MS)); + R.styleable.mapbox_LocationComponent_mapbox_staleStateTimeout, (int) STALE_STATE_DELAY_MS)); } builder.gpsDrawable(typedArray.getResourceId( - R.styleable.mapbox_LocationComponent_mapbox_gpsDrawable, -1)); + R.styleable.mapbox_LocationComponent_mapbox_gpsDrawable, -1)); float elevation = typedArray.getDimension( - R.styleable.mapbox_LocationComponent_mapbox_elevation, 0); + R.styleable.mapbox_LocationComponent_mapbox_elevation, 0); builder.accuracyColor(typedArray.getColor( - R.styleable.mapbox_LocationComponent_mapbox_accuracyColor, -1)); + R.styleable.mapbox_LocationComponent_mapbox_accuracyColor, -1)); builder.accuracyAlpha(typedArray.getFloat( - R.styleable.mapbox_LocationComponent_mapbox_accuracyAlpha, ACCURACY_ALPHA_DEFAULT)); + R.styleable.mapbox_LocationComponent_mapbox_accuracyAlpha, ACCURACY_ALPHA_DEFAULT)); builder.elevation(elevation); builder.trackingGesturesManagement(typedArray.getBoolean( - R.styleable.mapbox_LocationComponent_mapbox_trackingGesturesManagement, false)); + R.styleable.mapbox_LocationComponent_mapbox_trackingGesturesManagement, false)); builder.trackingInitialMoveThreshold(typedArray.getDimension( - R.styleable.mapbox_LocationComponent_mapbox_trackingInitialMoveThreshold, - context.getResources().getDimension(R.dimen.mapbox_locationComponentTrackingInitialMoveThreshold))); + R.styleable.mapbox_LocationComponent_mapbox_trackingInitialMoveThreshold, + context.getResources().getDimension(R.dimen.mapbox_locationComponentTrackingInitialMoveThreshold))); builder.trackingMultiFingerMoveThreshold(typedArray.getDimension( - R.styleable.mapbox_LocationComponent_mapbox_trackingMultiFingerMoveThreshold, - context.getResources().getDimension(R.dimen.mapbox_locationComponentTrackingMultiFingerMoveThreshold))); + R.styleable.mapbox_LocationComponent_mapbox_trackingMultiFingerMoveThreshold, + context.getResources().getDimension(R.dimen.mapbox_locationComponentTrackingMultiFingerMoveThreshold))); - builder.padding(new int[] { - typedArray.getInt(R.styleable.mapbox_LocationComponent_mapbox_iconPaddingLeft, 0), - typedArray.getInt(R.styleable.mapbox_LocationComponent_mapbox_iconPaddingTop, 0), - typedArray.getInt(R.styleable.mapbox_LocationComponent_mapbox_iconPaddingRight, 0), - typedArray.getInt(R.styleable.mapbox_LocationComponent_mapbox_iconPaddingBottom, 0), + builder.padding(new int[]{ + typedArray.getInt(R.styleable.mapbox_LocationComponent_mapbox_iconPaddingLeft, 0), + typedArray.getInt(R.styleable.mapbox_LocationComponent_mapbox_iconPaddingTop, 0), + typedArray.getInt(R.styleable.mapbox_LocationComponent_mapbox_iconPaddingRight, 0), + typedArray.getInt(R.styleable.mapbox_LocationComponent_mapbox_iconPaddingBottom, 0), }); builder.layerAbove( - typedArray.getString(R.styleable.mapbox_LocationComponent_mapbox_layer_above)); + typedArray.getString(R.styleable.mapbox_LocationComponent_mapbox_layer_above)); builder.layerBelow( - typedArray.getString(R.styleable.mapbox_LocationComponent_mapbox_layer_below)); + typedArray.getString(R.styleable.mapbox_LocationComponent_mapbox_layer_below)); float minScale = typedArray.getFloat( - R.styleable.mapbox_LocationComponent_mapbox_minZoomIconScale, MIN_ZOOM_ICON_SCALE_DEFAULT); + R.styleable.mapbox_LocationComponent_mapbox_minZoomIconScale, MIN_ZOOM_ICON_SCALE_DEFAULT); float maxScale = typedArray.getFloat( - R.styleable.mapbox_LocationComponent_mapbox_maxZoomIconScale, MAX_ZOOM_ICON_SCALE_DEFAULT); + R.styleable.mapbox_LocationComponent_mapbox_maxZoomIconScale, MAX_ZOOM_ICON_SCALE_DEFAULT); builder.minZoomIconScale(minScale); builder.maxZoomIconScale(maxScale); float trackingAnimationDurationMultiplier = typedArray.getFloat( - R.styleable.mapbox_LocationComponent_mapbox_trackingAnimationDurationMultiplier, - TRACKING_ANIMATION_DURATION_MULTIPLIER_DEFAULT + R.styleable.mapbox_LocationComponent_mapbox_trackingAnimationDurationMultiplier, + TRACKING_ANIMATION_DURATION_MULTIPLIER_DEFAULT ); builder.trackingAnimationDurationMultiplier(trackingAnimationDurationMultiplier); builder.compassAnimationEnabled = typedArray.getBoolean( - R.styleable.mapbox_LocationComponent_mapbox_compassAnimationEnabled, true + R.styleable.mapbox_LocationComponent_mapbox_compassAnimationEnabled, true ); builder.accuracyAnimationEnabled = typedArray.getBoolean( - R.styleable.mapbox_LocationComponent_mapbox_accuracyAnimationEnabled, true + R.styleable.mapbox_LocationComponent_mapbox_accuracyAnimationEnabled, true ); + builder.pulseEnabled = typedArray.getBoolean( + R.styleable.mapbox_LocationComponent_mapbox_pulsingLocationCircleEnabled, false + ); + + builder.pulseFadeEnabled = typedArray.getBoolean( + R.styleable.mapbox_LocationComponent_mapbox_pulsingLocationCircleFadeEnabled, true + ); + + if (typedArray.hasValue(R.styleable.mapbox_LocationComponent_mapbox_pulsingLocationCircleColor)) { + builder.pulseColor(typedArray.getColor( + R.styleable.mapbox_LocationComponent_mapbox_pulsingLocationCircleColor, + -1)); + } + + builder.pulseSingleDuration = typedArray.getFloat( + R.styleable.mapbox_LocationComponent_mapbox_pulsingLocationCircleDuration, CIRCLE_PULSING_DURATION_DEFAULT_MS + ); + + builder.pulseMaxRadius = typedArray.getFloat( + R.styleable.mapbox_LocationComponent_mapbox_pulsingLocationCircleRadius, CIRCLE_PULSING_MAX_RADIUS_DEFAULT + ); + + builder.pulseAlpha = typedArray.getFloat( + R.styleable.mapbox_LocationComponent_mapbox_pulsingLocationCircleAlpha, CIRCLE_PULSING_ALPHA_DEFAULT); + typedArray.recycle(); return builder.build(); @@ -330,7 +394,7 @@ public class LocationComponentOptions implements Parcelable { @NonNull public static Builder builder(@NonNull Context context) { return LocationComponentOptions.createFromAttributes(context, - R.style.mapbox_LocationComponent).toBuilder(); + R.style.mapbox_LocationComponent).toBuilder(); } /** @@ -740,42 +804,114 @@ 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 pulseEnabled; + } + + /** + * 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 pulseFadeEnabled() { + return pulseFadeEnabled; + } + + /** + * 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 pulseMaxRadius; + } + + /** + * The opacity of the LocationComponent's circle as it pulses. The expected range is + * 0 to 1. An opacity of 1 makes the layer fully visible. + * + * @return the current opacity of the LocationComponent's pulsing 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 + */ + @Nullable + public Interpolator pulseInterpolator() { + return pulseInterpolator; + } + @NonNull @Override public String toString() { return "LocationComponentOptions{" - + "accuracyAlpha=" + accuracyAlpha + ", " - + "accuracyColor=" + accuracyColor + ", " - + "backgroundDrawableStale=" + backgroundDrawableStale + ", " - + "backgroundStaleName=" + backgroundStaleName + ", " - + "foregroundDrawableStale=" + foregroundDrawableStale + ", " - + "foregroundStaleName=" + foregroundStaleName + ", " - + "gpsDrawable=" + gpsDrawable + ", " - + "gpsName=" + gpsName + ", " - + "foregroundDrawable=" + foregroundDrawable + ", " - + "foregroundName=" + foregroundName + ", " - + "backgroundDrawable=" + backgroundDrawable + ", " - + "backgroundName=" + backgroundName + ", " - + "bearingDrawable=" + bearingDrawable + ", " - + "bearingName=" + bearingName + ", " - + "bearingTintColor=" + bearingTintColor + ", " - + "foregroundTintColor=" + foregroundTintColor + ", " - + "backgroundTintColor=" + backgroundTintColor + ", " - + "foregroundStaleTintColor=" + foregroundStaleTintColor + ", " - + "backgroundStaleTintColor=" + backgroundStaleTintColor + ", " - + "elevation=" + elevation + ", " - + "enableStaleState=" + enableStaleState + ", " - + "staleStateTimeout=" + staleStateTimeout + ", " - + "padding=" + Arrays.toString(padding) + ", " - + "maxZoomIconScale=" + maxZoomIconScale + ", " - + "minZoomIconScale=" + minZoomIconScale + ", " - + "trackingGesturesManagement=" + trackingGesturesManagement + ", " - + "trackingInitialMoveThreshold=" + trackingInitialMoveThreshold + ", " - + "trackingMultiFingerMoveThreshold=" + trackingMultiFingerMoveThreshold + ", " - + "layerAbove=" + layerAbove - + "layerBelow=" + layerBelow - + "trackingAnimationDurationMultiplier=" + trackingAnimationDurationMultiplier - + "}"; + + "accuracyAlpha=" + accuracyAlpha + ", " + + "accuracyColor=" + accuracyColor + ", " + + "backgroundDrawableStale=" + backgroundDrawableStale + ", " + + "backgroundStaleName=" + backgroundStaleName + ", " + + "foregroundDrawableStale=" + foregroundDrawableStale + ", " + + "foregroundStaleName=" + foregroundStaleName + ", " + + "gpsDrawable=" + gpsDrawable + ", " + + "gpsName=" + gpsName + ", " + + "foregroundDrawable=" + foregroundDrawable + ", " + + "foregroundName=" + foregroundName + ", " + + "backgroundDrawable=" + backgroundDrawable + ", " + + "backgroundName=" + backgroundName + ", " + + "bearingDrawable=" + bearingDrawable + ", " + + "bearingName=" + bearingName + ", " + + "bearingTintColor=" + bearingTintColor + ", " + + "foregroundTintColor=" + foregroundTintColor + ", " + + "backgroundTintColor=" + backgroundTintColor + ", " + + "foregroundStaleTintColor=" + foregroundStaleTintColor + ", " + + "backgroundStaleTintColor=" + backgroundStaleTintColor + ", " + + "elevation=" + elevation + ", " + + "enableStaleState=" + enableStaleState + ", " + + "staleStateTimeout=" + staleStateTimeout + ", " + + "padding=" + Arrays.toString(padding) + ", " + + "maxZoomIconScale=" + maxZoomIconScale + ", " + + "minZoomIconScale=" + minZoomIconScale + ", " + + "trackingGesturesManagement=" + trackingGesturesManagement + ", " + + "trackingInitialMoveThreshold=" + trackingInitialMoveThreshold + ", " + + "trackingMultiFingerMoveThreshold=" + trackingMultiFingerMoveThreshold + ", " + + "layerAbove=" + layerAbove + + "layerBelow=" + layerBelow + + "trackingAnimationDurationMultiplier=" + trackingAnimationDurationMultiplier + + "pulseEnabled=" + pulseEnabled + + "pulseFadeEnabled=" + pulseFadeEnabled + + "pulseColor=" + pulseColor + + "pulseSingleDuration=" + pulseSingleDuration + + "pulseMaxRadius=" + pulseMaxRadius + + "pulseAlpha=" + pulseAlpha + + "}"; } @Override @@ -847,11 +983,11 @@ public class LocationComponentOptions implements Parcelable { return false; } if (backgroundStaleName != null ? !backgroundStaleName.equals(options.backgroundStaleName) : - options.backgroundStaleName != null) { + options.backgroundStaleName != null) { return false; } if (foregroundStaleName != null ? !foregroundStaleName.equals(options.foregroundStaleName) : - options.foregroundStaleName != null) { + options.foregroundStaleName != null) { return false; } if (gpsName != null ? !gpsName.equals(options.gpsName) : options.gpsName != null) { @@ -867,23 +1003,23 @@ public class LocationComponentOptions implements Parcelable { return false; } if (bearingTintColor != null ? !bearingTintColor.equals(options.bearingTintColor) : - options.bearingTintColor != null) { + options.bearingTintColor != null) { return false; } if (foregroundTintColor != null ? !foregroundTintColor.equals(options.foregroundTintColor) : - options.foregroundTintColor != null) { + options.foregroundTintColor != null) { return false; } if (backgroundTintColor != null ? !backgroundTintColor.equals(options.backgroundTintColor) : - options.backgroundTintColor != null) { + options.backgroundTintColor != null) { return false; } if (foregroundStaleTintColor != null ? !foregroundStaleTintColor.equals(options.foregroundStaleTintColor) : - options.foregroundStaleTintColor != null) { + options.foregroundStaleTintColor != null) { return false; } if (backgroundStaleTintColor != null ? !backgroundStaleTintColor.equals(options.backgroundStaleTintColor) : - options.backgroundStaleTintColor != null) { + options.backgroundStaleTintColor != null) { return false; } if (!Arrays.equals(padding, options.padding)) { @@ -892,6 +1028,31 @@ public class LocationComponentOptions implements Parcelable { if (layerAbove != null ? !layerAbove.equals(options.layerAbove) : options.layerAbove != null) { return false; } + + if (pulseEnabled != options.pulseEnabled) { + return false; + } + + if (pulseFadeEnabled != options.pulseFadeEnabled) { + 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.pulseMaxRadius, pulseMaxRadius) != 0) { + return false; + } + + if (Float.compare(options.pulseAlpha, pulseAlpha) != 0) { + return false; + } + return layerBelow != null ? layerBelow.equals(options.layerBelow) : options.layerBelow == null; } @@ -924,57 +1085,70 @@ public class LocationComponentOptions implements Parcelable { result = 31 * result + (minZoomIconScale != +0.0f ? Float.floatToIntBits(minZoomIconScale) : 0); result = 31 * result + (trackingGesturesManagement ? 1 : 0); result = 31 * result + (trackingInitialMoveThreshold != +0.0f - ? Float.floatToIntBits(trackingInitialMoveThreshold) : 0); + ? Float.floatToIntBits(trackingInitialMoveThreshold) : 0); result = 31 * result + (trackingMultiFingerMoveThreshold != +0.0f - ? Float.floatToIntBits(trackingMultiFingerMoveThreshold) : 0); + ? Float.floatToIntBits(trackingMultiFingerMoveThreshold) : 0); result = 31 * result + (layerAbove != null ? layerAbove.hashCode() : 0); result = 31 * result + (layerBelow != null ? layerBelow.hashCode() : 0); result = 31 * result + (trackingAnimationDurationMultiplier != +0.0f - ? Float.floatToIntBits(trackingAnimationDurationMultiplier) : 0); + ? Float.floatToIntBits(trackingAnimationDurationMultiplier) : 0); result = 31 * result + (compassAnimationEnabled ? 1 : 0); result = 31 * result + (accuracyAnimationEnabled ? 1 : 0); + result = 31 * result + (pulseEnabled ? 1 : 0); + result = 31 * result + (pulseFadeEnabled ? 1 : 0); + result = 31 * result + (pulseColor != null ? pulseColor.hashCode() : 0); + result = 31 * result + (pulseSingleDuration != +0.0f ? Float.floatToIntBits(pulseSingleDuration) : 0); + result = 31 * result + (pulseMaxRadius != +0.0f ? Float.floatToIntBits(pulseMaxRadius) : 0); + result = 31 * result + (pulseAlpha != +0.0f ? Float.floatToIntBits(pulseAlpha) : 0); return result; } public static final Parcelable.Creator CREATOR = - new Parcelable.Creator() { + new Parcelable.Creator() { @Override - public LocationComponentOptions createFromParcel(Parcel in) { + public LocationComponentOptions createFromParcel(Parcel in) { return new LocationComponentOptions( - in.readFloat(), - in.readInt(), - in.readInt(), - in.readInt() == 0 ? in.readString() : null, - in.readInt(), - in.readInt() == 0 ? in.readString() : null, - in.readInt(), - in.readInt() == 0 ? in.readString() : null, - in.readInt(), - in.readInt() == 0 ? in.readString() : null, - in.readInt(), - in.readInt() == 0 ? in.readString() : null, - in.readInt(), - in.readInt() == 0 ? in.readString() : null, - in.readInt() == 0 ? in.readInt() : null, - in.readInt() == 0 ? in.readInt() : null, - in.readInt() == 0 ? in.readInt() : null, - in.readInt() == 0 ? in.readInt() : null, - in.readInt() == 0 ? in.readInt() : null, - in.readFloat(), - in.readInt() == 1, - in.readLong(), - in.createIntArray(), - in.readFloat(), - in.readFloat(), - in.readInt() == 1, - in.readFloat(), - in.readFloat(), - in.readString(), - in.readString(), - in.readFloat(), - in.readInt() == 1, - in.readInt() == 1 - ); + in.readFloat(), + in.readInt(), + in.readInt(), + in.readInt() == 0 ? in.readString() : null, + in.readInt(), + in.readInt() == 0 ? in.readString() : null, + in.readInt(), + in.readInt() == 0 ? in.readString() : null, + in.readInt(), + in.readInt() == 0 ? in.readString() : null, + in.readInt(), + in.readInt() == 0 ? in.readString() : null, + in.readInt(), + in.readInt() == 0 ? in.readString() : null, + in.readInt() == 0 ? in.readInt() : null, + in.readInt() == 0 ? in.readInt() : null, + in.readInt() == 0 ? in.readInt() : null, + in.readInt() == 0 ? in.readInt() : null, + in.readInt() == 0 ? in.readInt() : null, + in.readFloat(), + in.readInt() == 1, + in.readLong(), + in.createIntArray(), + in.readFloat(), + in.readFloat(), + in.readInt() == 1, + in.readFloat(), + in.readFloat(), + in.readString(), + in.readString(), + in.readFloat(), + in.readInt() == 1, + in.readInt() == 1, + in.readInt() == 1, + in.readInt() == 1, + in.readInt() == 0 ? in.readInt() : null, + in.readFloat(), + in.readFloat(), + in.readFloat(), + null + ); } @Override @@ -1073,6 +1247,17 @@ 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(pulseFadeEnabled() ? 1 : 0); + if (pulseColor() == null) { + dest.writeInt(1); + } else { + dest.writeInt(0); + dest.writeInt(pulseColor()); + } + dest.writeFloat(pulseSingleDuration()); + dest.writeFloat(pulseMaxRadius()); + dest.writeFloat(pulseAlpha()); } @Override @@ -1095,19 +1280,45 @@ public class LocationComponentOptions implements Parcelable { LocationComponentOptions locationComponentOptions = autoBuild(); if (locationComponentOptions.accuracyAlpha() < 0 || locationComponentOptions.accuracyAlpha() > 1) { throw new IllegalArgumentException( - "Accuracy alpha value must be between 0.0 and 1.0."); + "Accuracy alpha value must be between 0.0 and 1.0."); } if (locationComponentOptions.elevation() < 0f) { throw new IllegalArgumentException("Invalid shadow size " - + locationComponentOptions.elevation() + ". Must be >= 0"); + + locationComponentOptions.elevation() + ". Must be >= 0"); } if (locationComponentOptions.layerAbove() != null && locationComponentOptions.layerBelow() != null) { throw new IllegalArgumentException("You cannot set both layerAbove and layerBelow options." - + "Choose one or the other."); + + " Choose one or the other."); } + if (locationComponentOptions.pulseEnabled() == null) { + String pulsingSetupError = ""; + if (locationComponentOptions.pulseFadeEnabled() != null) { + pulsingSetupError += " pulseFadeEnabled"; + } + if (locationComponentOptions.pulseColor() != null) { + pulsingSetupError += " pulseColor"; + } + if (locationComponentOptions.pulseSingleDuration() > 0) { + pulsingSetupError += " pulseSingleDuration"; + } + if (locationComponentOptions.pulseMaxRadius() > 0) { + pulsingSetupError += " pulseMaxRadius"; + } + if (locationComponentOptions.pulseAlpha() >= 0 && locationComponentOptions.pulseAlpha() <= 1) { + pulsingSetupError += " pulseAlpha"; + } + if (locationComponentOptions.pulseInterpolator() != null) { + pulsingSetupError += " pulseInterpolator"; + } + 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 +1367,14 @@ public class LocationComponentOptions implements Parcelable { private Float trackingAnimationDurationMultiplier; private Boolean compassAnimationEnabled; private Boolean accuracyAnimationEnabled; + private Boolean pulseEnabled; + private Boolean pulseFadeEnabled; + private int pulseColor; + private float pulseSingleDuration; + private float pulseMaxRadius; + private float pulseAlpha; + @Nullable + private Interpolator pulseInterpolator; Builder() { } @@ -1194,6 +1413,13 @@ public class LocationComponentOptions implements Parcelable { this.trackingAnimationDurationMultiplier = source.trackingAnimationDurationMultiplier(); this.compassAnimationEnabled = source.compassAnimationEnabled(); this.accuracyAnimationEnabled = source.accuracyAnimationEnabled(); + this.pulseEnabled = source.pulseEnabled; + this.pulseFadeEnabled = source.pulseFadeEnabled; + this.pulseColor = source.pulseColor; + this.pulseSingleDuration = source.pulseSingleDuration; + this.pulseMaxRadius = source.pulseMaxRadius; + this.pulseAlpha = source.pulseAlpha; + this.pulseInterpolator = source.pulseInterpolator; } /** @@ -1653,7 +1879,7 @@ public class LocationComponentOptions implements Parcelable { */ @NonNull public LocationComponentOptions.Builder trackingAnimationDurationMultiplier( - float trackingAnimationDurationMultiplier) { + float trackingAnimationDurationMultiplier) { this.trackingAnimationDurationMultiplier = trackingAnimationDurationMultiplier; return this; } @@ -1674,11 +1900,85 @@ 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 pulseEnabled(boolean pulseEnabled) { + this.pulseEnabled = pulseEnabled; + 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 pulseFadeEnabled(boolean pulseFadeEnabled) { + this.pulseFadeEnabled = pulseFadeEnabled; + return this; + } + + /** + * Sets the color of the LocationComponent's pulsing circle. + * + * @return the current set color of the circle + */ + public LocationComponentOptions.Builder pulseColor(@ColorInt int pulseColor) { + this.pulseColor = pulseColor; + 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 pulseSingleDuration(float pulseSingleDuration) { + this.pulseSingleDuration = pulseSingleDuration; + 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 pulseMaxRadius(float pulseMaxRadius) { + this.pulseMaxRadius = pulseMaxRadius; + return this; + } + + /** + * Sets the opacity of the LocationComponent's pulsing circle. The expected range is + * 0 to 1. An opacity of 1 makes the layer fully visible. + * + * @return the current opacity of the LocationComponent's pulsing circle + */ + public LocationComponentOptions.Builder pulseAlpha(float pulseAlpha) { + this.pulseAlpha = pulseAlpha; + return this; + } + + /** + * Sets the pulsing circle's interpolator animation. + * + * @param pulseInterpolator the type of Android-system interpolator to use when + * creating the pulsing animation + * @return a String which represents the interpolator animation that the pulsing circle will use. + */ + public LocationComponentOptions.Builder pulseInterpolator(Interpolator pulseInterpolator) { + this.pulseInterpolator = pulseInterpolator; + return this; + } + @Nullable LocationComponentOptions autoBuild() { String missing = ""; @@ -1740,39 +2040,46 @@ public class LocationComponentOptions implements Parcelable { throw new IllegalStateException("Missing required properties:" + missing); } return new LocationComponentOptions( - this.accuracyAlpha, - this.accuracyColor, - this.backgroundDrawableStale, - this.backgroundStaleName, - this.foregroundDrawableStale, - this.foregroundStaleName, - this.gpsDrawable, - this.gpsName, - this.foregroundDrawable, - this.foregroundName, - this.backgroundDrawable, - this.backgroundName, - this.bearingDrawable, - this.bearingName, - this.bearingTintColor, - this.foregroundTintColor, - this.backgroundTintColor, - this.foregroundStaleTintColor, - this.backgroundStaleTintColor, - this.elevation, - this.enableStaleState, - this.staleStateTimeout, - this.padding, - this.maxZoomIconScale, - this.minZoomIconScale, - trackingGesturesManagement, - this.trackingInitialMoveThreshold, - this.trackingMultiFingerMoveThreshold, - this.layerAbove, - this.layerBelow, - this.trackingAnimationDurationMultiplier, - this.compassAnimationEnabled, - this.accuracyAnimationEnabled); + this.accuracyAlpha, + this.accuracyColor, + this.backgroundDrawableStale, + this.backgroundStaleName, + this.foregroundDrawableStale, + this.foregroundStaleName, + this.gpsDrawable, + this.gpsName, + this.foregroundDrawable, + this.foregroundName, + this.backgroundDrawable, + this.backgroundName, + this.bearingDrawable, + this.bearingName, + this.bearingTintColor, + this.foregroundTintColor, + this.backgroundTintColor, + this.foregroundStaleTintColor, + this.backgroundStaleTintColor, + this.elevation, + this.enableStaleState, + this.staleStateTimeout, + this.padding, + this.maxZoomIconScale, + this.minZoomIconScale, + trackingGesturesManagement, + this.trackingInitialMoveThreshold, + this.trackingMultiFingerMoveThreshold, + this.layerAbove, + this.layerBelow, + this.trackingAnimationDurationMultiplier, + this.compassAnimationEnabled, + this.accuracyAnimationEnabled, + this.pulseEnabled, + this.pulseFadeEnabled, + this.pulseColor, + this.pulseSingleDuration, + this.pulseMaxRadius, + this.pulseAlpha, + this.pulseInterpolator); } } } diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationLayerController.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationLayerController.java index 656191ddb..d2f3c478e 100644 --- a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationLayerController.java +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationLayerController.java @@ -98,6 +98,7 @@ final class LocationLayerController { styleBitmaps(options); locationLayerRenderer.styleAccuracy(options.accuracyAlpha(), options.accuracyColor()); styleScaling(options); + locationLayerRenderer.stylePulsingCircle(options); determineIconsSource(options); if (!isHidden) { @@ -258,7 +259,7 @@ final class LocationLayerController { public void onNewAnimationValue(LatLng value) { locationLayerRenderer.setLatLng(value); } - }; + }; private final MapboxAnimator.AnimationsValueChangeListener gpsBearingValueListener = new MapboxAnimator.AnimationsValueChangeListener() { @@ -266,7 +267,7 @@ final class LocationLayerController { public void onNewAnimationValue(Float value) { locationLayerRenderer.setGpsBearing(value); } - }; + }; private final MapboxAnimator.AnimationsValueChangeListener compassBearingValueListener = new MapboxAnimator.AnimationsValueChangeListener() { @@ -274,7 +275,7 @@ final class LocationLayerController { public void onNewAnimationValue(Float value) { locationLayerRenderer.setCompassBearing(value); } - }; + }; private final MapboxAnimator.AnimationsValueChangeListener accuracyValueListener = new MapboxAnimator.AnimationsValueChangeListener() { @@ -282,7 +283,22 @@ final class LocationLayerController { public void onNewAnimationValue(Float value) { locationLayerRenderer.setAccuracyRadius(value); } - }; + }; + + /** + * The listener that handles the updating of the pulsing circle's radius and opacity. + */ + private final MapboxAnimator.AnimationsValueChangeListener pulsingCircleRadiusListener = + new MapboxAnimator.AnimationsValueChangeListener() { + @Override + public void onNewAnimationValue(Float newPulseRadiusValue) { + Float newPulseOpacityValue = null; + if (options.pulseFadeEnabled()) { + newPulseOpacityValue = (float) 1 - ((newPulseRadiusValue / 100) * 3); + } + locationLayerRenderer.updatePulsingUi(newPulseRadiusValue, newPulseOpacityValue); + } + }; Set getAnimationListeners() { Set holders = new HashSet<>(); @@ -299,6 +315,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; } @@ -311,4 +331,8 @@ final class LocationLayerController { void cameraTiltUpdated(double tilt) { locationLayerRenderer.cameraTiltUpdated(tilt); } + + void adjustPulsingCircleLayerVisibility(boolean visible) { + locationLayerRenderer.adjustPulsingCircleLayerVisibility(visible); + } } \ No newline at end of file diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationLayerRenderer.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationLayerRenderer.java index 01d4d1272..13333372e 100644 --- a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationLayerRenderer.java +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationLayerRenderer.java @@ -38,6 +38,12 @@ interface LocationLayerRenderer { void setLocationStale(boolean isStale, int renderMode); + void adjustPulsingCircleLayerVisibility(boolean visible); + + void stylePulsingCircle(LocationComponentOptions options); + + void updatePulsingUi(float radius, @Nullable Float opacity); + void updateIconIds(String foregroundIconString, String foregroundStaleIconString, String backgroundIconString, String backgroundStaleIconString, String bearingIconString); diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimator.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimator.java index 694594fa6..82853ef8c 100644 --- a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimator.java +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimator.java @@ -28,7 +28,8 @@ abstract class MapboxAnimator extends ValueAnimator implements ValueAnimator. ANIMATOR_CAMERA_COMPASS_BEARING, ANIMATOR_LAYER_ACCURACY, ANIMATOR_ZOOM, - ANIMATOR_TILT + ANIMATOR_TILT, + ANIMATOR_PULSING_CIRCLE }) @interface Type { } @@ -42,6 +43,7 @@ abstract class MapboxAnimator 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 updateListener; private final K target; @@ -68,6 +70,15 @@ abstract class MapboxAnimator extends ValueAnimator implements ValueAnimator. addListener(new AnimatorListener()); } + public MapboxAnimator(AnimationsValueChangeListener 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/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimatorProvider.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimatorProvider.java index c1109b959..16324f18a 100644 --- a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimatorProvider.java +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimatorProvider.java @@ -1,5 +1,8 @@ package com.mapbox.mapboxsdk.location; +import android.animation.ValueAnimator; +import android.view.animation.Interpolator; + import androidx.annotation.Nullable; import com.mapbox.mapboxsdk.geometry.LatLng; @@ -35,4 +38,31 @@ final class MapboxAnimatorProvider { @Nullable MapboxMap.CancelableCallback cancelableCallback) { return new MapboxCameraAnimatorAdapter(values, 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 pulseInterpolator 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, + Interpolator pulseInterpolator) { + PulsingLocationCircleAnimator pulsingLocationCircleAnimator = + new PulsingLocationCircleAnimator(updateListener, maxAnimationFps, pulseMaxRadius); + pulsingLocationCircleAnimator.setDuration((long) pulseSingleDuration); + pulsingLocationCircleAnimator.setRepeatMode(ValueAnimator.RESTART); + pulsingLocationCircleAnimator.setRepeatCount(ValueAnimator.INFINITE); + pulsingLocationCircleAnimator.setInterpolator(pulseInterpolator); + return pulsingLocationCircleAnimator; + } } diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimatorSetProvider.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimatorSetProvider.java index af7a73644..40bdf67be 100644 --- a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimatorSetProvider.java +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/MapboxAnimatorSetProvider.java @@ -2,9 +2,10 @@ package com.mapbox.mapboxsdk.location; import android.animation.Animator; import android.animation.AnimatorSet; -import androidx.annotation.NonNull; import android.view.animation.Interpolator; +import androidx.annotation.NonNull; + import java.util.List; class MapboxAnimatorSetProvider { diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/PulsingLocationCircleAnimator.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/PulsingLocationCircleAnimator.java new file mode 100644 index 000000000..1f09f6c4d --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/PulsingLocationCircleAnimator.java @@ -0,0 +1,19 @@ +package com.mapbox.mapboxsdk.location; + +/** + * 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(new Float[]{0f, circleMaxRadius}, updateListener, maxAnimationFps); + } +} diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/SymbolLocationLayerRenderer.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/SymbolLocationLayerRenderer.java index ae722b3d2..bd6151617 100644 --- a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/SymbolLocationLayerRenderer.java +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/SymbolLocationLayerRenderer.java @@ -41,11 +41,19 @@ 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.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.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; 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; @@ -84,6 +92,7 @@ final class SymbolLocationLayerRenderer implements LocationLayerRenderer { addSymbolLayer(BACKGROUND_LAYER, FOREGROUND_LAYER); addSymbolLayer(SHADOW_LAYER, BACKGROUND_LAYER); addAccuracyLayer(); + addPulsingCircleLayerToMap(); } @Override @@ -244,6 +253,42 @@ final class SymbolLocationLayerRenderer implements LocationLayerRenderer { } } + /** + * Adjust the visibility of the pulsing LocationComponent circle. + */ + @Override + public void adjustPulsingCircleLayerVisibility(boolean visible) { + setLayerVisibility(PULSING_CIRCLE_LAYER, visible); + } + + /** + * Adjust the the pulsing LocationComponent circle based on the set options. + */ + @Override + public 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)) + ); + } + } + + /** + * Adjust the visual appearance of the pulsing LocationComponent circle. + */ + @Override + public void updatePulsingUi(float radius, @Nullable Float opacity) { + locationFeature.addNumberProperty(PROPERTY_PULSING_RADIUS, radius); + if (opacity != null) { + locationFeature.addNumberProperty(PROPERTY_PULSING_OPACITY, opacity); + } + refreshSource(); + } + private void addSymbolLayer(@NonNull String layerId, @NonNull String beforeLayerId) { Layer layer = layerSourceProvider.generateLayer(layerId); addLayerToMap(layer, beforeLayerId); @@ -254,6 +299,14 @@ final class SymbolLocationLayerRenderer implements LocationLayerRenderer { addLayerToMap(accuracyLayer, BACKGROUND_LAYER); } + /** + * Add the pulsing LocationComponent circle to the map for future use, if need be. + */ + private void addPulsingCircleLayerToMap() { + Layer pulsingCircleLayer = layerSourceProvider.generatePulsingCircleLayer(); + addLayerToMap(pulsingCircleLayer, ACCURACY_LAYER); + } + private void addLayerToMap(Layer layer, @NonNull String idBelowLayer) { style.addLayerBelow(layer, idBelowLayer); layerSet.add(layer.getId()); diff --git a/MapboxGLAndroidSDK/src/main/res-public/values/public.xml b/MapboxGLAndroidSDK/src/main/res-public/values/public.xml index b79a5494d..a5e759f6c 100644 --- a/MapboxGLAndroidSDK/src/main/res-public/values/public.xml +++ b/MapboxGLAndroidSDK/src/main/res-public/values/public.xml @@ -165,4 +165,13 @@ + + + + + + + + + diff --git a/MapboxGLAndroidSDK/src/main/res/values/attrs.xml b/MapboxGLAndroidSDK/src/main/res/values/attrs.xml index 2b9022c96..4acf3301c 100644 --- a/MapboxGLAndroidSDK/src/main/res/values/attrs.xml +++ b/MapboxGLAndroidSDK/src/main/res/values/attrs.xml @@ -188,5 +188,14 @@ + + + + + + + + + diff --git a/MapboxGLAndroidSDK/src/main/res/values/styles.xml b/MapboxGLAndroidSDK/src/main/res/values/styles.xml index c6c2d3fc7..b38b3758c 100644 --- a/MapboxGLAndroidSDK/src/main/res/values/styles.xml +++ b/MapboxGLAndroidSDK/src/main/res/values/styles.xml @@ -33,5 +33,14 @@ @dimen/mapbox_locationComponentTrackingInitialMoveThreshold @dimen/mapbox_locationComponentTrackingMultiFingerMoveThreshold + + false + true + @color/mapbox_location_layer_blue + 2300 + 35 + 0.4 + decelerate + \ No newline at end of file diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinatorTest.kt b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinatorTest.kt index 7c4feb83b..bf479c973 100644 --- a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinatorTest.kt +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinatorTest.kt @@ -35,7 +35,9 @@ class LocationAnimatorCoordinatorTest { @Before fun setUp() { - locationAnimatorCoordinator = LocationAnimatorCoordinator(projection, animatorSetProvider, animatorProvider) + locationAnimatorCoordinator = LocationAnimatorCoordinator(projection, + animatorSetProvider, + animatorProvider) configureAnimatorProvider() every { projection.getMetersPerPixelAtLatitude(any()) } answers { 1.0 } every { animatorSetProvider.startAnimation(any(), any(), any()) } answers {} diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationComponentActivationOptionsTest.java b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationComponentActivationOptionsTest.java index d5256735c..9f4648468 100644 --- a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationComponentActivationOptionsTest.java +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationComponentActivationOptionsTest.java @@ -3,6 +3,9 @@ package com.mapbox.mapboxsdk.location; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; +import android.graphics.Color; +import android.view.animation.LinearInterpolator; + import androidx.annotation.NonNull; import com.mapbox.mapboxsdk.R; @@ -66,6 +69,44 @@ public class LocationComponentActivationOptionsTest { assertNotNull(locationComponentActivationOptions); } + @Test + public void sanityWithDefaultPulsingCircle() throws Exception { + when(style.isFullyLoaded()).thenReturn(true); + + LocationComponentOptions locationComponentOptions = LocationComponentOptions.builder(context) + .accuracyAlpha(0.5f) + .pulseEnabled(true) + .build(); + assertNotNull(locationComponentOptions); + + LocationComponentActivationOptions locationComponentActivationOptions = + LocationComponentActivationOptions.builder(context, style) + .locationComponentOptions(locationComponentOptions) + .useDefaultLocationEngine(true) + .build(); + assertNotNull(locationComponentActivationOptions); + } + + @Test + public void sanityWithCustomizedPulsingCircle() throws Exception { + when(style.isFullyLoaded()).thenReturn(true); + + LocationComponentOptions locationComponentOptions = LocationComponentOptions.builder(context) + .accuracyAlpha(0.5f) + .pulseEnabled(true) + .pulseColor(Color.RED) + .pulseInterpolator(new LinearInterpolator()) + .build(); + assertNotNull(locationComponentOptions); + + LocationComponentActivationOptions locationComponentActivationOptions = + LocationComponentActivationOptions.builder(context, style) + .locationComponentOptions(locationComponentOptions) + .useDefaultLocationEngine(true) + .build(); + assertNotNull(locationComponentActivationOptions); + } + @Test public void includingBothStyleResAndComponentOptions_causesExceptionToBeThrown() throws Exception { diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationComponentOptionsTest.java b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationComponentOptionsTest.java index 6e454a7f7..ccf968183 100644 --- a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationComponentOptionsTest.java +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationComponentOptionsTest.java @@ -72,7 +72,7 @@ public class LocationComponentOptionsTest { public void passingBothLayerPositionOptions_throwsException() throws Exception { thrown.expect(IllegalArgumentException.class); thrown.expectMessage("You cannot set both layerAbove and layerBelow options." - + "Choose one or the other."); + + " Choose one or the other."); LocationComponentOptions.builder(context) .layerAbove("above") .layerBelow("below") diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationLayerControllerTest.java b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationLayerControllerTest.java index 40726acbe..1d5ebb124 100644 --- a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationLayerControllerTest.java +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationLayerControllerTest.java @@ -37,6 +37,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; @@ -167,6 +168,23 @@ public class LocationLayerControllerTest { verify(style).addLayerBelow(accuracyLayer, BACKGROUND_LAYER); } + @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, false); + + verify(style).addLayerBelow(pulsingCircleLayer, ACCURACY_LAYER); + } + @Test public void onInitialization_numberOfCachedLayerIdsIsConstant() { OnRenderModeChangedListener internalRenderModeChangedListener = mock(OnRenderModeChangedListener.class); @@ -184,7 +202,7 @@ public class LocationLayerControllerTest { controller.initializeComponents(mapboxMap.getStyle(), options); - assertEquals(5, layerSet.size()); + assertEquals(6, layerSet.size()); } @Test @@ -402,7 +420,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 @@ -424,7 +442,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.any()); + verify(style, times(5)).addLayerBelow(any(Layer.class), Mockito.any()); } @Test @@ -726,6 +744,9 @@ 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/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/location/LocationLayerControllerTest.kt b/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/location/LocationLayerControllerTest.kt index 03c395a59..6700bda76 100644 --- a/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/location/LocationLayerControllerTest.kt +++ b/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 androidx.test.annotation.UiThreadTest import androidx.test.espresso.Espresso.onView @@ -123,11 +124,49 @@ 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) + .pulseEnabled(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)) + } + } + } + @Test fun renderModeCompass_bearingLayersDoGetAdded() { val componentAction = object : LocationComponentAction.OnPerformLocationComponentAction { @@ -159,6 +198,137 @@ class LocationLayerControllerTest : EspressoTest() { executeComponentTest(componentAction) } + @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) + .pulseEnabled(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) + + 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) + .pulseEnabled(true) + .pulseColor(Color.RED) + .build()) + + component.applyStyle(LocationComponentOptions.builder(context) + .pulseEnabled(true) + .pulseColor(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) + .pulseEnabled(true) + .pulseSingleDuration(8000f) + .build()) + + component.applyStyle(LocationComponentOptions.builder(context) + .pulseEnabled(true) + .pulseSingleDuration(400f) + .build()) + + mapboxMap.style.apply { + assertThat(component.locationComponentOptions.pulseSingleDuration(), `is`(400f)) + } + } + } + executeComponentTest(componentAction) + } + @Test fun renderModeGps_navigationLayersDoGetAdded() { val componentAction = object : LocationComponentAction.OnPerformLocationComponentAction { diff --git a/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/location/utils/StyleChangeIdlingResource.kt b/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/location/utils/StyleChangeIdlingResource.kt index 43e6d55f6..12bcfc78f 100644 --- a/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/location/utils/StyleChangeIdlingResource.kt +++ b/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/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml b/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml index ba26ee8d6..5077894dd 100644 --- a/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml +++ b/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml @@ -1015,6 +1015,28 @@ android:name="android.support.PARENT_ACTIVITY" android:value=".activity.FeatureOverviewActivity" /> + + + + + + + + { + locationComponent = mapboxMap.getLocationComponent(); + + LocationComponentOptions locationComponentOptions = + LocationComponentOptions.builder(BasicLocationPulsingCircleActivity.this) + .pulseEnabled(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) { + loadNewStyle(); + 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_stop_pulsing) { + locationComponent.applyStyle(LocationComponentOptions.builder( + BasicLocationPulsingCircleActivity.this) + .pulseEnabled(false) + .build()); + return true; + } else if (id == R.id.action_start_pulsing) { + locationComponent.applyStyle(LocationComponentOptions.builder( + BasicLocationPulsingCircleActivity.this) + .pulseEnabled(true) + .build()); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void loadNewStyle() { + mapboxMap.setStyle(new Style.Builder().fromUri(getNextStyle())); + } + + private void checkPermissions() { + if (PermissionsManager.areLocationPermissionsGranted(this)) { + mapView.getMapAsync(this); + } else { + permissionsManager = new PermissionsManager(new PermissionsListener() { + @Override + public void onExplanationNeeded(List 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/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/location/CustomizedLocationPulsingCircleActivity.java b/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/location/CustomizedLocationPulsingCircleActivity.java new file mode 100644 index 000000000..0125eded6 --- /dev/null +++ b/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/location/CustomizedLocationPulsingCircleActivity.java @@ -0,0 +1,360 @@ +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.view.Menu; +import android.view.MenuItem; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; +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.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; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.ListPopupWindow; + +import static com.mapbox.mapboxsdk.testapp.activity.location.Utils.getNextStyle; + +/** + * This activity shows how to customize the LocationComponent's pulsing circle. + */ +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 Interpolator DEFAULT_LOCATION_CIRCLE_INTERPOLATOR_PULSE_MODE + = new DecelerateInterpolator(); + 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 LAYER_BELOW_ID = "waterway-label"; + + 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( + LOCATION_CIRCLE_PULSE_COLOR, + LOCATION_CIRCLE_PULSE_DURATION) + .pulseEnabled(true) + .build(); + + LocationComponentActivationOptions locationComponentActivationOptions = + buildLocationComponentActivationOptions(style,locationComponentOptions); + + locationComponent.activateLocationComponent(locationComponentActivationOptions); + locationComponent.setLocationComponentEnabled(true); + locationComponent.setCameraMode(CameraMode.TRACKING); + locationComponent.forceLocationUpdate(lastLocation); + }); + } + + private LocationComponentOptions.Builder buildLocationComponentOptions(int pulsingCircleColor, + float pulsingCircleDuration + ) { + currentPulseDuration = pulsingCircleDuration; + return LocationComponentOptions.builder(this) + .layerBelow(LAYER_BELOW_ID) + .pulseFadeEnabled(DEFAULT_LOCATION_CIRCLE_PULSE_FADE_MODE) + .pulseInterpolator(DEFAULT_LOCATION_CIRCLE_INTERPOLATOR_PULSE_MODE) + .pulseColor(pulsingCircleColor) + .pulseAlpha(DEFAULT_LOCATION_CIRCLE_PULSE_ALPHA) + .pulseSingleDuration(pulsingCircleDuration) + .pulseMaxRadius(DEFAULT_LOCATION_CIRCLE_PULSE_RADIUS); + } + + @SuppressLint("MissingPermission") + private void setNewLocationComponentOptions(float newPulsingDuration, + int newPulsingColor) { + mapboxMap.getStyle(style -> locationComponent.applyStyle( + buildLocationComponentOptions( + newPulsingColor, + newPulsingDuration) + .pulseEnabled(true) + .build())); + } + + 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) { + loadNewStyle(); + 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_stop_pulsing) { + locationComponent.applyStyle(LocationComponentOptions.builder( + CustomizedLocationPulsingCircleActivity.this) + .pulseEnabled(false) + .build()); + return true; + } else if (id == R.id.action_start_pulsing) { + locationComponent.applyStyle(buildLocationComponentOptions( + LOCATION_CIRCLE_PULSE_COLOR, + LOCATION_CIRCLE_PULSE_DURATION) + .pulseEnabled(true) + .build()); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void loadNewStyle() { + mapboxMap.setStyle(new Style.Builder().fromUri(getNextStyle())); + } + + private void checkPermissions() { + if (PermissionsManager.areLocationPermissionsGranted(this)) { + mapView.getMapAsync(this); + } else { + permissionsManager = new PermissionsManager(new PermissionsListener() { + @Override + public void onExplanationNeeded(List 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 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 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 trackingTypes = new ArrayList<>(); + trackingTypes.add("Blue"); + trackingTypes.add("Red"); + trackingTypes.add("Green"); + trackingTypes.add("Gray"); + ArrayAdapter 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/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/location/Utils.java b/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/location/Utils.java index fdc2353f9..b9a8aad84 100644 --- a/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/location/Utils.java +++ b/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/location/Utils.java @@ -10,7 +10,7 @@ import java.util.Random; import timber.log.Timber; /** - * Useful utilities used throughout the testapp. + * Useful utilities used throughout the test app. */ public class Utils { diff --git a/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_location_layer_basic_pulsing_circle.xml b/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_location_layer_basic_pulsing_circle.xml new file mode 100644 index 000000000..9b4c82b37 --- /dev/null +++ b/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_location_layer_basic_pulsing_circle.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_location_layer_customized_pulsing_circle.xml b/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_location_layer_customized_pulsing_circle.xml new file mode 100644 index 000000000..d277e4485 --- /dev/null +++ b/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_location_layer_customized_pulsing_circle.xml @@ -0,0 +1,78 @@ + + + + + + + + + +