From 3d2fa433335fd54d701856bda4391979020f1bc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Paczos?= Date: Wed, 21 Oct 2020 17:29:19 +0200 Subject: [PATCH] expose LocationUpdate builder and immediate location animation when resuming the map With this update, we're consolidating all options available to developers when they are driving their own location updates. This deprecates the lookAhead flag and in return exposes an option to completely control the animation duration of the puck and camera location transitions. This can be used for the lookAhead animation, or for situational change in the animation duration, for example, if an immediate transition is necessary instead of a smooth one. --- .../location/LocationAnimatorCoordinator.java | 38 ++--- .../mapboxsdk/location/LocationComponent.java | 135 ++++++++++++---- .../location/LocationComponentOptions.java | 2 + .../mapboxsdk/location/LocationUpdate.java | 151 ++++++++++++++++++ .../LocationAnimatorCoordinatorTest.kt | 53 +++--- .../location/LocationComponentTest.kt | 114 ++++++++++++- 6 files changed, 411 insertions(+), 82 deletions(-) create mode 100644 MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationUpdate.java 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 fc945bcff..e45f0fda5 100644 --- a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinator.java +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinator.java @@ -89,13 +89,9 @@ void updateAnimatorListenerHolders(@NonNull Set listener } } - void feedNewLocation(@NonNull Location newLocation, @NonNull CameraPosition currentCameraPosition, - boolean isGpsNorth) { - feedNewLocation(new Location[] {newLocation}, currentCameraPosition, isGpsNorth, false); - } - void feedNewLocation(@NonNull @Size(min = 1) Location[] newLocations, - @NonNull CameraPosition currentCameraPosition, boolean isGpsNorth, boolean lookAheadUpdate) { + @NonNull CameraPosition currentCameraPosition, boolean isGpsNorth, + @Nullable Long animationDuration) { Location newLocation = newLocations[newLocations.length - 1]; if (previousLocation == null) { previousLocation = newLocation; @@ -125,28 +121,20 @@ void feedNewLocation(@NonNull @Size(min = 1) Location[] newLocations, boolean snap = immediateAnimation(projection, previousCameraLatLng, targetLatLng) || immediateAnimation(projection, previousLayerLatLng, targetLatLng); - long animationDuration = 0; - if (!snap) { - long previousUpdateTimeStamp = locationUpdateTimestamp; - locationUpdateTimestamp = SystemClock.elapsedRealtime(); - - if (previousUpdateTimeStamp == 0) { - animationDuration = 0; - } else if (lookAheadUpdate) { - long currentTimestamp = System.currentTimeMillis(); - if (currentTimestamp > newLocation.getTime()) { - animationDuration = 0; - Logger.e("LocationAnimatorCoordinator", - "Lookahead enabled, but the target location's timestamp is smaller than current timestamp"); + long previousUpdateTimeStamp = locationUpdateTimestamp; + locationUpdateTimestamp = SystemClock.elapsedRealtime(); + if (animationDuration == null) { + animationDuration = 0L; + if (!snap) { + if (previousUpdateTimeStamp == 0) { + animationDuration = 0L; } else { - animationDuration = newLocation.getTime() - currentTimestamp; + animationDuration = (long) ((locationUpdateTimestamp - previousUpdateTimeStamp) * durationMultiplier) + /* make animation slightly longer with durationMultiplier, defaults to 1.1f */; } - } else { - animationDuration = (long) ((locationUpdateTimestamp - previousUpdateTimeStamp) * durationMultiplier) - /* make animation slightly longer with durationMultiplier, defaults to 1.1f */; - } - animationDuration = Math.min(animationDuration, MAX_ANIMATION_DURATION_MS); + animationDuration = Math.min(animationDuration, MAX_ANIMATION_DURATION_MS); + } } playAnimators(animationDuration, 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 14bc45140..7c25ec2bd 100644 --- a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponent.java +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponent.java @@ -4,6 +4,7 @@ import android.content.Context; import android.hardware.SensorManager; import android.location.Location; +import android.os.Build; import android.os.Looper; import android.os.SystemClock; import android.view.WindowManager; @@ -41,6 +42,7 @@ import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; import static android.Manifest.permission.ACCESS_COARSE_LOCATION; import static android.Manifest.permission.ACCESS_FINE_LOCATION; @@ -185,7 +187,7 @@ public final class LocationComponent { private final CopyOnWriteArrayList onRenderModeChangedListeners = new CopyOnWriteArrayList<>(); private final CopyOnWriteArrayList onIndicatorPositionChangedListener - = new CopyOnWriteArrayList<>(); + = new CopyOnWriteArrayList<>(); // Workaround for too frequent updates, see https://github.com/mapbox/mapbox-gl-native/issues/13587 private long fastestInterval; @@ -239,6 +241,34 @@ public LocationComponent(@NonNull MapboxMap mapboxMap, isComponentInitialized = true; } + @VisibleForTesting + LocationComponent(@NonNull MapboxMap mapboxMap, + @NonNull Transform transform, + @NonNull List developerAnimationListeners, + @NonNull LocationEngineCallback currentListener, + @NonNull LocationLayerController locationLayerController, + @NonNull LocationCameraController locationCameraController, + @NonNull LocationAnimatorCoordinator locationAnimatorCoordinator, + @NonNull StaleStateManager staleStateManager, + @NonNull CompassEngine compassEngine, + @NonNull InternalLocationEngineProvider internalLocationEngineProvider, + boolean useSpecializedLocationLayer, + @NonNull LocationEngineRequest locationEngineRequest) { + this.mapboxMap = mapboxMap; + this.transform = transform; + developerAnimationListeners.add(developerAnimationListener); + this.currentLocationEngineListener = currentListener; + this.locationLayerController = locationLayerController; + this.locationCameraController = locationCameraController; + this.locationAnimatorCoordinator = locationAnimatorCoordinator; + this.staleStateManager = staleStateManager; + this.compassEngine = compassEngine; + this.internalLocationEngineProvider = internalLocationEngineProvider; + this.useSpecializedLocationLayer = useSpecializedLocationLayer; + this.locationEngineRequest = locationEngineRequest; + isComponentInitialized = true; + } + /** * This method initializes the component and needs to be called before any other operations are performed. * Afterwards, you can manage component's visibility by {@link #setLocationComponentEnabled(boolean)}. @@ -885,19 +915,19 @@ public void paddingWhileTracking(double[] padding, long animationDuration) { * @param callback The callback with finish/cancel information */ public void paddingWhileTracking(double[] padding, long animationDuration, - @Nullable MapboxMap.CancelableCallback callback) { + @Nullable MapboxMap.CancelableCallback callback) { checkActivationState(); if (!isLayerReady) { notifyUnsuccessfulCameraOperation(callback, null); return; } else if (getCameraMode() == CameraMode.NONE) { notifyUnsuccessfulCameraOperation(callback, String.format("%s%s", - "LocationComponent#paddingWhileTracking method can only be used", - " when a camera mode other than CameraMode#NONE is engaged.")); + "LocationComponent#paddingWhileTracking method can only be used", + " when a camera mode other than CameraMode#NONE is engaged.")); return; } else if (locationCameraController.isTransitioning()) { notifyUnsuccessfulCameraOperation(callback, - "LocationComponent#paddingWhileTracking method call is ignored because the camera mode is transitioning"); + "LocationComponent#paddingWhileTracking method call is ignored because the camera mode is transitioning"); return; } @@ -996,7 +1026,9 @@ public void cancelTiltWhileTrackingAnimation() { * updated. * * @param location where the location icon is placed on the map + * @deprecated use {@link #forceLocationUpdate(LocationUpdate)} instead */ + @Deprecated public void forceLocationUpdate(@Nullable Location location) { checkActivationState(); updateLocation(location, false); @@ -1014,20 +1046,62 @@ public void forceLocationUpdate(@Nullable Location location) { * @param lookAheadUpdate If set to true, the last location's timestamp has to be greater than current timestamp and * should represent the time at which the animation should actually reach this position, * cutting out the time interpolation delay. + * @deprecated use {@link #forceLocationUpdate(LocationUpdate)} instead */ + @Deprecated public void forceLocationUpdate(@Nullable List locations, boolean lookAheadUpdate) { checkActivationState(); if (locations != null && locations.size() >= 1) { + Location targetLocation = locations.get(locations.size() - 1); + if (targetLocation == null) { + return; + } + LocationUpdate.Builder builder = new LocationUpdate.Builder() + .location(targetLocation) + .intermediatePoints(locations.subList(0, locations.size() - 1)); + + if (lookAheadUpdate) { + long currentTimestampNs; + long locationTimestampNs; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + currentTimestampNs = SystemClock.elapsedRealtimeNanos(); + locationTimestampNs = targetLocation.getElapsedRealtimeNanos(); + if (locationTimestampNs == 0) { + // fallback in case the elapsed realtime is not set + currentTimestampNs = TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis()); + locationTimestampNs = TimeUnit.MILLISECONDS.toNanos(targetLocation.getTime()); + } + } else { + currentTimestampNs = TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis()); + locationTimestampNs = TimeUnit.MILLISECONDS.toNanos(targetLocation.getTime()); + } + + if (currentTimestampNs > locationTimestampNs) { + builder.animationDuration(0L); + Logger.e("LocationAnimatorCoordinator", + "Lookahead enabled, but the target location's timestamp is smaller than current timestamp"); + } else { + builder.animationDuration(locationTimestampNs - currentTimestampNs); + } + } updateLocation( - locations.get(locations.size() - 1), // target location - locations.subList(0, locations.size() - 1), // intermediate locations - false, - lookAheadUpdate); - } else { - updateLocation(null, false); + builder.build(), + false + ); } } + /** + * Use to either force a location update or to manually control when the user location gets + * updated. + * + * @param locationUpdate location update + * @see LocationUpdate.Builder + */ + public void forceLocationUpdate(@NonNull LocationUpdate locationUpdate) { + updateLocation(locationUpdate, false); + } + /** * Set max FPS at which location animators can output updates. The throttling will only impact the location puck * and camera tracking smooth animations. @@ -1515,15 +1589,19 @@ private void updateMapWithOptions(@NonNull LocationComponentOptions options) { * @param location the latest user location */ private void updateLocation(@Nullable final Location location, boolean fromLastLocation) { - updateLocation(location, null, fromLastLocation, false); + if (location != null) { + updateLocation( + new LocationUpdate.Builder() + .location(location) + .build(), + fromLastLocation + ); + } } - private void updateLocation(@Nullable final Location location, @Nullable List intermediatePoints, - boolean fromLastLocation, boolean lookAheadUpdate) { - if (location == null) { - return; - } else if (!isLayerReady) { - lastLocation = location; + private void updateLocation(@NonNull LocationUpdate locationUpdate, boolean fromLastLocation) { + if (!isLayerReady) { + lastLocation = locationUpdate.getLocation(); return; } else { long currentTime = SystemClock.elapsedRealtime(); @@ -1541,17 +1619,13 @@ private void updateLocation(@Nullable final Location location, @Nullable List intermediatePoints) { @@ -1800,7 +1874,8 @@ public void onRenderModeChanged(int currentMode) { @NonNull @VisibleForTesting OnIndicatorPositionChangedListener indicatorPositionChangedListener = new OnIndicatorPositionChangedListener() { - @Override public void onIndicatorPositionChanged(@NonNull Point point) { + @Override + public void onIndicatorPositionChanged(@NonNull Point point) { for (OnIndicatorPositionChangedListener listener : onIndicatorPositionChangedListener) { listener.onIndicatorPositionChanged(point); } 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 671012a75..06fced655 100644 --- a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentOptions.java +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationComponentOptions.java @@ -1867,6 +1867,8 @@ public LocationComponentOptions.Builder layerBelow(String layerBelow) { /** * Sets the tracking animation duration multiplier. + *

+ * This value is ignored if {@link LocationUpdate.Builder#animationDuration(Long)} is provided. * * @param trackingAnimationDurationMultiplier the tracking animation duration multiplier */ diff --git a/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationUpdate.java b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationUpdate.java new file mode 100644 index 000000000..1fd17c5ea --- /dev/null +++ b/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/location/LocationUpdate.java @@ -0,0 +1,151 @@ +package com.mapbox.mapboxsdk.location; + +import android.location.Location; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Collections; +import java.util.List; + +/** + * A class that contains the location update configuration. + * + * @see LocationComponent#forceLocationUpdate(LocationUpdate) + */ +public class LocationUpdate { + + @NonNull + private final Location location; + @NonNull + private final List intermediatePoints; + @Nullable + private final Long animationDuration; + + private LocationUpdate( + @NonNull Location location, + @NonNull List intermediatePoints, + @Nullable Long animationDuration + ) { + this.location = location; + this.intermediatePoints = intermediatePoints; + this.animationDuration = animationDuration; + } + + /** + * @return target location of the transition + */ + @NonNull + public Location getLocation() { + return location; + } + + /** + * @return list of locations that are on the path to the target location for animation interpolation + */ + @NonNull + public List getIntermediatePoints() { + return intermediatePoints; + } + + /** + * @return If set, all of the transitions to this update (puck's and possibly camera's if tracking mode is engaged) + * will have the provided duration. If null, the duration will be calculated internally. + *

+ * {@link LocationComponentOptions.Builder#trackingAnimationDurationMultiplier(float)} + * is ignored if this value is provided. + */ + @Nullable + public Long getAnimationDuration() { + return animationDuration; + } + + @Override + public String toString() { + return "LocationUpdate{" + "location=" + location + ", intermediatePoints=" + intermediatePoints + + ", animationDuration=" + animationDuration + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + LocationUpdate that = (LocationUpdate) o; + + if (!location.equals(that.location)) { + return false; + } + if (!intermediatePoints.equals(that.intermediatePoints)) { + return false; + } + return animationDuration != null + ? animationDuration.equals(that.animationDuration) + : that.animationDuration == null; + } + + @Override + public int hashCode() { + int result = location.hashCode(); + result = 31 * result + intermediatePoints.hashCode(); + result = 31 * result + (animationDuration != null ? animationDuration.hashCode() : 0); + return result; + } + + public static class Builder { + + @Nullable + private Location location; + @NonNull + private List intermediatePoints = Collections.emptyList(); + @Nullable + private Long animationDuration; + + /** + * Target location. + */ + public Builder location(@Nullable Location location) { + this.location = location; + return this; + } + + /** + * This method can be used to provide the list of locations that are on the path to the target location. + * Those intermediate points are used as the animation path. + * The puck and the camera will be animated between each of the points linearly until reaching the target. + */ + public Builder intermediatePoints(@NonNull List intermediatePoints) { + this.intermediatePoints = intermediatePoints; + return this; + } + + /** + * If set, all of the transitions to this update (puck's and possibly camera's if tracking mode is engaged) will + * have the provided duration. If null, the duration will be calculated internally. + *

+ * This can also be used to disable transition animation by providing a duration equal to zero. + *

+ * {@link LocationComponentOptions.Builder#trackingAnimationDurationMultiplier(float)} + * is ignored if this value is provided. + */ + public Builder animationDuration(@Nullable Long animationDuration) { + this.animationDuration = animationDuration; + return this; + } + + /** + * Builds a new {@link LocationUpdate}. + */ + public LocationUpdate build() { + if (location == null) { + throw new IllegalArgumentException("target location has to be provided when constructing the LocationUpdate"); + } + + return new LocationUpdate(location, intermediatePoints, animationDuration); + } + } +} 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 732ac8b66..78f331574 100644 --- a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinatorTest.kt +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationAnimatorCoordinatorTest.kt @@ -7,9 +7,7 @@ import android.util.SparseArray import android.view.animation.LinearInterpolator import com.mapbox.mapboxsdk.camera.CameraPosition import com.mapbox.mapboxsdk.geometry.LatLng -import com.mapbox.mapboxsdk.location.LocationComponentConstants.DEFAULT_TRACKING_PADDING_ANIM_DURATION -import com.mapbox.mapboxsdk.location.LocationComponentConstants.DEFAULT_TRACKING_TILT_ANIM_DURATION -import com.mapbox.mapboxsdk.location.LocationComponentConstants.DEFAULT_TRACKING_ZOOM_ANIM_DURATION +import com.mapbox.mapboxsdk.location.LocationComponentConstants.* import com.mapbox.mapboxsdk.location.MapboxAnimator.* import com.mapbox.mapboxsdk.location.modes.RenderMode import com.mapbox.mapboxsdk.maps.MapboxMap @@ -108,7 +106,7 @@ class LocationAnimatorCoordinatorTest { @Test fun feedNewLocation_animatorsAreCreated() { - locationAnimatorCoordinator.feedNewLocation(Location(""), cameraPosition, false) + feedNewLocation(Location(""), cameraPosition, false) assertTrue(locationAnimatorCoordinator.animatorArray[ANIMATOR_CAMERA_LATLNG] != null) assertTrue(locationAnimatorCoordinator.animatorArray[ANIMATOR_CAMERA_GPS_BEARING] != null) @@ -122,7 +120,7 @@ class LocationAnimatorCoordinatorTest { location.latitude = 51.0 location.longitude = 17.0 location.bearing = 35f - locationAnimatorCoordinator.feedNewLocation(location, cameraPosition, false) + feedNewLocation(location, cameraPosition, false) val cameraLatLngTarget = locationAnimatorCoordinator.animatorArray[ANIMATOR_CAMERA_LATLNG]?.target as LatLng assertEquals(location.latitude, cameraLatLngTarget.latitude) @@ -154,7 +152,7 @@ class LocationAnimatorCoordinatorTest { location.latitude = 51.2 location.longitude = 17.2 location.bearing = 36f - locationAnimatorCoordinator.feedNewLocation(arrayOf(locationInter, location), cameraPosition, false, false) + locationAnimatorCoordinator.feedNewLocation(arrayOf(locationInter, location), cameraPosition, false, null) val cameraLatLngTarget = locationAnimatorCoordinator.animatorArray[ANIMATOR_CAMERA_LATLNG]?.target as LatLng assertEquals(location.latitude, cameraLatLngTarget.latitude) @@ -198,7 +196,7 @@ class LocationAnimatorCoordinatorTest { current.longitude = 17.2 current.bearing = 0f - locationAnimatorCoordinator.feedNewLocation(arrayOf(previous, current), cameraPosition, false, false) + locationAnimatorCoordinator.feedNewLocation(arrayOf(previous, current), cameraPosition, false, null) verify { animatorProvider.floatAnimator( @@ -206,7 +204,7 @@ class LocationAnimatorCoordinatorTest { ) } - locationAnimatorCoordinator.feedNewLocation(arrayOf(previous, current), cameraPosition, true, false) + locationAnimatorCoordinator.feedNewLocation(arrayOf(previous, current), cameraPosition, true, null) verify { animatorProvider.floatAnimator( @@ -216,7 +214,7 @@ class LocationAnimatorCoordinatorTest { } @Test - fun feedNewLocation_animatorValue_multiplePoints_animationDuration() { + fun feedNewLocation_animatorValue_multiplePoints_animationDurationDefaultsToZero() { every { projection.getMetersPerPixelAtLatitude(any()) } answers { 10000.0 } // disable snap val locationInter = Location("") locationInter.latitude = 51.1 @@ -226,7 +224,7 @@ class LocationAnimatorCoordinatorTest { location.latitude = 51.2 location.longitude = 17.2 location.bearing = 36f - locationAnimatorCoordinator.feedNewLocation(arrayOf(locationInter, location), cameraPosition, false, false) + locationAnimatorCoordinator.feedNewLocation(arrayOf(locationInter, location), cameraPosition, false, null) verify { animatorSetProvider.startAnimation(eq(listOf( @@ -239,7 +237,7 @@ class LocationAnimatorCoordinatorTest { } @Test - fun feedNewLocation_animatorValue_multiplePoints_animationDuration_lookAhead() { + fun feedNewLocation_animatorValue_multiplePoints_externalAnimationDuration() { every { projection.getMetersPerPixelAtLatitude(any()) } answers { 10000.0 } // disable snap val locationInter = Location("") locationInter.latitude = 51.1 @@ -249,8 +247,7 @@ class LocationAnimatorCoordinatorTest { location.latitude = 51.2 location.longitude = 17.2 location.bearing = 36f - location.time = System.currentTimeMillis() + 2000 - locationAnimatorCoordinator.feedNewLocation(arrayOf(locationInter, location), cameraPosition, false, true) + locationAnimatorCoordinator.feedNewLocation(arrayOf(locationInter, location), cameraPosition, false, 1300) verify { animatorSetProvider.startAnimation(eq(listOf( @@ -258,7 +255,7 @@ class LocationAnimatorCoordinatorTest { locationAnimatorCoordinator.animatorArray[ANIMATOR_LAYER_GPS_BEARING], locationAnimatorCoordinator.animatorArray[ANIMATOR_CAMERA_LATLNG], locationAnimatorCoordinator.animatorArray[ANIMATOR_CAMERA_GPS_BEARING] - )), any(), more(1500L)) + )), any(), 1300) } } @@ -273,7 +270,7 @@ class LocationAnimatorCoordinatorTest { every { animator.animatedValue } returns 270f locationAnimatorCoordinator.animatorArray.put(ANIMATOR_LAYER_GPS_BEARING, animator) - locationAnimatorCoordinator.feedNewLocation(location, cameraPosition, false) + feedNewLocation(location, cameraPosition, false) val layerBearingTarget = locationAnimatorCoordinator.animatorArray[ANIMATOR_LAYER_GPS_BEARING]?.target as Float assertEquals(360f, layerBearingTarget) @@ -290,7 +287,7 @@ class LocationAnimatorCoordinatorTest { every { animator.animatedValue } returns 280f locationAnimatorCoordinator.animatorArray.put(ANIMATOR_LAYER_GPS_BEARING, animator) - locationAnimatorCoordinator.feedNewLocation(location, cameraPosition, false) + feedNewLocation(location, cameraPosition, false) val layerBearingTarget = locationAnimatorCoordinator.animatorArray[ANIMATOR_LAYER_GPS_BEARING]?.target as Float assertEquals(450f, layerBearingTarget) @@ -307,7 +304,7 @@ class LocationAnimatorCoordinatorTest { every { animator.animatedValue } returns 450f locationAnimatorCoordinator.animatorArray.put(ANIMATOR_LAYER_GPS_BEARING, animator) - locationAnimatorCoordinator.feedNewLocation(location, cameraPosition, false) + feedNewLocation(location, cameraPosition, false) val layerBearingTarget = locationAnimatorCoordinator.animatorArray[ANIMATOR_LAYER_GPS_BEARING]?.target as Float assertEquals(-60f, layerBearingTarget) @@ -324,7 +321,7 @@ class LocationAnimatorCoordinatorTest { every { animator.animatedValue } returns 10f locationAnimatorCoordinator.animatorArray.put(ANIMATOR_LAYER_GPS_BEARING, animator) - locationAnimatorCoordinator.feedNewLocation(location, cameraPosition, false) + feedNewLocation(location, cameraPosition, false) val layerBearingTarget = locationAnimatorCoordinator.animatorArray[ANIMATOR_LAYER_GPS_BEARING]?.target as Float assertEquals(-10f, layerBearingTarget) @@ -341,7 +338,7 @@ class LocationAnimatorCoordinatorTest { every { animator.animatedValue } returns -280f locationAnimatorCoordinator.animatorArray.put(ANIMATOR_LAYER_GPS_BEARING, animator) - locationAnimatorCoordinator.feedNewLocation(location, cameraPosition, false) + feedNewLocation(location, cameraPosition, false) val layerBearingTarget = locationAnimatorCoordinator.animatorArray[ANIMATOR_LAYER_GPS_BEARING]?.target as Float assertEquals(90f, layerBearingTarget) @@ -358,7 +355,7 @@ class LocationAnimatorCoordinatorTest { every { animator.animatedValue } returns -350f locationAnimatorCoordinator.animatorArray.put(ANIMATOR_LAYER_GPS_BEARING, animator) - locationAnimatorCoordinator.feedNewLocation(location, cameraPosition, false) + feedNewLocation(location, cameraPosition, false) val layerBearingTarget = locationAnimatorCoordinator.animatorArray[ANIMATOR_LAYER_GPS_BEARING]?.target as Float assertEquals(-90f, layerBearingTarget) @@ -370,7 +367,7 @@ class LocationAnimatorCoordinatorTest { location.latitude = 51.0 location.longitude = 17.0 location.bearing = 35f - locationAnimatorCoordinator.feedNewLocation(location, cameraPosition, false) + feedNewLocation(location, cameraPosition, false) assertTrue(locationAnimatorCoordinator.animatorArray[ANIMATOR_CAMERA_LATLNG] != null) assertTrue(locationAnimatorCoordinator.animatorArray[ANIMATOR_CAMERA_GPS_BEARING] != null) @@ -384,7 +381,7 @@ class LocationAnimatorCoordinatorTest { location.latitude = 51.0 location.longitude = 17.0 location.bearing = 35f - locationAnimatorCoordinator.feedNewLocation(location, cameraPosition, true) + feedNewLocation(location, cameraPosition, true) val cameraLatLngTarget = locationAnimatorCoordinator.animatorArray[ANIMATOR_CAMERA_LATLNG]?.target as LatLng assertEquals(cameraLatLngTarget.latitude, cameraLatLngTarget.latitude) @@ -534,7 +531,7 @@ class LocationAnimatorCoordinatorTest { @Test fun cancelAllAnimators() { - locationAnimatorCoordinator.feedNewLocation(Location(""), cameraPosition, true) + feedNewLocation(Location(""), cameraPosition, true) assertTrue(locationAnimatorCoordinator.animatorArray[ANIMATOR_CAMERA_LATLNG].isStarted) locationAnimatorCoordinator.cancelAllAnimations() @@ -729,7 +726,7 @@ class LocationAnimatorCoordinatorTest { @Test fun maxFps_givenToAnimator() { locationAnimatorCoordinator.setMaxAnimationFps(5) - locationAnimatorCoordinator.feedNewLocation(Location(""), cameraPosition, false) + feedNewLocation(Location(""), cameraPosition, false) verify { animatorProvider.latLngAnimator(any(), any(), 5) } verify { animatorProvider.floatAnimator(any(), any(), 5) } } @@ -750,6 +747,14 @@ class LocationAnimatorCoordinatorTest { } } } + + private fun feedNewLocation( + newLocation: Location, + currentCameraPosition: CameraPosition, + isGpsNorth: Boolean + ) { + locationAnimatorCoordinator.feedNewLocation(arrayOf(newLocation), currentCameraPosition, isGpsNorth, null) + } } private fun SparseArray.contains(listener: AnimationsValueChangeListener<*>?): Boolean { diff --git a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationComponentTest.kt b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationComponentTest.kt index 3c9d4865e..5ddb5df0d 100644 --- a/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationComponentTest.kt +++ b/MapboxGLAndroidSDK/src/test/java/com/mapbox/mapboxsdk/location/LocationComponentTest.kt @@ -6,7 +6,9 @@ import android.content.res.TypedArray import android.location.Location import android.os.Looper import com.mapbox.android.core.location.LocationEngine +import com.mapbox.android.core.location.LocationEngineCallback import com.mapbox.android.core.location.LocationEngineRequest +import com.mapbox.android.core.location.LocationEngineResult import com.mapbox.mapboxsdk.R import com.mapbox.mapboxsdk.camera.CameraPosition import com.mapbox.mapboxsdk.location.LocationComponentConstants.TRANSITION_ANIMATION_DURATION_MS @@ -20,10 +22,8 @@ import org.junit.Assert import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Mock +import org.mockito.* import org.mockito.Mockito.* -import org.mockito.MockitoAnnotations import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) @@ -671,4 +671,112 @@ class LocationComponentTest { verify(locationAnimatorCoordinator).feedNewAccuracyRadius(location.accuracy, false) } + + @Test + fun newLocation_lookAhead_animationDurationGenerated() { + val location = Location("test") + location.accuracy = 50f + location.time = System.currentTimeMillis() + 2000 + `when`(style.isFullyLoaded).thenReturn(true) + locationComponent = LocationComponent(mapboxMap, transform, developerAnimationListeners, currentListener, lastListener, locationLayerController, locationCameraController, locationAnimatorCoordinator, staleStateManager, compassEngine, locationEngineProvider, true) + locationComponent.activateLocationComponent( + LocationComponentActivationOptions.builder(context, style) + .locationComponentOptions(locationComponentOptions) + .useSpecializedLocationLayer(true) + .useDefaultLocationEngine(false) + .build() + ) + locationComponent.isLocationComponentEnabled = true + locationComponent.onStart() + val locations: List = listOf( + mock(Location::class.java), + location + ) + locationComponent.forceLocationUpdate(locations, true) + + verify(locationAnimatorCoordinator).feedNewLocation(eq(locations.toTypedArray()), any(), eq(false), AdditionalMatchers.gt(1500L)) + } + + @Test + fun newLocation_nullAnimationDurationPassed() { + val location = Location("test") + location.accuracy = 50f + `when`(style.isFullyLoaded).thenReturn(true) + locationComponent = LocationComponent(mapboxMap, transform, developerAnimationListeners, currentListener, lastListener, locationLayerController, locationCameraController, locationAnimatorCoordinator, staleStateManager, compassEngine, locationEngineProvider, true) + locationComponent.activateLocationComponent( + LocationComponentActivationOptions.builder(context, style) + .locationComponentOptions(locationComponentOptions) + .useSpecializedLocationLayer(true) + .useDefaultLocationEngine(false) + .build() + ) + locationComponent.isLocationComponentEnabled = true + locationComponent.onStart() + locationComponent.forceLocationUpdate( + LocationUpdate.Builder() + .location(location) + .build() + ) + verify(locationAnimatorCoordinator).feedNewLocation(eq(listOf(location).toTypedArray()), any(), eq(false), isNull()) + } + + @Test + fun whenComponentReEnabled_noEngine_animationDurationZero() { + val location = Location("test") + location.accuracy = 50f + `when`(style.isFullyLoaded).thenReturn(true) + locationComponent = LocationComponent(mapboxMap, transform, developerAnimationListeners, currentListener, lastListener, locationLayerController, locationCameraController, locationAnimatorCoordinator, staleStateManager, compassEngine, locationEngineProvider, true) + locationComponent.activateLocationComponent( + LocationComponentActivationOptions.builder(context, style) + .locationComponentOptions(locationComponentOptions) + .useSpecializedLocationLayer(true) + .useDefaultLocationEngine(false) + .build() + ) + locationComponent.isLocationComponentEnabled = true + locationComponent.onStart() + locationComponent.forceLocationUpdate( + LocationUpdate.Builder() + .location(location) + .animationDuration(1500L) + .build() + ) + verify(locationAnimatorCoordinator).feedNewLocation(eq(listOf(location).toTypedArray()), any(), eq(false), eq(1500L)) + + locationComponent.onStop() + locationComponent.onStart() + verify(locationAnimatorCoordinator).feedNewLocation(eq(listOf(location).toTypedArray()), any(), eq(false), eq(0L)) + } + + @Test + fun whenComponentReEnabled_hasEngine_animationDurationZero() { + val engineRequest = LocationEngineRequest.Builder( + 1000L + ) + .setFastestInterval(0L) + .build() + val location = Location("test") + location.accuracy = 50f + val locationEngine = mock(LocationEngine::class.java) + `when`(style.isFullyLoaded).thenReturn(true) + locationComponent = LocationComponent(mapboxMap, transform, developerAnimationListeners, currentListener, locationLayerController, locationCameraController, locationAnimatorCoordinator, staleStateManager, compassEngine, locationEngineProvider, true, engineRequest) + locationComponent.activateLocationComponent( + LocationComponentActivationOptions.builder(context, style) + .locationComponentOptions(locationComponentOptions) + .useSpecializedLocationLayer(true) + .useDefaultLocationEngine(false) + .locationEngine(locationEngine) + .build() + ) + locationComponent.isLocationComponentEnabled = true + locationComponent.onStart() + + val capture: ArgumentCaptor> = ArgumentCaptor.forClass(LocationEngineCallback::class.java as Class>) + verify(locationEngine).getLastLocation(capture.capture()) + val result = mock(LocationEngineResult::class.java) + `when`(result.lastLocation).thenReturn(location) + capture.value.onSuccess(result) + + verify(locationAnimatorCoordinator).feedNewLocation(eq(listOf(location).toTypedArray()), any(), eq(false), eq(0L)) + } } \ No newline at end of file