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.

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) {
    // Handle exception. Cannot happen here as we set the allowed max value for zoom level 12.
}

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:

// Set the map to a new location - together with a new zoom level and tilt value.
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;

    // Take the shorter bearing difference.
    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();

    // Interpolate current values for zoom, tilt, bearing, lat/lon to the desired new values.
    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);

    // Update all values together.
    zoomValueAnimator.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.

results matching ""

    No results matching ""