Skip to content
This repository has been archived by the owner on Jun 21, 2023. It is now read-only.

Expose option to pass intermediate points for smooth location animation #166

Merged
merged 2 commits into from
Feb 11, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
import android.animation.Animator;
import android.location.Location;
import android.os.SystemClock;
import android.util.SparseArray;
import android.view.animation.LinearInterpolator;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Size;
import androidx.annotation.VisibleForTesting;
import android.util.SparseArray;
import android.view.animation.LinearInterpolator;

import com.mapbox.mapboxsdk.camera.CameraPosition;
import com.mapbox.mapboxsdk.geometry.LatLng;
Expand All @@ -33,6 +35,8 @@
import static com.mapbox.mapboxsdk.location.MapboxAnimator.ANIMATOR_TILT;
import static com.mapbox.mapboxsdk.location.MapboxAnimator.ANIMATOR_ZOOM;
import static com.mapbox.mapboxsdk.location.Utils.immediateAnimation;
import static com.mapbox.mapboxsdk.location.Utils.normalize;
import static com.mapbox.mapboxsdk.location.Utils.shortestRotation;

final class LocationAnimatorCoordinator {

Expand Down Expand Up @@ -74,6 +78,12 @@ void updateAnimatorListenerHolders(@NonNull Set<AnimatorListenerHolder> 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) {
Location newLocation = newLocations[newLocations.length - 1];
if (previousLocation == null) {
previousLocation = newLocation;
locationUpdateTimestamp = SystemClock.elapsedRealtime() - TRANSITION_ANIMATION_DURATION_MS;
Expand All @@ -82,16 +92,23 @@ void feedNewLocation(@NonNull Location newLocation, @NonNull CameraPosition curr
LatLng previousLayerLatLng = getPreviousLayerLatLng();
float previousLayerBearing = getPreviousLayerGpsBearing();
LatLng previousCameraLatLng = currentCameraPosition.target;
float previousCameraBearing = (float) currentCameraPosition.bearing;
float previousCameraBearing = normalize((float) currentCameraPosition.bearing);

LatLng targetLatLng = new LatLng(newLocation);
float targetLayerBearing = newLocation.getBearing();
float targetCameraBearing = newLocation.getBearing();
targetCameraBearing = checkGpsNorth(isGpsNorth, targetCameraBearing);
// generate targets for layer
LatLng[] latLngValues = getLatLngValues(previousLayerLatLng, newLocations);
Float[] bearingValues = getBearingValues(previousLayerBearing, newLocations);
updateLayerAnimators(latLngValues, bearingValues);

updateLayerAnimators(previousLayerLatLng, targetLatLng, previousLayerBearing, targetLayerBearing);
updateCameraAnimators(previousCameraLatLng, previousCameraBearing, targetLatLng, targetCameraBearing);
// replace the animation start with the camera's previous value
latLngValues[0] = previousCameraLatLng;
if (isGpsNorth) {
bearingValues = new Float[] {previousCameraBearing, 0f};
} else {
bearingValues[0] = previousCameraBearing;
}
updateCameraAnimators(latLngValues, bearingValues);

LatLng targetLatLng = new LatLng(newLocation);
boolean snap = immediateAnimation(projection, previousCameraLatLng, targetLatLng)
|| immediateAnimation(projection, previousLayerLatLng, targetLatLng);

Expand All @@ -102,6 +119,15 @@ void feedNewLocation(@NonNull Location newLocation, @NonNull CameraPosition curr

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");
} else {
animationDuration = newLocation.getTime() - currentTimestamp;
}
} else {
animationDuration = (long) ((locationUpdateTimestamp - previousUpdateTimeStamp) * durationMultiplier)
/* make animation slightly longer with durationMultiplier, defaults to 1.1f */;
Expand Down Expand Up @@ -207,23 +233,35 @@ private float getPreviousAccuracyRadius() {
return previousRadius;
}

private void updateLayerAnimators(LatLng previousLatLng, LatLng targetLatLng,
float previousBearing, float targetBearing) {
createNewLatLngAnimator(ANIMATOR_LAYER_LATLNG, previousLatLng, targetLatLng);
private LatLng[] getLatLngValues(LatLng previousLatLng, Location[] targetLocations) {
LatLng[] latLngs = new LatLng[targetLocations.length + 1];
latLngs[0] = previousLatLng;
for (int i = 1; i < latLngs.length; i++) {
latLngs[i] = new LatLng(targetLocations[i - 1]);
}
return latLngs;
}

private Float[] getBearingValues(Float previousBearing, Location[] targetLocations) {
Float[] bearings = new Float[targetLocations.length + 1];

// Because Location bearing values are normalized to [0, 360]
// we need to do the same for the previous bearing value to determine the shortest path
previousBearing = Utils.normalize(previousBearing);
float normalizedLayerBearing = Utils.shortestRotation(targetBearing, previousBearing);
createNewFloatAnimator(ANIMATOR_LAYER_GPS_BEARING, previousBearing, normalizedLayerBearing);
bearings[0] = normalize(previousBearing);
for (int i = 1; i < bearings.length; i++) {
bearings[i] = shortestRotation(targetLocations[i - 1].getBearing(), bearings[i - 1]);
}
return bearings;
}

private void updateCameraAnimators(LatLng previousCameraLatLng, float previousCameraBearing,
LatLng targetLatLng, float targetBearing) {
createNewLatLngAnimator(ANIMATOR_CAMERA_LATLNG, previousCameraLatLng, targetLatLng);
private void updateLayerAnimators(LatLng[] latLngValues, Float[] bearingValues) {
createNewLatLngAnimator(ANIMATOR_LAYER_LATLNG, latLngValues);
createNewFloatAnimator(ANIMATOR_LAYER_GPS_BEARING, bearingValues);
}

float normalizedCameraBearing = Utils.shortestRotation(targetBearing, previousCameraBearing);
createNewFloatAnimator(ANIMATOR_CAMERA_GPS_BEARING, previousCameraBearing, normalizedCameraBearing);
private void updateCameraAnimators(LatLng[] latLngValues, Float[] bearingValues) {
createNewLatLngAnimator(ANIMATOR_CAMERA_LATLNG, latLngValues);
createNewFloatAnimator(ANIMATOR_CAMERA_GPS_BEARING, bearingValues);
}

private void updateCompassAnimators(float targetCompassBearing, float previousLayerBearing,
Expand All @@ -241,36 +279,45 @@ private void updateAccuracyAnimators(float targetAccuracyRadius, float previousA

private void updateZoomAnimator(float targetZoomLevel, float previousZoomLevel,
@Nullable MapboxMap.CancelableCallback cancelableCallback) {
createNewCameraAdapterAnimator(ANIMATOR_ZOOM, previousZoomLevel, targetZoomLevel, cancelableCallback);
createNewCameraAdapterAnimator(ANIMATOR_ZOOM, new Float[] {previousZoomLevel, targetZoomLevel}, cancelableCallback);
}

private void updateTiltAnimator(float targetTilt, float previousTiltLevel,
@Nullable MapboxMap.CancelableCallback cancelableCallback) {
createNewCameraAdapterAnimator(ANIMATOR_TILT, previousTiltLevel, targetTilt, cancelableCallback);
createNewCameraAdapterAnimator(ANIMATOR_TILT, new Float[] {previousTiltLevel, targetTilt}, cancelableCallback);
}

private void createNewLatLngAnimator(@MapboxAnimator.Type int animatorType, LatLng previous, LatLng target) {
createNewLatLngAnimator(animatorType, new LatLng[] {previous, target});
}

private void createNewLatLngAnimator(@MapboxAnimator.Type int animatorType, LatLng[] values) {
cancelAnimator(animatorType);
MapboxAnimator.AnimationsValueChangeListener listener = listeners.get(animatorType);
if (listener != null) {
animatorArray.put(animatorType, animatorProvider.latLngAnimator(previous, target, listener, maxAnimationFps));
animatorArray.put(animatorType, animatorProvider.latLngAnimator(values, listener, maxAnimationFps));
}
}

private void createNewFloatAnimator(@MapboxAnimator.Type int animatorType, float previous, float target) {
createNewFloatAnimator(animatorType, new Float[] {previous, target});
}

private void createNewFloatAnimator(@MapboxAnimator.Type int animatorType, @NonNull @Size(min = 2) Float[] values) {
cancelAnimator(animatorType);
MapboxAnimator.AnimationsValueChangeListener listener = listeners.get(animatorType);
if (listener != null) {
animatorArray.put(animatorType, animatorProvider.floatAnimator(previous, target, listener, maxAnimationFps));
animatorArray.put(animatorType, animatorProvider.floatAnimator(values, listener, maxAnimationFps));
}
}

private void createNewCameraAdapterAnimator(@MapboxAnimator.Type int animatorType, float previous, float target,
private void createNewCameraAdapterAnimator(@MapboxAnimator.Type int animatorType,
@NonNull @Size(min = 2) Float[] values,
@Nullable MapboxMap.CancelableCallback cancelableCallback) {
cancelAnimator(animatorType);
MapboxAnimator.AnimationsValueChangeListener listener = listeners.get(animatorType);
if (listener != null) {
animatorArray.put(animatorType, animatorProvider.cameraAnimator(previous, target, listener, cancelableCallback));
animatorArray.put(animatorType, animatorProvider.cameraAnimator(values, listener, cancelableCallback));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
import android.location.Location;
import android.os.Looper;
import android.os.SystemClock;
import android.view.WindowManager;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresPermission;
import androidx.annotation.StyleRes;
import androidx.annotation.VisibleForTesting;
import android.view.WindowManager;

import com.mapbox.android.core.location.LocationEngine;
import com.mapbox.android.core.location.LocationEngineCallback;
Expand Down Expand Up @@ -889,6 +890,32 @@ public void forceLocationUpdate(@Nullable Location location) {
updateLocation(location, false);
}

/**
* Use to either force a location update or to manually control when the user location gets
* updated.
* <p>
* This method can be used to provide the list of locations where the last one is the target location
* and the rest are intermediate points used as the animation path.
* The puck and the camera will be animated between each of the points linearly until reaching the target.
*
* @param locations where the location icon is placed on the map
* @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.
*/
public void forceLocationUpdate(@Nullable List<Location> locations, boolean lookAheadUpdate) {
checkActivationState();
if (locations != null && locations.size() >= 1) {
updateLocation(
locations.get(locations.size() - 1), // target location
locations.subList(0, locations.size() - 1), // intermediate locations
false,
lookAheadUpdate);
} else {
updateLocation(null, false);
}
}

/**
* Set max FPS at which location animators can output updates. The throttling will only impact the location puck
* and camera tracking smooth animations.
Expand Down Expand Up @@ -1341,6 +1368,11 @@ 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);
}

private void updateLocation(@Nullable final Location location, @Nullable List<Location> intermediatePoints,
boolean fromLastLocation, boolean lookAheadUpdate) {
if (location == null) {
return;
} else if (!isLayerReady) {
Expand All @@ -1362,11 +1394,28 @@ private void updateLocation(@Nullable final Location location, boolean fromLastL
}
CameraPosition currentCameraPosition = mapboxMap.getCameraPosition();
boolean isGpsNorth = getCameraMode() == CameraMode.TRACKING_GPS_NORTH;
locationAnimatorCoordinator.feedNewLocation(location, currentCameraPosition, isGpsNorth);
if (intermediatePoints != null) {
locationAnimatorCoordinator.feedNewLocation(
getTargetLocationWithIntermediates(location, intermediatePoints),
currentCameraPosition,
isGpsNorth,
lookAheadUpdate);
} else {
locationAnimatorCoordinator.feedNewLocation(location, currentCameraPosition, isGpsNorth);
}
updateAccuracyRadius(location, false);
lastLocation = location;
}

private Location[] getTargetLocationWithIntermediates(Location location, List<Location> intermediatePoints) {
Location[] locations = new Location[intermediatePoints.size() + 1];
locations[locations.length - 1] = location;
for (int i = 0; i < intermediatePoints.size(); i++) {
locations[i] = intermediatePoints.get(i);
}
return locations;
}

private void showLocationLayerIfHidden() {
boolean isLocationLayerHidden = locationLayerController.isHidden();
if (isEnabled && isComponentStarted && isLocationLayerHidden) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
import android.animation.AnimatorListenerAdapter;
import android.animation.TypeEvaluator;
import android.animation.ValueAnimator;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Size;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
Expand Down Expand Up @@ -48,13 +50,13 @@ abstract class MapboxAnimator<K> extends ValueAnimator implements ValueAnimator.
private final double minUpdateInterval;
private long timeElapsed;

MapboxAnimator(@NonNull K previous, @NonNull K target, @NonNull AnimationsValueChangeListener<K> updateListener,
MapboxAnimator(@NonNull @Size(min = 2) K[] values, @NonNull AnimationsValueChangeListener<K> updateListener,
int maxAnimationFps) {
minUpdateInterval = 1E9 / maxAnimationFps;
setObjectValues(previous, target);
setObjectValues((Object[]) values);
setEvaluator(provideEvaluator());
this.updateListener = updateListener;
this.target = target;
this.target = values[values.length - 1];
addUpdateListener(this);
addListener(new AnimatorListener());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,19 @@ public static MapboxAnimatorProvider getInstance() {
return INSTANCE;
}

MapboxLatLngAnimator latLngAnimator(LatLng previous, LatLng target,
MapboxAnimator.AnimationsValueChangeListener updateListener,
MapboxLatLngAnimator latLngAnimator(LatLng[] values, MapboxAnimator.AnimationsValueChangeListener updateListener,
int maxAnimationFps) {
return new MapboxLatLngAnimator(previous, target, updateListener, maxAnimationFps);
return new MapboxLatLngAnimator(values, updateListener, maxAnimationFps);
}

MapboxFloatAnimator floatAnimator(Float previous, Float target,
MapboxAnimator.AnimationsValueChangeListener updateListener,
MapboxFloatAnimator floatAnimator(Float[] values, MapboxAnimator.AnimationsValueChangeListener updateListener,
int maxAnimationFps) {
return new MapboxFloatAnimator(previous, target, updateListener, maxAnimationFps);
return new MapboxFloatAnimator(values, updateListener, maxAnimationFps);
}

MapboxCameraAnimatorAdapter cameraAnimator(Float previous, Float target,
MapboxCameraAnimatorAdapter cameraAnimator(Float[] values,
MapboxAnimator.AnimationsValueChangeListener updateListener,
@Nullable MapboxMap.CancelableCallback cancelableCallback) {
return new MapboxCameraAnimatorAdapter(previous, target, updateListener, cancelableCallback);
return new MapboxCameraAnimatorAdapter(values, updateListener, cancelableCallback);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,21 @@

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Size;

import com.mapbox.mapboxsdk.maps.MapboxMap;

class MapboxCameraAnimatorAdapter extends MapboxFloatAnimator {
@Nullable
private final MapboxMap.CancelableCallback cancelableCallback;

MapboxCameraAnimatorAdapter(Float previous, Float target,
MapboxCameraAnimatorAdapter(@NonNull @Size(min = 2) Float[] values,
AnimationsValueChangeListener updateListener,
@Nullable MapboxMap.CancelableCallback cancelableCallback) {
super(previous, target, updateListener, Integer.MAX_VALUE);
super(values, updateListener, Integer.MAX_VALUE);
this.cancelableCallback = cancelableCallback;
addListener(new MapboxAnimatorListener());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

import android.animation.FloatEvaluator;
import android.animation.TypeEvaluator;

import androidx.annotation.NonNull;
import androidx.annotation.Size;

class MapboxFloatAnimator extends MapboxAnimator<Float> {
MapboxFloatAnimator(Float previous, Float target, AnimationsValueChangeListener updateListener, int maxAnimationFps) {
super(previous, target, updateListener, maxAnimationFps);
MapboxFloatAnimator(@NonNull @Size(min = 2) Float[] values,
@NonNull AnimationsValueChangeListener updateListener, int maxAnimationFps) {
super(values, updateListener, maxAnimationFps);
}

@NonNull
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
package com.mapbox.mapboxsdk.location;

import android.animation.TypeEvaluator;

import androidx.annotation.NonNull;

import com.mapbox.mapboxsdk.geometry.LatLng;

class MapboxLatLngAnimator extends MapboxAnimator<LatLng> {

MapboxLatLngAnimator(LatLng previous, LatLng target, AnimationsValueChangeListener updateListener,
MapboxLatLngAnimator(@NonNull LatLng[] values, @NonNull AnimationsValueChangeListener updateListener,
int maxAnimationFps) {
super(previous, target, updateListener, maxAnimationFps);
super(values, updateListener, maxAnimationFps);
}

@NonNull
Expand Down
Loading