Camera
The HERE SDK for Android provides several ways to change the view of the map. While with map styles you can change the look of the map, you can use a camera to look at the map from different perspectives.
For example, the HERE SDK for Android allows you to set a target location, tilt the map, zoom in and out or set a bearing.
Note
At a glance
- Use the
Camera
returned by mapView.getCamera()
to manipulate the view of the map. - Call
camera.updateCamera(CameraUpdate cameraUpdate)
to set all camera properties in one go. - Monitor changes to a camera by registering a
CameraObserver
. - Put constraints on a camera by setting limits to the
CameraLimits
object returned by camera.getLimits()
. - Convert between view and geographical coordinate spaces by using
geoToViewCoordinates()
and viewToGeoCoordinates()
. - Get the bounds of the currently displayed area by calling
getBoundingBox()
.
By default, the camera is located centered above the map. From a bird's eye view looking straight-down, the map is oriented North-up. This means that on your device, the top edge is pointing to the north of the map.
Rotate the Map
You can change the orientation of the map by setting a bearing angle. 'Bearing' is a navigational term, counted in degrees, from the North in clockwise direction.
Illustration: Set a bearing direction.
By default, the map is set to a bearing of 0°. When setting an angle of 45°, as visualized in the illustration above, the map rotates counter-clockwise and the direction of the bearing becomes the new upward direction on your map, pointing to the top edge of your device. This is similar to holding and rotating a paper map while hiking in a certain direction. Apparently, it is easier to orient yourself if the map's top edge points in the direction in which you want to go. However, this will not always be the true North direction (bearing = 0°).
Tilt the Map
The camera can also be used to turn the flat 2D map surface to a 3D perspective to see, for example, roads at a greater distance that may appear towards the horizon. By default, the map is not tilted (tilt = 0°).
The tilt angle is calculated from the vertical axis at the target location. This direction pointing straight-down below the observer is called the nadir. As visualized in the illustration below, tilting the map by 45° will change the bird's eye view of the camera to a 3D perspective. Although this will effectively move the camera, any subsequent tilt values will always be applied from the camera's default location. Tilt values above and below the limits are clamped, otherwise, no map may be visible. These absolute values are also available as constant for the minimum value (CameraLimits.MIN_TILT
), or can be retrieved at runtime:
try {
cameraLimits.setMaxTilt(CameraLimits.getMaxTiltForZoomLevel(12));
} catch (CameraLimits.CameraLimitsException e) {
}
This way, you can specify your own camera limits within the absolute range.
Illustration: Tilt the map.
Change the Map Location
By setting a new camera target, you can change the map location. By default, the target is centered on the map. Overall, using the camera is very simple. See some examples in the following code snippets:
mapView.getCamera().setTarget(new GeoCoordinates(52.518043, 13.405991));
mapView.getCamera().setZoomLevel(14);
mapView.getCamera().setTilt(60);
Alternatively, you can apply multiple changes in one go by setting a CameraUpdate
.
The camera also allows you to specify a custom range to limit specific values, and it provides a way to observe updates on these values, for example, when a user has interacted with the map by performing a double tap gesture.
By setting a new target anchor point, you can change the default camera anchor point which is located at x = 0.5, y = 0.5 - this indicates the center of the map view. The target location will be used for all programmatical map transformations like rotate and tilt - or when setting a new target location. It does not affect default gestures like pinch rotate and two finger pan to tilt the map. This is how to set a new target anchor point:
Anchor2D transformationCenter = new Anchor2D(normalizedX, normalizedY);
camera.setTargetAnchorPoint(transformationCenter);
The anchor point can be specified with normalized coordinates where (0, 0) marks the top-left corner and (1, 1) the bottom-right corner of the map view. Therefore, after setting the anchor to (1, 1) any new target location would appear at the bottom-right corner of the map view. Values outside the map view will be clamped.
Setting a new anchor point for the target has no visible effect on the map - until you set a new target location, or when tilting or rotating the map programmatically: The tile angle is located at the horizontal coordinate of the anchor. Likewise, the rotation center point is equal to the anchor.
You can find an example on how to make use of target anchor points in this tutorial. It shows how to zoom in at a specific point on the map view.
Listen to Camera Changes
By adding an observer, your class can be notified when the camera (and thus the map) is changed:
private final CameraObserver cameraObserver = new CameraObserver() {
@Override
public void onCameraUpdated(@NonNull CameraUpdate cameraUpdate) {
GeoCoordinates mapCenter = cameraUpdate.target;
Log.d(TAG, "Current map center location: " + mapCenter +
" Current zoom level: " + cameraUpdate.zoomLevel);
}
};
private void addCameraObserver() {
mapView.getCamera().addObserver(cameraObserver);
}
In addition, you can manually perform fast and smooth interactions with the map. By default, a double tap zooms in, and panning allows you to move the map around with your fingers. You can read more about default map behaviors in the Gestures section.
Tutorial - Animate to a Location
By setting a new camera target, you can instantly jump to any location on the map - without delay. However, for some scenarios, it may be more user-friendly to show a map that moves slowly from the current location to a new location.
Such a Move-To-XY method can be realized by interpolating between the current and the new geographic coordinates. Each intermediate set of latitude / longitude pairs can then be set as the new camera target. Luckily, this animated transition is easy to achieve with Android's animation framework.
As you may know, each animation done with this framework can contain a set of ValueAnimator
instances that allow to specify a TimeInterpolator
. An interpolator defines how fast (or slow) a value should change over time. For our purpose, we choose the AccelerateDecelerateInterpolator
that provides a nice slowing down effect at the end of the animation. An AnimatorSet
helps to animate multiple values at the same time: Then we may not only transition from one coordinate to another, but also adjust other values like rotation, tilt and zoom level.
To get started, we first define how our interfaces should look like. It's best to hold all animation code separated from the rest of your application code. Therefore we decide to create a new class called CameraAnimator
. Usage should look like this:
private static final float DEFAULT_ZOOM_LEVEL = 14;
...
cameraAnimator = new CameraAnimator(camera);
cameraAnimator.setTimeInterpolator(new AccelerateDecelerateInterpolator());
...
cameraAnimator.moveTo(geoCoordinates, DEFAULT_ZOOM_LEVEL);
That's all we are about to show in this tutorial. First, we need to create a new CameraAnimator
instance that requires a Camera
object as dependency. Then we would like to experiment with different interpolators offered by the Android animation framework. Therefore, we allow our class to accept any TimeInterpolator
instance. Finally, our moveTo()
method accepts the new camera target location and the desired zoom level.
The implementation of the moveTo()
method should take care to start the animation. The animation should also stop automatically after it has ended (as we show later):
public void moveTo(GeoCoordinates destination, double targetZoom) {
CameraUpdate targetCameraUpdate = createTargetCameraUpdate(destination, targetZoom);
createAnimation(targetCameraUpdate);
startAnimation(targetCameraUpdate);
}
Let's go through this method line by line. We start by creating a new CameraUpdate
as want to animate not only the location, but also other camera parameters like tilt and rotation. This is how we create a new CameraUpdate
:
private CameraUpdate createTargetCameraUpdate(GeoCoordinates destination, double targetZoom) {
double targetTilt = 0;
double targetBearing = camera.getBearing() > 180 ? 360 : 0;
return new CameraUpdate(targetTilt, targetBearing, targetZoom, destination);
}
As you can see from the implementation, we define the end values of each property when the animation is finished. Irrespective of what tilt value is currently set, we would like to animate back to a non-tilted map (targetTilt = 0
). For the rotation, we need to decide which bearing value is faster to reach: A non-rotated map has a bearing of 0°, which is the same as 360° - therefore we check the current bearing value: If it is 200°, for example, it is faster for us to animate until we reach 360°.
The resulting CameraUpdate
instance holds all desired values of the intended end state. We need this class to create the actual animation:
private void createAnimation(CameraUpdate cameraUpdate) {
valueAnimatorList.clear();
ValueAnimator zoomValueAnimator = createAnimator(camera.getZoomLevel(), cameraUpdate.zoomLevel);
ValueAnimator tiltValueAnimator = createAnimator(camera.getTilt(), cameraUpdate.tilt);
ValueAnimator bearingValueAnimator = createAnimator(camera.getBearing(), cameraUpdate.bearing);
ValueAnimator latitudeValueAnimator = createAnimator(
camera.getTarget().latitude, cameraUpdate.target.latitude);
ValueAnimator longitudeValueAnimator = createAnimator(
camera.getTarget().longitude, cameraUpdate.target.longitude);
valueAnimatorList.add(zoomValueAnimator);
valueAnimatorList.add(tiltValueAnimator);
valueAnimatorList.add(bearingValueAnimator);
valueAnimatorList.add(latitudeValueAnimator);
valueAnimatorList.add(longitudeValueAnimator);
longitudeValueAnimator.addUpdateListener(animation -> {
float zoom = (float) zoomValueAnimator.getAnimatedValue();
float tilt = (float) tiltValueAnimator.getAnimatedValue();
float bearing = (float) bearingValueAnimator.getAnimatedValue();
float latitude = (float) latitudeValueAnimator.getAnimatedValue();
float longitude = (float) longitudeValueAnimator.getAnimatedValue();
GeoCoordinates intermediateGeoCoordinates = new GeoCoordinates(latitude, longitude);
camera.updateCamera(new CameraUpdate(tilt, bearing, zoom, intermediateGeoCoordinates));
});
}
As mentioned above, we create a set of Animator
instances that we store in an ArrayList
. Before starting a new animation, we clear the list from the previous animation (if any). We need five ValueAnimator
instances:
-
zoomValueAnimator
: Interpolates from current zoom level to our target zoom level. -
tiltValueAnimator
: Interpolates from current tilt value to our target tilt value. -
bearingValueAnimator
: Interpolates from the current bearing degree to our target bearing value. This effectively rotates the map. -
latitudeValueAnimator
and longitudeValueAnimator
: Both interpolate a single coordinate from the current target location to the desired new target.
Since the code to create each animator is the same, we externalized it to this separate method:
private ValueAnimator createAnimator(double from, double to) {
ValueAnimator valueAnimator = ValueAnimator.ofFloat((float) from, (float )to);
if (timeInterpolator != null) {
valueAnimator.setInterpolator(timeInterpolator);
}
return valueAnimator;
}
Each ValueAnimator
expects float values and the previously set timeInterpolator
. If nothing was set, the animation framework will take a default interpolator.
Back to our createAnimation()
method, we add all animators to the class member valueAnimatorList
. We need that list later when we want to start the animation.
As a next step, we add a listener that is called periodically until the end value is reached. Inside this listener, we get all the current intermediate values during the animation phase. Since we play all animators together in an AmimationSet
, the animation framework will take care that each 'animated' value will be updated according to the specified timeInterpolator
. This makes it easy for us, as we simply have to update the map's camera to transition the map:
GeoCoordinates intermediateGeoCoordinates = new GeoCoordinates(latitude, longitude);
camera.updateCamera(new CameraUpdate(tilt, bearing, zoom, intermediateGeoCoordinates));
This will instantly change the map's appearance to the specified values. As this code is executed many times per second, it will appear as a smooth animation to the human eye.
Finally, all that is left to do is to start the animation:
private void startAnimation(CameraUpdate cameraUpdate) {
if (animatorSet != null) {
animatorSet.cancel();
}
animatorSet = new AnimatorSet();
animatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
camera.updateCamera(cameraUpdate);
}
});
animatorSet.playTogether(valueAnimatorList);
animatorSet.setDuration(animationDurationInMillis);
animatorSet.start();
}
Note that we have defined the AnimatorSet
as a static instance in our CameraAnimator
class to allow only one instance. Before a new animation is created, we can cancel the previous one. We can also set a listener to the animatorSet
to update the camera with the desired end state-just in case an interruption causes some frames to skip. However, we want to make it reach the desired camera update state.
Before we actually call start()
, we set up the list of ValueAnimator
instances that should be played together. Additionally, we specify how long the duration should take. For animationDurationInMillis
, we have set 2000 milliseconds: No matter how far it goes, we want to make sure that the animation only takes 2 seconds - even if we have to move around the earth.
This is just an example of how to implement custom transitions from one location to another with the Camera
. By using different interpolators - or even custom interpolators - you can realize any animation style. From classic bow transitions (zooms out and then in again) over straight-forward linear transitions to any other transition you would like to have.