diff options
author | Mikhail Pozdnyakov <mikhail.pozdnyakov@mapbox.com> | 2019-01-18 13:12:04 +0200 |
---|---|---|
committer | langsmith <langstonlmcs@gmail.com> | 2019-04-09 17:25:35 -0700 |
commit | 194094e769fbbc0c25a5a6aa86db123528b70c86 (patch) | |
tree | 40a5dc5e3cb06ccc7c05ffff3f02d8614e153c64 | |
parent | 8389e746b6745a68fcd58ece8e398bde0a85b57f (diff) | |
download | qtlocation-mapboxgl-194094e769fbbc0c25a5a6aa86db123528b70c86.tar.gz |
[android] initial additions to add a pulsing locationComponent circle
19 files changed, 1014 insertions, 40 deletions
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerSourceProvider.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerSourceProvider.java index cac513c2f9..00afe82816 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerSourceProvider.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LayerSourceProvider.java @@ -1,6 +1,7 @@ package com.mapbox.mapboxsdk.location; import android.support.annotation.NonNull; + import com.mapbox.geojson.Feature; import com.mapbox.mapboxsdk.style.layers.CircleLayer; import com.mapbox.mapboxsdk.style.layers.Layer; @@ -27,6 +28,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.PROPERTY_PULSING_CIRCLE_LAYER; import static com.mapbox.mapboxsdk.location.LocationComponentConstants.SHADOW_ICON; import static com.mapbox.mapboxsdk.location.LocationComponentConstants.SHADOW_LAYER; import static com.mapbox.mapboxsdk.style.expressions.Expression.get; @@ -108,4 +110,12 @@ class LayerSourceProvider { circlePitchAlignment(Property.CIRCLE_PITCH_ALIGNMENT_MAP) ); } + + @NonNull + Layer generatePulsingCircleLayer() { + return new CircleLayer(PROPERTY_PULSING_CIRCLE_LAYER, LOCATION_SOURCE) + .withProperties( + circlePitchAlignment(Property.CIRCLE_PITCH_ALIGNMENT_MAP) + ); + } } diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinator.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinator.java index 35e5efc266..c6c9340c06 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinator.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinator.java @@ -7,10 +7,15 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.util.SparseArray; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.BounceInterpolator; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; import com.mapbox.mapboxsdk.camera.CameraPosition; import com.mapbox.mapboxsdk.geometry.LatLng; +import com.mapbox.mapboxsdk.location.modes.PulseMode; import com.mapbox.mapboxsdk.log.Logger; import com.mapbox.mapboxsdk.maps.MapboxMap; import com.mapbox.mapboxsdk.maps.Projection; @@ -51,6 +56,7 @@ final class LocationAnimatorCoordinator { private final MapboxAnimatorSetProvider animatorSetProvider; private boolean compassAnimationEnabled; private boolean accuracyAnimationEnabled; + private PulsingLocationCircleAnimator pulsingLocationCircleAnimator; @VisibleForTesting int maxAnimationFps = Integer.MAX_VALUE; @@ -147,6 +153,16 @@ final class LocationAnimatorCoordinator { playAnimators(animationDuration, ANIMATOR_TILT); } + void startLocationCirclePulsing(LocationComponentOptions options, MapboxMap mapboxMap) { + pulsingLocationCircleAnimator = new PulsingLocationCircleAnimator(); + pulsingLocationCircleAnimator.animatePulsingCircleRadius( + retrievePulseInterpolator(options.pulseInterpolator()), mapboxMap, options); + } + + void stopPulsingAnimation() { + pulsingLocationCircleAnimator.stopPulsingAnimation(); + } + private LatLng getPreviousLayerLatLng() { LatLng previousLatLng; MapboxAnimator latLngAnimator = animatorArray.get(ANIMATOR_LAYER_LATLNG); @@ -381,11 +397,26 @@ final class LocationAnimatorCoordinator { this.accuracyAnimationEnabled = accuracyAnimationEnabled; } - void setMaxAnimationFps(int maxAnimationFps) { - if (maxAnimationFps <= 0) { - Logger.e(TAG, "Max animation FPS cannot be less or equal to 0."); - return; + void setMaxAnimationFps(int maxAnimationFps){ + if (maxAnimationFps <= 0) { + Logger.e(TAG, "Max animation FPS cannot be less or equal to 0."); + return; + } + this.maxAnimationFps = maxAnimationFps; + } + + private Interpolator retrievePulseInterpolator(String interpolatorAnimationType) { + switch(interpolatorAnimationType) { + case PulseMode.LINEAR: + return new LinearInterpolator(); + case PulseMode.ACCELERATE: + return new AccelerateInterpolator(); + case PulseMode.DECELERATE: + return new DecelerateInterpolator(); + case PulseMode.BOUNCE: + return new BounceInterpolator(); + default: + return new DecelerateInterpolator(); } - this.maxAnimationFps = maxAnimationFps; } } diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponent.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponent.java index d643795c04..03aee9d1f3 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponent.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponent.java @@ -11,6 +11,7 @@ import android.support.annotation.Nullable; import android.support.annotation.RequiresPermission; import android.support.annotation.StyleRes; import android.support.annotation.VisibleForTesting; +import android.util.Log; import android.view.WindowManager; import com.mapbox.android.core.location.LocationEngine; @@ -654,6 +655,7 @@ public final class LocationComponent { * * @param options to update the current style */ + @SuppressLint("MissingPermission") public void applyStyle(@NonNull final LocationComponentOptions options) { checkActivationState(); LocationComponent.this.options = options; @@ -665,6 +667,9 @@ public final class LocationComponent { locationAnimatorCoordinator.setTrackingAnimationDurationMultiplier(options.trackingAnimationDurationMultiplier()); locationAnimatorCoordinator.setCompassAnimationEnabled(options.compassAnimationEnabled()); locationAnimatorCoordinator.setAccuracyAnimationEnabled(options.accuracyAnimationEnabled()); + if (options.pulseEnabled()) { + locationAnimatorCoordinator.startLocationCirclePulsing(options, mapboxMap); + } updateMapWithOptions(options); } } @@ -1140,6 +1145,10 @@ public final class LocationComponent { if (locationEngine != null) { locationEngine.removeLocationUpdates(currentLocationEngineListener); } + if (options.pulseEnabled()) { + Log.d(TAG, "onLocationLayerStop: about to stop animation "); + locationAnimatorCoordinator.stopPulsingAnimation(); + } mapboxMap.removeOnCameraMoveListener(onCameraMoveListener); mapboxMap.removeOnCameraIdleListener(onCameraIdleListener); } diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentConstants.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentConstants.java index f2158584c7..66c8cdc180 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentConstants.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentConstants.java @@ -80,6 +80,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 PROPERTY_PULSING_CIRCLE_LAYER = "mapbox-location-pulsing-circle-layer"; + // Icons static final String FOREGROUND_ICON = "mapbox-location-icon"; static final String BACKGROUND_ICON = "mapbox-location-stroke-icon"; diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentOptions.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentOptions.java index a7b83d7d9d..ccb3709120 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentOptions.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentOptions.java @@ -11,6 +11,7 @@ import android.support.annotation.DrawableRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.StyleRes; +import android.util.Log; import com.mapbox.android.gestures.AndroidGesturesManager; import com.mapbox.mapboxsdk.R; @@ -68,6 +69,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 = 1500; + + /** + * Default number of milliseconds which pass after the LocationComponent circle pulse ends and a new pulse begins. + */ + private static final float CIRCLE_PULSING_FREQUENCY_DEFAULT_MS = 1800; + + /** + * Default opacity of the LocationComponent circle when it ends a single pulse. + */ + private static final float CIRCLE_PULSING_FINAL_ALPHA_DEFAULT = 0.6f; + private float accuracyAlpha; private int accuracyColor; private int backgroundDrawableStale; @@ -112,6 +128,13 @@ public class LocationComponentOptions implements Parcelable { private float trackingAnimationDurationMultiplier; private boolean compassAnimationEnabled; private boolean accuracyAnimationEnabled; + private boolean pulsingCircleEnabled; + private boolean pulsingCircleFadeEnabled; + private int pulseColor; + private float pulseSingleDuration; + private float pulseFrequency; + private float pulseAlpha; + private String pulseInterpolator; public LocationComponentOptions( float accuracyAlpha, @@ -145,7 +168,14 @@ public class LocationComponentOptions implements Parcelable { String layerBelow, float trackingAnimationDurationMultiplier, boolean compassAnimationEnabled, - boolean accuracyAnimationEnabled) { + boolean accuracyAnimationEnabled, + boolean pulsingCircleEnabled, + boolean pulsingCircleFadeEnabled, + Integer pulsingCircleColor, + float pulsingCircleDuration, + float pulsingCircleFrequency, + float pulsingCircleAlpha, + String pulsingCircleInterpolator) { this.accuracyAlpha = accuracyAlpha; this.accuracyColor = accuracyColor; this.backgroundDrawableStale = backgroundDrawableStale; @@ -181,6 +211,13 @@ public class LocationComponentOptions implements Parcelable { this.trackingAnimationDurationMultiplier = trackingAnimationDurationMultiplier; this.compassAnimationEnabled = compassAnimationEnabled; this.accuracyAnimationEnabled = accuracyAnimationEnabled; + this.pulsingCircleEnabled = pulsingCircleEnabled; + this.pulsingCircleFadeEnabled = pulsingCircleFadeEnabled; + this.pulseColor = pulsingCircleColor; + this.pulseSingleDuration = pulsingCircleDuration; + this.pulseFrequency = pulsingCircleFrequency; + this.pulseAlpha = pulsingCircleAlpha; + this.pulseInterpolator = pulsingCircleInterpolator; } /** @@ -295,6 +332,35 @@ public class LocationComponentOptions implements Parcelable { R.styleable.mapbox_LocationComponent_mapbox_accuracyAnimationEnabled, true ); + builder.pulsingCircleEnabled = typedArray.getBoolean( + R.styleable.mapbox_LocationComponent_mapbox_pulsingLocationCircleEnabled, false + ); + + builder.pulsingCircleFadeEnabled = typedArray.getBoolean( + R.styleable.mapbox_LocationComponent_mapbox_pulsingLocationCircleFadeEnabled, true + ); + + if (typedArray.hasValue(R.styleable.mapbox_LocationComponent_mapbox_pulsingLocationCircleColor)) { + builder.pulsingCircleColor(typedArray.getColor( + R.styleable.mapbox_LocationComponent_mapbox_pulsingLocationCircleColor, + -1)); + } + + builder.pulsingCircleDuration = typedArray.getFloat( + R.styleable.mapbox_LocationComponent_mapbox_pulsingLocationCircleDuration, CIRCLE_PULSING_DURATION_DEFAULT_MS + ); + + builder.pulsingCircleFrequency = typedArray.getFloat( + R.styleable.mapbox_LocationComponent_mapbox_pulsingLocationCircleFrequency, CIRCLE_PULSING_FREQUENCY_DEFAULT_MS + ); + + builder.pulsingCircleAlpha = typedArray.getFloat( + R.styleable.mapbox_LocationComponent_mapbox_pulsingLocationCircleAlpha, CIRCLE_PULSING_FINAL_ALPHA_DEFAULT + ); + + builder.pulsingCircleInterpolator = typedArray.getString( + R.styleable.mapbox_LocationComponent_mapbox_pulsingLocationCircleInterpolator); + typedArray.recycle(); return builder.build(); @@ -717,6 +783,71 @@ public class LocationComponentOptions implements Parcelable { return accuracyAnimationEnabled; } + /** + * Enable or disable the LocationComponent's pulsing circle. + * + * @return whether the LocationComponent's pulsing circle is enabled + */ + public boolean pulseEnabled() { + return pulsingCircleEnabled; + } + + + /** + * Enable or disable fading of the LocationComponent's pulsing circle. If it fades, the circle's + * opacity decreases as its radius increases. + * + * @return whether fading of the LocationComponent's pulsing circle is enabled + */ + public boolean pulsingCircleFadeEnabled() { + return pulsingCircleFadeEnabled; + } + + /** + * Color of the LocationComponent's pulsing circle as it pulses. + * + * @return the current set color of the circle + */ + public Integer pulseColor() { + return pulseColor; + } + + /** + * The number of milliseconds it takes for a single pulse of the LocationComponent's pulsing circle. + * + * @return the current set length of time for a single pulse + */ + public float pulseSingleDuration() { + return pulseSingleDuration; + } + + /** + * The number of milliseconds between each pulse of the LocationComponent's circle + * + * @return the current set length of time between pulses + */ + public float pulseFrequency() { + return pulseFrequency; + } + + /** + * The opacity of the LocationComponent's circle as it pulses. + * + * @return the current set opacity of the LocationComponent's circle + */ + public float pulseAlpha() { + return pulseAlpha; + } + + /** + * The interpolator type of animation for the movement of the LocationComponent's circle + * + * @return the current set type of animation interpolator for the pulsing circle + */ + public String pulseInterpolator() { + return pulseInterpolator; + } + @NonNull @Override public String toString() { @@ -750,7 +881,13 @@ public class LocationComponentOptions implements Parcelable { + "trackingInitialMoveThreshold=" + trackingInitialMoveThreshold + ", " + "trackingMultiFingerMoveThreshold=" + trackingMultiFingerMoveThreshold + ", " + "layerBelow=" + layerBelow - + "trackingAnimationDurationMultiplier=" + trackingAnimationDurationMultiplier + + "enablePulsingCircle=" + pulsingCircleEnabled + + "enablePulsingCircleFade=" + pulsingCircleFadeEnabled + + "pulseColor=" + pulseColor + + "pulseSingleDuration=" + pulseSingleDuration + + "pulseFrequency=" + pulseFrequency + + "pulseAlpha=" + pulseAlpha + + "pulseInterpolator=" + pulseInterpolator + "}"; } @@ -803,7 +940,18 @@ public class LocationComponentOptions implements Parcelable { == Float.floatToIntBits(that.trackingMultiFingerMoveThreshold())) && layerBelow.equals(that.layerBelow)) && (Float.floatToIntBits(this.trackingAnimationDurationMultiplier) - == Float.floatToIntBits(that.trackingAnimationDurationMultiplier())); + == Float.floatToIntBits(that.trackingAnimationDurationMultiplier())) + && (this.pulsingCircleEnabled == that.pulseEnabled()) + && (this.pulsingCircleFadeEnabled == that.pulseEnabled()) + && (this.pulseColor == that.pulseColor()) + && (Float.floatToIntBits(this.pulseSingleDuration) + == Float.floatToIntBits(that.pulseSingleDuration())) + && (Float.floatToIntBits(this.pulseFrequency) + == Float.floatToIntBits(that.pulseFrequency())) + && (Float.floatToIntBits(this.pulseAlpha) + == Float.floatToIntBits(that.pulseAlpha())) + && ((this.pulseInterpolator == null) ? (that.pulseInterpolator() == null) + : this.pulseInterpolator.equals(that.pulseInterpolator())); } return false; } @@ -873,6 +1021,21 @@ public class LocationComponentOptions implements Parcelable { h$ ^= compassAnimationEnabled ? 1231 : 1237; h$ *= 1000003; h$ ^= accuracyAnimationEnabled ? 1231 : 1237; + h$ ^= 1000003; + h$ ^= pulsingCircleEnabled ? 1231 : 1237; + h$ ^= 1000003; + h$ ^= pulsingCircleFadeEnabled ? 1231 : 1237; + h$ ^= 1000003; + h$ ^= pulseColor; + h$ *= 1000003; + h$ ^= Float.floatToIntBits(pulseSingleDuration); + h$ *= 1000003; + h$ ^= Float.floatToIntBits(pulseFrequency); + h$ *= 1000003; + h$ ^= Float.floatToIntBits(pulseAlpha); + h$ *= 1000003; + h$ ^= (pulseInterpolator == null) ? 0 : pulseInterpolator.hashCode(); + h$ *= 1000003; return h$; } @@ -912,7 +1075,14 @@ public class LocationComponentOptions implements Parcelable { in.readString(), in.readFloat(), in.readInt() == 1, - in.readInt() == 1 + in.readInt() == 1, + in.readInt() == 1, + in.readInt() == 1, + in.readInt() == 0 ? in.readInt() : null, + in.readFloat(), + in.readFloat(), + in.readFloat(), + in.readString() ); } @@ -1011,6 +1181,18 @@ public class LocationComponentOptions implements Parcelable { dest.writeFloat(trackingAnimationDurationMultiplier); dest.writeInt(compassAnimationEnabled() ? 1 : 0); dest.writeInt(accuracyAnimationEnabled() ? 1 : 0); + dest.writeInt(pulseEnabled() ? 1 : 0); + dest.writeInt(pulsingCircleFadeEnabled() ? 1 : 0); + if (pulseColor() == null) { + dest.writeInt(1); + } else { + dest.writeInt(0); + dest.writeInt(pulseColor()); + } + dest.writeFloat(pulseSingleDuration()); + dest.writeFloat(pulseFrequency()); + dest.writeFloat(pulseAlpha()); + dest.writeString(pulseInterpolator()); } @Override @@ -1041,6 +1223,18 @@ public class LocationComponentOptions implements Parcelable { + locationComponentOptions.elevation() + ". Must be >= 0"); } + // TODO: Finish this logic below + + /*Log.d("LocationComponentOptions", "locationComponentOptions.pulseFrequency() = " + locationComponentOptions.pulseFrequency()); + Log.d("LocationComponentOptions", "locationComponentOptions.pulseSingleDuration() = " + locationComponentOptions.pulseSingleDuration()); + if (locationComponentOptions.pulseFrequency() < locationComponentOptions.pulseSingleDuration()) { + + throw new IllegalArgumentException("Invalid relationship between the LocationComponent " + + "pulsing circle frequency of " + locationComponentOptions.pulseFrequency() + " and duration of " + + locationComponentOptions.pulseSingleDuration() + " . The frequency of the pulsing must be >= to the duration " + + "of a single pulse."); + }*/ + return locationComponentOptions; } @@ -1088,6 +1282,13 @@ public class LocationComponentOptions implements Parcelable { private Float trackingAnimationDurationMultiplier; private Boolean compassAnimationEnabled; private Boolean accuracyAnimationEnabled; + private boolean pulsingCircleEnabled; + private boolean pulsingCircleFadeEnabled; + private int pulsingCircleColor; + private float pulsingCircleDuration; + private float pulsingCircleFrequency; + private float pulsingCircleAlpha; + private String pulsingCircleInterpolator; Builder() { } @@ -1125,6 +1326,13 @@ public class LocationComponentOptions implements Parcelable { this.trackingAnimationDurationMultiplier = source.trackingAnimationDurationMultiplier(); this.compassAnimationEnabled = source.compassAnimationEnabled(); this.accuracyAnimationEnabled = source.accuracyAnimationEnabled(); + this.pulsingCircleEnabled = source.pulsingCircleEnabled; + this.pulsingCircleFadeEnabled = source.pulsingCircleFadeEnabled; + this.pulsingCircleColor = source.pulseColor; + this.pulsingCircleDuration = source.pulseSingleDuration; + this.pulsingCircleFrequency = source.pulseFrequency; + this.pulsingCircleAlpha = source.pulseAlpha; + this.pulsingCircleInterpolator = source.pulseInterpolator; } /** @@ -1585,11 +1793,82 @@ public class LocationComponentOptions implements Parcelable { * * @return whether smooth animation of the accuracy circle is enabled */ - public Builder accuracyAnimationEnabled(Boolean accuracyAnimationEnabled) { + public LocationComponentOptions.Builder accuracyAnimationEnabled(Boolean accuracyAnimationEnabled) { this.accuracyAnimationEnabled = accuracyAnimationEnabled; return this; } + /** + * Enable or disable the LocationComponent's pulsing circle. + * + * @return whether the LocationComponent's pulsing circle is enabled + */ + public LocationComponentOptions.Builder pulsingCircleEnabled(Boolean pulsingCircleEnabled) { + this.pulsingCircleEnabled = pulsingCircleEnabled; + return this; + } + + /** + * Enable or disable fading of the LocationComponent's pulsing circle. If it fades, the circle's + * opacity decreases as its radius increases. + * + * @return whether fading of the LocationComponent's pulsing circle is enabled + */ + public LocationComponentOptions.Builder pulsingCircleFadeEnabled(Boolean pulsingCircleFadeEnabled) { + this.pulsingCircleFadeEnabled = pulsingCircleFadeEnabled; + return this; + } + + /** + * Sets the color of the LocationComponent's pulsing circle. + * + * @return the current set color of the circle + */ + public LocationComponentOptions.Builder pulsingCircleColor(int pulsingCircleColor) { + this.pulsingCircleColor = pulsingCircleColor; + return this; + } + + /** + * Sets the number of milliseconds it takes for a single pulse of the LocationComponent's pulsing circle. + * + * @return the current set length of time for a single pulse + */ + public LocationComponentOptions.Builder pulsingCircleDuration(float pulsingCircleDuration) { + this.pulsingCircleDuration = pulsingCircleDuration; + return this; + } + + /** + * Sets the number of milliseconds that are waiting between pulses of the LocationComponent's pulsing circle. + * + * @return the current set length of time between pulses + */ + public LocationComponentOptions.Builder pulsingCircleFrequency(float pulsingCircleFrequency) { + this.pulsingCircleFrequency = pulsingCircleFrequency; + return this; + } + + /** + * Sets the opacity of the LocationComponent's pulsing circle. + * + * @return the current set opacity of the LocationComponent's circle + */ + public LocationComponentOptions.Builder pulsingCircleAlpha(float pulsingCircleAlpha) { + this.pulsingCircleAlpha = pulsingCircleAlpha; + return this; + } + + /** + * Sets the pulsing circle's interpolator animation. + * + * @return the interpolator animation which the pulsing circle is using + */ + public LocationComponentOptions.Builder pulsingCircleInterpolator(String pulsingCircleInterpolator) { + this.pulsingCircleInterpolator = pulsingCircleInterpolator; + return this; + } + @Nullable LocationComponentOptions autoBuild() { String missing = ""; @@ -1644,8 +1923,8 @@ public class LocationComponentOptions implements Parcelable { if (this.trackingMultiFingerMoveThreshold == null) { missing += " trackingMultiFingerMoveThreshold"; } - if (this.trackingAnimationDurationMultiplier == null) { - missing += " trackingAnimationDurationMultiplier"; + if (this.pulsingCircleInterpolator == null) { + missing += " pulseInterpolator"; } if (!missing.isEmpty()) { throw new IllegalStateException("Missing required properties:" + missing); @@ -1682,7 +1961,14 @@ public class LocationComponentOptions implements Parcelable { this.layerBelow, this.trackingAnimationDurationMultiplier, this.compassAnimationEnabled, - this.accuracyAnimationEnabled); + this.accuracyAnimationEnabled, + this.pulsingCircleEnabled, + this.pulsingCircleFadeEnabled, + this.pulsingCircleColor, + this.pulsingCircleDuration, + this.pulsingCircleFrequency, + this.pulsingCircleAlpha, + this.pulsingCircleInterpolator); } } } diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationLayerController.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationLayerController.java index aa8a82bf6d..5d2ff4d0ee 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationLayerController.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationLayerController.java @@ -6,6 +6,7 @@ import android.support.annotation.ColorInt; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; +import android.util.Log; import com.google.gson.JsonArray; import com.google.gson.JsonObject; @@ -19,6 +20,7 @@ import com.mapbox.mapboxsdk.style.layers.Layer; import com.mapbox.mapboxsdk.style.layers.SymbolLayer; import com.mapbox.mapboxsdk.style.sources.GeoJsonSource; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -46,6 +48,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.PROPERTY_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.interpolate; @@ -54,6 +57,9 @@ import static com.mapbox.mapboxsdk.style.expressions.Expression.stop; import static com.mapbox.mapboxsdk.style.expressions.Expression.zoom; import static com.mapbox.mapboxsdk.style.layers.Property.NONE; import static com.mapbox.mapboxsdk.style.layers.Property.VISIBLE; +import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.circleColor; +import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.circleOpacity; +import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.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; @@ -72,6 +78,8 @@ final class LocationLayerController { @VisibleForTesting final Set<String> layerSet = new HashSet<>(); + private final List<String> layerMap = new ArrayList<>(); + private final String TAG = "LocationLayerController"; private Feature locationFeature; private GeoJsonSource locationSource; @@ -133,6 +141,8 @@ final class LocationLayerController { styleBearing(options); styleAccuracy(options.accuracyAlpha(), options.accuracyColor()); styleScaling(options); + styleScaling(options); + stylePulsingCircle(options); determineIconsSource(options); } @@ -244,6 +254,7 @@ final class LocationLayerController { addSymbolLayer(BACKGROUND_LAYER, FOREGROUND_LAYER); addSymbolLayer(SHADOW_LAYER, BACKGROUND_LAYER); addAccuracyLayer(); + addPulsingCircleLayerToMap(); } private void addSymbolLayer(@NonNull String layerId, @NonNull String beforeLayerId) { @@ -261,6 +272,11 @@ final class LocationLayerController { layerSet.add(layer.getId()); } + private void addPulsingCircleLayerToMap() { + Layer pulsingCircleLayer = layerSourceProvider.generatePulsingCircleLayer(); + addLayerToMap(pulsingCircleLayer, ACCURACY_LAYER); + } + private void removeLayers() { for (String layerId : layerSet) { style.removeLayer(layerId); @@ -367,6 +383,21 @@ final class LocationLayerController { } } + private void stylePulsingCircle(LocationComponentOptions options) { + if (mapboxMap.getStyle() != null) { + if (mapboxMap.getStyle().getLayer(PROPERTY_PULSING_CIRCLE_LAYER) != null) { + Log.d(TAG, "stylePulsingCircle: options.pulseEnabled() = " + options.pulseEnabled()); + // TODO: Swap true for options.pulseEnabled() but figure out why options.pulseEnabled() sometimes returns false + setLayerVisibility(PROPERTY_PULSING_CIRCLE_LAYER, true); + mapboxMap.getStyle().getLayer(PROPERTY_PULSING_CIRCLE_LAYER).setProperties( + circleColor(options.pulseColor()), + circleStrokeColor(options.pulseColor()), + circleOpacity(options.pulseAlpha()) + ); + } + } + } + private void determineIconsSource(LocationComponentOptions options) { String foregroundIconString = buildIconString( renderMode == RenderMode.GPS ? options.gpsName() : options.foregroundName(), FOREGROUND_ICON); @@ -442,6 +473,7 @@ final class LocationLayerController { new MapboxAnimator.AnimationsValueChangeListener<Float>() { @Override public void onNewAnimationValue(Float value) { + // TODO: Hide the pulsing circle layer once the accuracy circle is visibile? (i.e. newAnimationValue > 0) updateAccuracyRadius(value); } }; diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/PulsingLocationCircleAnimator.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/PulsingLocationCircleAnimator.java new file mode 100644 index 0000000000..c8f6260fb1 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/PulsingLocationCircleAnimator.java @@ -0,0 +1,80 @@ +package com.mapbox.mapboxsdk.location; + +import android.animation.ValueAnimator; +import android.os.Handler; +import android.support.annotation.NonNull; +import android.util.Log; +import android.view.animation.Interpolator; + +import com.mapbox.mapboxsdk.maps.MapboxMap; +import com.mapbox.mapboxsdk.style.layers.Layer; + +import static com.mapbox.mapboxsdk.location.LocationComponentConstants.PROPERTY_PULSING_CIRCLE_LAYER; +import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.circleOpacity; +import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.circleRadius; + +/** + * Manages the logic of the interpolated animation which is applied to the LocationComponent's pulsing circle + */ +class PulsingLocationCircleAnimator { + + private static final String TAG = "Mbgl-PulsingLocationCircleAnimator"; + private static final float PULSING_CIRCLE_RADIUS = 60; + private Handler handler; + private Runnable runnable; + private float opacityCounter; + + public PulsingLocationCircleAnimator() { + this.opacityCounter = 0; + } + + /** + * Start the LocationComponent circle pulse animation + * + * @param interpolatorToUse the type of Android-system interpolator to use + * @param mapboxMap the MapboxMap object which pulsing circle should be shown on + * @param locationComponentOptions the stying options of the LocationComponent pulsing circle + */ + public void animatePulsingCircleRadius(@NonNull final Interpolator interpolatorToUse, + @NonNull final MapboxMap mapboxMap, + @NonNull final LocationComponentOptions locationComponentOptions) { + handler = new Handler(); + runnable = new Runnable() { + @Override + public void run() { + // Check if we are at the end of the points list, if so we want to stop using + // the handler. + if (mapboxMap.getStyle().getLayer(PROPERTY_PULSING_CIRCLE_LAYER) != null) { + + opacityCounter = 0; + + final Layer pulsingCircleLayer = mapboxMap.getStyle().getLayer(PROPERTY_PULSING_CIRCLE_LAYER); + ValueAnimator animator = ValueAnimator.ofFloat(0f, PULSING_CIRCLE_RADIUS); + animator.setDuration((long) locationComponentOptions.pulseSingleDuration()); + animator.setRepeatMode(ValueAnimator.RESTART); + animator.setInterpolator(interpolatorToUse); + animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + pulsingCircleLayer.setProperties(circleRadius((Float) valueAnimator.getAnimatedValue())); + if (locationComponentOptions.pulsingCircleFadeEnabled()) { + pulsingCircleLayer.setProperties(circleOpacity(1 - opacityCounter * .01f)); + opacityCounter++; + } + } + }); + animator.start(); + // Once we finish we need to repeat the entire process by executing the + // handler again once the ValueAnimator is finished. + handler.postDelayed(this, (long) locationComponentOptions.pulseFrequency()); + } + } + }; + handler.post(runnable); + } + + public void stopPulsingAnimation() { + Log.d(TAG, "stopPulsingAnimation: "); + handler.removeCallbacks(runnable); + } +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/Utils.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/Utils.java index 49f9a7c43c..f179586782 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/Utils.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/Utils.java @@ -56,6 +56,14 @@ public final class Utils { return (float) (location.getAccuracy() * (1 / metersPerPixel)); } + static float calculatePulsingCircleRadius(@NonNull MapboxMap mapboxMap, @Nullable Location location) { + if (location == null) { + return 0; + } + double metersPerPixel = mapboxMap.getProjection().getMetersPerPixelAtLatitude(location.getLatitude()); + return (float) (location.getAccuracy() * (1 / metersPerPixel)); + } + static boolean immediateAnimation(@NonNull Projection projection, @NonNull LatLng current, @NonNull LatLng target) { double metersPerPixel = projection.getMetersPerPixelAtLatitude((current.getLatitude() + target.getLatitude()) / 2); double distance = current.distanceTo(target); diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/modes/PulseMode.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/modes/PulseMode.java new file mode 100644 index 0000000000..1f905716ea --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/modes/PulseMode.java @@ -0,0 +1,46 @@ +package com.mapbox.mapboxsdk.location.modes; + +import android.support.annotation.StringDef; + +import com.mapbox.mapboxsdk.location.LocationComponentOptions; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +public final class PulseMode { + + private PulseMode() { + // Class should not be initialized + } + + /** + * An interpolator defines the rate of change of an animation. + * + * One of these constants should be used with {@link LocationComponentOptions#pulseInterpolator}. + * + */ + @StringDef( {LINEAR, ACCELERATE, DECELERATE, BOUNCE}) + @Retention(RetentionPolicy.SOURCE) + public @interface Mode { + } + + /** + * An interpolator where the rate of change is constant. + */ + public static final String LINEAR = "linear"; + + /** + * An interpolator where the rate of change starts out slowly and and then accelerates. + */ + public static final String ACCELERATE = "accelerate"; + + /** + * An interpolator where the rate of change starts out quickly and and then decelerates. + */ + public static final String DECELERATE = "decelerate"; + + /** + * An interpolator where the change bounces at the end. + */ + public static final String BOUNCE = "bounce"; +} diff --git a/platform/android/MapboxGLAndroidSDK/src/main/res-public/values/public.xml b/platform/android/MapboxGLAndroidSDK/src/main/res-public/values/public.xml index 58109257f2..39b6813c53 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/res-public/values/public.xml +++ b/platform/android/MapboxGLAndroidSDK/src/main/res-public/values/public.xml @@ -12,7 +12,7 @@ <public name="mapbox_apiBaseUrl" type="attr" /> <public name="mapbox_localIdeographFontFamily" type="attr" /> <public name="mapbox_cross_source_collisions" type="attr" /> - <public name="mapbox_pixelRatio" type="float" /> + <public name="mapbox_pixelRatio" type="float" /> <!--Camera--> <public name="mapbox_cameraTargetLng" type="attr" /> @@ -105,42 +105,42 @@ <!-- Exposed colors --> <public name="mapbox_blue" type="color" /> - <public name="mapbox_LocationComponent" type="style"/> + <public name="mapbox_LocationComponent" type="style" /> - <public name="mapbox_foregroundDrawable" format="reference" type="attr"/> - <public name="mapbox_foregroundTintColor" format="color" type="attr"/> - <public name="mapbox_backgroundDrawable" format="reference" type="attr"/> - <public name="mapbox_backgroundTintColor" format="color" type="attr"/> - <public name="mapbox_bearingDrawable" format="reference" type="attr"/> - <public name="mapbox_bearingTintColor" format="color" type="attr"/> - <public name="mapbox_navigationDrawable" format="reference" type="attr"/> + <public name="mapbox_foregroundDrawable" format="reference" type="attr" /> + <public name="mapbox_foregroundTintColor" format="color" type="attr" /> + <public name="mapbox_backgroundDrawable" format="reference" type="attr" /> + <public name="mapbox_backgroundTintColor" format="color" type="attr" /> + <public name="mapbox_bearingDrawable" format="reference" type="attr" /> + <public name="mapbox_bearingTintColor" format="color" type="attr" /> + <public name="mapbox_navigationDrawable" format="reference" type="attr" /> - <public name="mapbox_foregroundDrawableStale" format="reference" type="attr"/> - <public name="mapbox_foregroundStaleTintColor" format="color" type="attr"/> - <public name="mapbox_backgroundDrawableStale" format="reference" type="attr"/> - <public name="mapbox_backgroundStaleTintColor" format="color" type="attr"/> + <public name="mapbox_foregroundDrawableStale" format="reference" type="attr" /> + <public name="mapbox_foregroundStaleTintColor" format="color" type="attr" /> + <public name="mapbox_backgroundDrawableStale" format="reference" type="attr" /> + <public name="mapbox_backgroundStaleTintColor" format="color" type="attr" /> - <public name="mapbox_accuracyAlpha" format="float" type="attr"/> - <public name="mapbox_accuracyColor" format="color" type="attr"/> + <public name="mapbox_accuracyAlpha" format="float" type="attr" /> + <public name="mapbox_accuracyColor" format="color" type="attr" /> - <public name="mapbox_elevation" format="dimension" type="attr"/> + <public name="mapbox_elevation" format="dimension" type="attr" /> - <public name="mapbox_enableStaleState" format="boolean" type="attr"/> - <public name="mapbox_staleStateTimeout" format="integer" min="0" type="attr"/> + <public name="mapbox_enableStaleState" format="boolean" type="attr" /> + <public name="mapbox_staleStateTimeout" format="integer" min="0" type="attr" /> <!-- Location icon padding --> - <public name="mapbox_iconPaddingLeft" format="integer" type="attr"/> - <public name="mapbox_iconPaddingTop" format="integer" type="attr"/> - <public name="mapbox_iconPaddingRight" format="integer" type="attr"/> - <public name="mapbox_iconPaddingBottom" format="integer" type="attr"/> + <public name="mapbox_iconPaddingLeft" format="integer" type="attr" /> + <public name="mapbox_iconPaddingTop" format="integer" type="attr" /> + <public name="mapbox_iconPaddingRight" format="integer" type="attr" /> + <public name="mapbox_iconPaddingBottom" format="integer" type="attr" /> <!-- Icon scale based on map zoom levels --> - <public name="mapbox_maxZoomIconScale" format="float" type="attr"/> - <public name="mapbox_minZoomIconScale" format="float" type="attr"/> + <public name="mapbox_maxZoomIconScale" format="float" type="attr" /> + <public name="mapbox_minZoomIconScale" format="float" type="attr" /> <!-- Camera tracking settings --> - <public name="mapbox_trackingInitialMoveThreshold" format="float" type="attr"/> - <public name="mapbox_trackingMultiFingerMoveThreshold" format="float" type="attr"/> + <public name="mapbox_trackingInitialMoveThreshold" format="float" type="attr" /> + <public name="mapbox_trackingMultiFingerMoveThreshold" format="float" type="attr" /> <!-- Animation duration multiplier --> <public name="mapbox_trackingAnimationDurationMultiplier" format="float" type="attr" /> @@ -150,4 +150,13 @@ <!-- Accuracy animation--> <public name="mapbox_accuracyAnimationEnabled" format="boolean" type="attr" /> + + <!-- Pulsing circle --> + <public name="mapbox_pulsingLocationCircleEnabled" format="boolean" type="attr" /> + <public name="mapbox_pulsingLocationCircleFadeEnabled" format="boolean" type="attr" /> + <public name="mapbox_pulsingLocationCircleColor" format="color" type="attr" /> + <public name="mapbox_pulsingLocationCircleDuration" format="float" type="attr" /> + <public name="mapbox_pulsingLocationCircleFrequency" format="float" type="attr" /> + <public name="mapbox_pulsingLocationCircleAlpha" format="float" type="attr" /> + <public name="mapbox_pulsingLocationCircleInterpolator" format="string" type="attr" /> </resources> diff --git a/platform/android/MapboxGLAndroidSDK/src/main/res/values/attrs.xml b/platform/android/MapboxGLAndroidSDK/src/main/res/values/attrs.xml index 2eeb5057f2..f47327c8a9 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/res/values/attrs.xml +++ b/platform/android/MapboxGLAndroidSDK/src/main/res/values/attrs.xml @@ -176,5 +176,14 @@ <!-- Accuracy animation--> <attr name="mapbox_accuracyAnimationEnabled" format="boolean" /> + <!-- Pulsing circle --> + <attr name="mapbox_pulsingLocationCircleEnabled" format="boolean" /> + <attr name="mapbox_pulsingLocationCircleFadeEnabled" format="boolean" /> + <attr name="mapbox_pulsingLocationCircleColor" format="color" /> + <attr name="mapbox_pulsingLocationCircleDuration" format="float" /> + <attr name="mapbox_pulsingLocationCircleFrequency" format="float" /> + <attr name="mapbox_pulsingLocationCircleAlpha" format="float" /> + <attr name="mapbox_pulsingLocationCircleInterpolator" format="string" /> + </declare-styleable> </resources> diff --git a/platform/android/MapboxGLAndroidSDK/src/main/res/values/styles.xml b/platform/android/MapboxGLAndroidSDK/src/main/res/values/styles.xml index c6c2d3fc7b..8cb784eb58 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/res/values/styles.xml +++ b/platform/android/MapboxGLAndroidSDK/src/main/res/values/styles.xml @@ -33,5 +33,11 @@ <item name="mapbox_trackingInitialMoveThreshold">@dimen/mapbox_locationComponentTrackingInitialMoveThreshold</item> <item name="mapbox_trackingMultiFingerMoveThreshold">@dimen/mapbox_locationComponentTrackingMultiFingerMoveThreshold</item> + <!-- Location pulsing circle --> + <item name="mapbox_pulsingLocationCircleColor">@color/mapbox_location_layer_blue</item> + <item name="mapbox_pulsingLocationCircleDuration">2.5</item> + <item name="mapbox_pulsingLocationCircleFrequency">2</item> + <item name="mapbox_pulsingLocationCircleAlpha">0.4</item> + <item name="mapbox_pulsingLocationCircleInterpolator">linear</item> </style> </resources>
\ No newline at end of file diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml index 017fe3ddca..6e048b49f2 100644 --- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml @@ -865,6 +865,17 @@ android:value=".activity.FeatureOverviewActivity" /> </activity> <activity + android:name=".activity.location.LocationPulsingCircleActivity" + android:description="@string/description_location_pulsing_circle" + android:label="@string/activity_location_pulsing_circle"> + <meta-data + android:name="@string/category" + android:value="@string/category_location" /> + <meta-data + android:name="android.support.PARENT_ACTIVITY" + android:value=".activity.FeatureOverviewActivity" /> + </activity> + <activity android:name=".activity.location.LocationFragmentActivity" android:description="@string/description_location_fragment" android:label="@string/activity_location_fragment"> diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/MapboxApplication.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/MapboxApplication.java index d5cff301db..d07b8a8245 100644 --- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/MapboxApplication.java +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/MapboxApplication.java @@ -52,7 +52,7 @@ public class MapboxApplication extends Application { private void initializeLogger() { Logger.setLoggerDefinition(new TimberLogger()); - if (BuildConfig.DEBUG) { + if (BuildConfig.DEBUG) {contro Timber.plant(new DebugTree()); } } diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/location/LocationPulsingCircleActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/location/LocationPulsingCircleActivity.java new file mode 100644 index 0000000000..1694b7de5a --- /dev/null +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/location/LocationPulsingCircleActivity.java @@ -0,0 +1,324 @@ +package com.mapbox.mapboxsdk.testapp.activity.location; + +import android.annotation.SuppressLint; +import android.graphics.Color; +import android.location.Location; +import android.os.Build; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.content.ContextCompat; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.ListPopupWindow; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.Toast; + +import com.mapbox.android.core.location.LocationEngineRequest; +import com.mapbox.android.core.permissions.PermissionsListener; +import com.mapbox.android.core.permissions.PermissionsManager; +import com.mapbox.mapboxsdk.location.LocationComponent; +import com.mapbox.mapboxsdk.location.LocationComponentOptions; +import com.mapbox.mapboxsdk.location.modes.CameraMode; +import com.mapbox.mapboxsdk.location.modes.PulseMode; +import com.mapbox.mapboxsdk.maps.MapView; +import com.mapbox.mapboxsdk.maps.MapboxMap; +import com.mapbox.mapboxsdk.maps.OnMapReadyCallback; +import com.mapbox.mapboxsdk.maps.Style; +import com.mapbox.mapboxsdk.testapp.R; + +import java.util.ArrayList; +import java.util.List; + +public class LocationPulsingCircleActivity extends AppCompatActivity implements OnMapReadyCallback { + + private MapView mapView; + private Button pulsingCircleFrequencyButton; + private Button pulsingCircleColorButton; + + private PermissionsManager permissionsManager; + + private LocationComponent locationComponent; + private MapboxMap mapboxMap; + private boolean defaultStyle = false; + private float currentPulseFrequency; + + private static final String SAVED_STATE_LOCATION = "saved_state_location"; + + // Make sure that the frequency value is equal to or larger than the duration value. + private static final float DEFAULT_LOCATION_CIRCLE_PULSE_FREQUENCY = 1700; + private static final float DEFAULT_LOCATION_CIRCLE_PULSE_DURATION = 1700; + + private static final float DEFAULT_LOCATION_CIRCLE_PULSE_ALPHA = .4f; + private static final String DEFAULT_LOCATION_CIRCLE_INTERPOLATOR_PULSE_MODE = PulseMode.DECELERATE; + private static final boolean DEFAULT_LOCATION_CIRCLE_PULSE_FADE_MODE = false; + + private static int DEFAULT_LOCATION_CIRCLE_PULSE_COLOR; + + private Location lastLocation; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_location_layer_pulsing_circle); + + DEFAULT_LOCATION_CIRCLE_PULSE_COLOR = ContextCompat.getColor(this, R.color.mapbox_location_layer_blue); + + mapView = findViewById(R.id.mapView); + + pulsingCircleFrequencyButton = findViewById(R.id.button_location_circle_frequency); + pulsingCircleFrequencyButton.setText(String.format("%sms", + String.valueOf(DEFAULT_LOCATION_CIRCLE_PULSE_FREQUENCY))); + pulsingCircleFrequencyButton.setOnClickListener(v -> { + if (locationComponent == null) { + return; + } + showFrequencyListDialog(); + }); + + pulsingCircleColorButton = findViewById(R.id.button_location_circle_color); + pulsingCircleColorButton.setOnClickListener(v -> { + if (locationComponent == null) { + return; + } + showColorListDialog(); + }); + + + if (savedInstanceState != null) { + lastLocation = savedInstanceState.getParcelable(SAVED_STATE_LOCATION); + } + + mapView.onCreate(savedInstanceState); + + if (PermissionsManager.areLocationPermissionsGranted(this)) { + mapView.getMapAsync(this); + } else { + permissionsManager = new PermissionsManager(new PermissionsListener() { + @Override + public void onExplanationNeeded(List<String> permissionsToExplain) { + Toast.makeText(LocationPulsingCircleActivity.this, "You need to accept location permissions.", + Toast.LENGTH_SHORT).show(); + } + + @Override + public void onPermissionResult(boolean granted) { + if (granted) { + mapView.getMapAsync(LocationPulsingCircleActivity.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); + } + + @SuppressLint("MissingPermission") + @Override + public void onMapReady(@NonNull MapboxMap mapboxMap) { + this.mapboxMap = mapboxMap; + + mapboxMap.setStyle(Style.MAPBOX_STREETS, style -> { + + locationComponent = mapboxMap.getLocationComponent(); + locationComponent.activateLocationComponent(this, style, true, + new LocationEngineRequest.Builder(750) + .setFastestInterval(750) + .setPriority(LocationEngineRequest.PRIORITY_HIGH_ACCURACY) + .build(), + buildLocationComponentOptions( + "waterway-label", + DEFAULT_LOCATION_CIRCLE_PULSE_COLOR, + DEFAULT_LOCATION_CIRCLE_PULSE_ALPHA, + DEFAULT_LOCATION_CIRCLE_PULSE_DURATION, + DEFAULT_LOCATION_CIRCLE_PULSE_FREQUENCY)); + + locationComponent.setLocationComponentEnabled(true); + locationComponent.setCameraMode(CameraMode.TRACKING); + locationComponent.forceLocationUpdate(lastLocation); + }); + } + + private LocationComponentOptions buildLocationComponentOptions(@Nullable String idOfLayerBelow, + @Nullable int pulsingCircleColor, + @Nullable float pulsingCircleAlpha, + @Nullable float pulsingCircleDuration, + @Nullable float pulsingCircleFrequency) { + currentPulseFrequency = pulsingCircleFrequency; + return LocationComponentOptions.builder(this) + .layerBelow(idOfLayerBelow) + .pulsingCircleEnabled(true) + .pulsingCircleFadeEnabled(DEFAULT_LOCATION_CIRCLE_PULSE_FADE_MODE) + .pulsingCircleInterpolator(DEFAULT_LOCATION_CIRCLE_INTERPOLATOR_PULSE_MODE) + .pulsingCircleColor(pulsingCircleColor) + .pulsingCircleAlpha(pulsingCircleAlpha) + .pulsingCircleDuration(pulsingCircleDuration) + .pulsingCircleFrequency(pulsingCircleFrequency) + .build(); + } + + @SuppressLint("MissingPermission") + private void setNewLocationComponentOptions(@Nullable float newPulsingFrequency, + @Nullable int newPulsingColor) { + if (mapboxMap.getStyle() != null) { + locationComponent.activateLocationComponent(this, mapboxMap.getStyle(), + true, + new LocationEngineRequest.Builder(750) + .setFastestInterval(750) + .setPriority(LocationEngineRequest.PRIORITY_HIGH_ACCURACY) + .build(), buildLocationComponentOptions("waterway-label", + newPulsingColor, DEFAULT_LOCATION_CIRCLE_PULSE_ALPHA, + DEFAULT_LOCATION_CIRCLE_PULSE_DURATION, newPulsingFrequency)); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu_location_mode, menu); + return true; + } + + @SuppressLint("MissingPermission") + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (locationComponent == null) { + return super.onOptionsItemSelected(item); + } + + int id = item.getItemId(); + if (id == R.id.action_map_style_change) { + toggleMapStyle(); + return true; + } else if (id == R.id.action_component_disable) { + locationComponent.setLocationComponentEnabled(false); + return true; + } else if (id == R.id.action_component_enabled) { + locationComponent.setLocationComponentEnabled(true); + return true; + } else if (id == R.id.action_component_throttling_enabled) { + locationComponent.setMaxAnimationFps(5); + } else if (id == R.id.action_component_throttling_disabled) { + locationComponent.setMaxAnimationFps(Integer.MAX_VALUE); + } + + return super.onOptionsItemSelected(item); + } + + private void toggleMapStyle() { + if (locationComponent == null) { + return; + } + String styleUrl = Style.DARK.equals(mapboxMap.getStyle().getUrl()) ? Style.LIGHT : Style.DARK; + mapboxMap.setStyle(new Style.Builder().fromUrl(styleUrl)); + } + + @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(); + } + + private void showFrequencyListDialog() { + List<String> modes = new ArrayList<>(); + modes.add("400ms"); + modes.add("1000ms"); + modes.add("2000ms"); + ArrayAdapter<String> profileAdapter = new ArrayAdapter<>(this, + android.R.layout.simple_list_item_1, modes); + ListPopupWindow listPopup = new ListPopupWindow(this); + listPopup.setAdapter(profileAdapter); + listPopup.setAnchorView(pulsingCircleFrequencyButton); + listPopup.setOnItemClickListener((parent, itemView, position, id) -> { + String selectedMode = modes.get(position); + pulsingCircleFrequencyButton.setText(selectedMode); + if (selectedMode.contentEquals(String.format("%sms", + String.valueOf(DEFAULT_LOCATION_CIRCLE_PULSE_FREQUENCY)))) { + setNewLocationComponentOptions(DEFAULT_LOCATION_CIRCLE_PULSE_FREQUENCY, Color.BLUE); + } else if (selectedMode.contentEquals("1000ms")) { + setNewLocationComponentOptions(1000, Color.BLUE); + } else if (selectedMode.contentEquals("3000ms")) { + setNewLocationComponentOptions(3000, Color.BLUE); + } + listPopup.dismiss(); + }); + listPopup.show(); + } + + + private void showColorListDialog() { + List<String> trackingTypes = new ArrayList<>(); + trackingTypes.add("Blue"); + trackingTypes.add("Red"); + trackingTypes.add("Green"); + trackingTypes.add("Yellow"); + ArrayAdapter<String> profileAdapter = new ArrayAdapter<>(this, + android.R.layout.simple_list_item_1, trackingTypes); + ListPopupWindow listPopup = new ListPopupWindow(this); + listPopup.setAdapter(profileAdapter); + listPopup.setAnchorView(pulsingCircleColorButton); + listPopup.setOnItemClickListener((parent, itemView, position, id) -> { + String selectedTrackingType = trackingTypes.get(position); + pulsingCircleColorButton.setText(selectedTrackingType); + if (selectedTrackingType.contentEquals("Blue")) { + setNewLocationComponentOptions(currentPulseFrequency, Color.BLUE); + } else if (selectedTrackingType.contentEquals("Red")) { + setNewLocationComponentOptions(currentPulseFrequency, Color.RED); + } else if (selectedTrackingType.contentEquals("Green")) { + setNewLocationComponentOptions(currentPulseFrequency, Color.GREEN); + } else if (selectedTrackingType.contentEquals("Yellow")) { + setNewLocationComponentOptions(currentPulseFrequency, Color.YELLOW); + } + listPopup.dismiss(); + }); + listPopup.show(); + } +}
\ No newline at end of file diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_location_layer_pulsing_circle.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_location_layer_pulsing_circle.xml new file mode 100644 index 0000000000..36ab53af63 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_location_layer_pulsing_circle.xml @@ -0,0 +1,78 @@ +<?xml version="1.0" encoding="utf-8"?> +<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <com.mapbox.mapboxsdk.maps.MapView + android:id="@+id/mapView" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginBottom="0dp" + app:layout_constraintBottom_toTopOf="@+id/linearLayout" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:mapbox_uiAttribution="false" /> + + <LinearLayout + android:id="@+id/linearLayout" + style="?android:attr/buttonBarStyle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:background="@color/primary" + android:orientation="horizontal" + android:weightSum="4" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + tools:layout_constraintBottom_creator="1" + tools:layout_constraintLeft_creator="1" + tools:layout_constraintRight_creator="1"> + + <TextView + android:id="@+id/tv_frequency" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight=".75" + android:gravity="center" + android:text="Frequency:" + android:textColor="@color/white" + android:textSize="18sp" + android:textStyle="bold" /> + + <Button + android:id="@+id/button_location_circle_frequency" + style="?android:attr/buttonBarButtonStyle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1.25" + android:gravity="center" + tools:text="400ms" + android:textColor="@android:color/white" /> + + <TextView + android:id="@+id/tv_color" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight=".85" + android:gravity="center" + android:text="Color:" + android:textColor="@color/white" + android:textSize="18sp" + android:textStyle="bold" /> + + <Button + android:id="@+id/button_location_circle_color" + style="?android:attr/buttonBarButtonStyle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1.15" + android:gravity="center" + android:text="None" + android:textColor="@android:color/white" /> + + </LinearLayout> + +</android.support.constraint.ConstraintLayout>
\ No newline at end of file diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_location_pulsing_circle_styling.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_location_pulsing_circle_styling.xml new file mode 100644 index 0000000000..cdb0ad5392 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/menu/menu_location_pulsing_circle_styling.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <item android:id="@+id/action_component_disable" + android:title="Disable Component" + app:showAsAction="never"/> + + <item android:id="@+id/action_component_enabled" + android:title="Enable Component" + app:showAsAction="never"/> + + <item android:id="@+id/action_gestures_management_enabled" + android:title="Enable Gestures Management" + app:showAsAction="never"/> + + <item android:id="@+id/action_gestures_management_disabled" + android:title="Disable Gestures Management" + app:showAsAction="never"/> + + <item android:id="@+id/action_component_throttling_enabled" + android:title="Enable animation throttling (5 FPS)" + app:showAsAction="never"/> + + <item android:id="@+id/action_component_throttling_disabled" + android:title="Disable animation throttling" + app:showAsAction="never"/> +</menu>
\ No newline at end of file diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/descriptions.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/descriptions.xml index 778805b3b3..edf6e5166c 100644 --- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/descriptions.xml +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/descriptions.xml @@ -75,6 +75,7 @@ <string name="description_draggable_marker">Click to add a marker, long-click to drag</string> <string name="description_location_map_change">Change map\'s style while location is displayed</string> <string name="description_location_modes">Showcases location render and tracking modes</string> + <string name="description_location_pulsing_circle">Showcases the LocationComponent pulsing circle</string> <string name="description_location_fragment">Uses LocationComponent in a Fragment</string> <string name="description_location_manual">Force location updates and don\'t rely on the engine</string> <string name="description_location_activation_builder">Use LocationComponentActivationOptions to set options</string> diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/titles.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/titles.xml index 12c82bf21a..a76cfea05d 100644 --- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/titles.xml +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/titles.xml @@ -75,6 +75,7 @@ <string name="activity_draggable_maker">Draggable marker</string> <string name="activity_location_map_change">Simple Location Activity</string> <string name="activity_location_modes">Location Modes Activity</string> + <string name="activity_location_pulsing_circle">Location Pulsing Circle Activity</string> <string name="activity_location_fragment">Location Fragment</string> <string name="activity_location_manual">Manual Location updates</string> <string name="activity_location_activation_builder">Build Location Activation</string> |