Interact with the map
By default, a HERE SDK map view supports all common map gestures, such as pinch and double-tap to zoom in. The table below summarizes the available gestures and their corresponding default behaviors on the map.
|
Tap the screen with one finger. This gesture does not have a predefined map action. |
|
To zoom the map in by a fixed amount, tap the screen twice with one finger. |
|
Press and hold one finger to the screen. This gesture does not have a predefined map action. |
|
To move the map, press and hold one finger to the screen, and move it in any direction. The map will keep moving with a little momentum after the finger was lifted. |
|
To tilt the map, press and hold two fingers to the screen, and move them vertically. No behavior is predefined for other directions. |
|
To zoom out by a fixed amount, tap the screen with two fingers. |
|
To zoom in or out continuously, press and hold two fingers to the screen, and increase or decrease the distance between them. To rotate the map continuously, press and hold two fingers to the screen, and change the angle between them either by rotating them both or by moving one of them. |
The HERE SDK for Android provides support for the following gestures:
- Tap:
TapListener - Double Tap:
DoubleTapListener - Long Press:
LongPressListener - Pan:
PanListener - Two Finger Pan:
TwoFingerPanListener - Two Finger Tap:
TwoFingerTapListener - Pinch Rotate:
PinchRotateListener
Each listener provides a dedicated callback that informs you whenever the user performs an action that could be detected, for example, the beginning or the end of that specific gesture. Usually, you will want to add a specific behavior to your application after a gesture was detected, like placing a map marker after a long press.
Note that only one listener can be set at a time for the same gesture.
Attach a gesture listener
Let's see an example of how a gesture listener can be attached to the map view. The map view provides specific setters for each gesture. As soon as you set a listener, it will receive all related events for that gesture via the dedicated callback, which is onTap() in case of a TapListener:
private void setTapGestureHandler(MapView mapView) {
mapView.getGestures().setTapListener(new TapListener() {
@Override
public void onTap(@NonNull Point2D touchPoint) {
GeoCoordinates geoCoordinates = mapView.viewToGeoCoordinates(touchPoint);
Log.d(TAG, "Tap at: " + geoCoordinates);
}
});
}
private fun setTapGestureHandler(mapView: MapView) {
mapView.gestures.tapListener = TapListener { touchPoint ->
val geoCoordinates = mapView.viewToGeoCoordinates(touchPoint)
Log.d(TAG, "Tap at: $geoCoordinates")
}
}
As soon as you set up a listener, you will start receiving notifications whenever gestures are detected.
The touchPoint specifies the MapView coordinates where the gesture occurred. By calling mapView.viewToGeoCoordinates(touchPoint), you can convert the pixels into geographic coordinates (as shown above).
Likewise, to stop listening, we can simply call:
mapView.getGestures().setTapListener(null);
mapView.getGestures().setTapListener(null)
For continuous gestures (like long press, pinch, pan, two finger pan), the BEGIN gesture state will indicate that the gesture was detected. While the finger(s) still touch the display, you may receive UPDATE states, until the END state indicates that a finger has been lifted or the CANCEL state indicates that the gesture detection has been cancelled:
private void setLongPressGestureHandler(MapView mapView) {
mapView.getGestures().setLongPressListener(new LongPressListener() {
@Override
public void onLongPress(@NonNull GestureState gestureState, @NonNull Point2D touchPoint) {
GeoCoordinates geoCoordinates = mapView.viewToGeoCoordinates(touchPoint);
if (gestureState == GestureState.BEGIN) {
Log.d(TAG, "LongPress detected at: " + geoCoordinates);
}
if (gestureState == GestureState.UPDATE) {
Log.d(TAG, "LongPress update at: " + geoCoordinates);
}
if (gestureState == GestureState.END) {
Log.d(TAG, "LongPress finger lifted at: " + geoCoordinates);
}
if (gestureState == GestureState.CANCEL) {
Log.d(TAG, "Map view lost focus. Maybe a modal dialog is shown or the app is sent to background.");
}
}
});
}
private fun setLongPressGestureHandler(mapView: MapView) {
mapView.gestures.longPressListener = LongPressListener { gestureState, touchPoint ->
val geoCoordinates = mapView.viewToGeoCoordinates(touchPoint)
if (gestureState == GestureState.BEGIN) {
Log.d(TAG, "LongPress detected at: $geoCoordinates")
}
if (gestureState == GestureState.UPDATE) {
Log.d(TAG, "LongPress update at: $geoCoordinates")
}
if (gestureState == GestureState.END) {
Log.d(TAG, "LongPress finger lifted at: $geoCoordinates")
}
if (gestureState == GestureState.CANCEL) {
Log.d(TAG, "Map view lost focus. Maybe a modal dialog is shown or the app is sent to background.")
}
}
}
For example, a user may still keep his finger on the screen after a long press event was detected - or even move it around. However, only the BEGIN event will mark the point in time, when the long press gesture was detected.
A long press gesture can be useful to place a map marker onto the map. An example of this can be seen in the "Search" example app. You can find the example app on GitHub for your preferred platform.
Note that for the non-continuous gestures (like tap, double tap, two finger tap), no GestureState is needed to handle the gesture event.
NoteThe pan gesture can result in a swipe that moves the map, which keeps moving although the gesture has already ended and all fingers have been lifted. To detect when any map movement has ended, a
MapIdleListenercan be used. It can be added for aHereMapinstance, which you can get from aMapViewinstance. The pan gesture is canceled when a pinch-rotate gesture starts. If two-finger panning is detected during a pinch-rotate gesture, the pan gesture is initiated, and both pan and pinch-rotate events are emitted.
Code snippets and more usage examples are available on GitHub as part of the "Gestures" example app.
Control map actions
Setting a listener does not affect the default map behavior of the gestures. That can be controlled independently. By default, all standard behaviors, such as zooming in when double tapping the map, are enabled.
For example, you can disable the default map gesture behavior for double tap (zooms in) and two finger tap (zooms out) as follows:
mapView.getGestures().disableDefaultAction(GestureType.DOUBLE_TAP);
mapView.getGestures().disableDefaultAction(GestureType.TWO_FINGER_TAP);
mapView.gestures.disableDefaultAction(GestureType.DOUBLE_TAP)
mapView.gestures.disableDefaultAction(GestureType.TWO_FINGER_TAP)
When disabling a default map action, you can still listen for the gesture event. This can be useful when you want to turn off the default action of a gesture to implement your own zooming behavior, for example. All gestures - except for tap and long press - provide a default map action. More details can be found in the overview above.
To bring back the default map gesture behavior, you can call:
mapView.getGestures().enableDefaultAction(GestureType.DOUBLE_TAP);
mapView.getGestures().enableDefaultAction(GestureType.TWO_FINGER_TAP);
mapView.gestures.enableDefaultAction(GestureType.DOUBLE_TAP)
mapView.gestures.enableDefaultAction(GestureType.TWO_FINGER_TAP)
Customize map actions
As we have already seen, by default, a double tap gesture zooms in the map in discrete steps - for example, from city level closer to street level. You can disable such default map gesture actions to implement your own behaviors - or you can add your desired actions to existing behaviors.
NoteIf needed, it is also possible to combine platform gesture handling with the HERE SDK gesture detection. The HERE SDK does not provide all kinds of low level gesture events, as it focuses primarily on the common map gestures - for your convenience. If you need more granular control, you can always combine the gesture handling available from the HERE SDK with the native Android gesture detection.
Below we show an example of how to enable custom zoom animations.
Add custom zoom behavior
In this tutorial, we will demonstrate how to implement custom zoom behavior for the map. Specifically, we will enable the map to zoom in or out gradually after the user performs a double-tap or a two-finger tap gesture. The zoom animation will decelerate smoothly after a short duration.
Let's start with the animation. For this we can use Android's animation framework and a ValueAnimator that allows us to interpolate between certain start and end value.
For convenience, we create a new class called GestureMapAnimator, that should handle all gesture related animations. It requires a reference to the map's MapCamera instance, as the map needs to be zoomed via the camera.
By default, the map zooms in/out in one discrete step at the location where the finger touches the map - without intermediate steps.
Let's hook up the needed gesture events:
mapView.getGestures().disableDefaultAction(GestureType.DOUBLE_TAP);
mapView.getGestures().disableDefaultAction(GestureType.TWO_FINGER_TAP);
mapView.getGestures().setDoubleTapListener(new DoubleTapListener() {
@Override
public void onDoubleTap(@NonNull Point2D touchPoint) {
// Start our custom zoom in animation.
gestureMapAnimator.zoomIn(touchPoint);
}
});
mapView.getGestures().setTwoFingerTapListener(new TwoFingerTapListener() {
@Override
public void onTwoFingerTap(@NonNull Point2D touchCenterPoint) {
// Start our custom zoom out animation.
gestureMapAnimator.zoomOut(touchCenterPoint);
}
});
// Disable the default map gesture behavior for DoubleTap (zooms in) and TwoFingerTap (zooms out)
// as we want to enable custom map animations when such gestures are detected.
mapView.gestures.disableDefaultAction(GestureType.DOUBLE_TAP)
mapView.gestures.disableDefaultAction(GestureType.TWO_FINGER_TAP)
private fun setDoubleTapGestureHandler(mapView: MapView) {
mapView.gestures.doubleTapListener = DoubleTapListener { touchPoint ->
// Start our custom zoom in animation.
gestureMapAnimator?.zoomIn(touchPoint)
}
}
private fun setTwoFingerTapGestureHandler(mapView: MapView) {
mapView.gestures.twoFingerTapListener = TwoFingerTapListener { touchCenterPoint ->
// Start our custom zoom out animation.
gestureMapAnimator?.zoomOut(touchCenterPoint)
}
}
No magic here: We simply listen for the two gesture events. We just need to make sure that the default zoom behavior is disabled in advance. The zoomIn() and zoomOut() methods from above lead to a new method in our GestureMapAnimator that you can see below.
// Starts the zoom in animation.
public void zoomIn(Point2D touchPoint) {
zoomOrigin = touchPoint;
startZoomAnimation(true);
}
// Starts the zoom out animation.
public void zoomOut(Point2D touchPoint) {
zoomOrigin = touchPoint;
startZoomAnimation(false);
}
// Starts the zoom in animation.
fun zoomIn(touchPoint: Point2D) {
zoomOrigin = touchPoint
startZoomAnimation(true)
}
// Starts the zoom out animation.
fun zoomOut(touchPoint: Point2D) {
zoomOrigin = touchPoint
startZoomAnimation(false)
}
Note that we store the zoomOrigin to know where on the map we should zoom in or out.
The implementation of startZoomAnimation() inside the new GestureMapAnimator class uses a ValueAnimator:
private void startZoomAnimation(boolean zoomIn) {
stopAnimations();
// A new Animator that zooms the map.
zoomValueAnimator = createZoomValueAnimator(zoomIn);
// Start the animation.
zoomValueAnimator.start();
}
private ValueAnimator createZoomValueAnimator(boolean zoomIn) {
ValueAnimator zoomValueAnimator = ValueAnimator.ofFloat(0.1F, 0);
zoomValueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
zoomValueAnimator.addUpdateListener(animation -> {
// Called periodically until zoomVelocity is zero.
float zoomVelocity = (float) animation.getAnimatedValue();
double zoomFactor = 1;
zoomFactor = zoomIn ? zoomFactor + zoomVelocity : zoomFactor - zoomVelocity;
// zoomFactor values > 1 will zoom in and values < 1 will zoom out.
camera.zoomBy(zoomFactor, zoomOrigin);
});
long halfSecond = 500;
zoomValueAnimator.setDuration(halfSecond);
return zoomValueAnimator;
}
private fun startZoomAnimation(zoomIn: Boolean) {
stopAnimations()
// A new Animator that zooms the map.
zoomValueAnimator = createZoomValueAnimator(zoomIn)
// Start the animation.
zoomValueAnimator!!.start()
}
private fun createZoomValueAnimator(zoomIn: Boolean): ValueAnimator {
val zoomValueAnimator = ValueAnimator.ofFloat(0.1f, 0f)
zoomValueAnimator.interpolator = AccelerateDecelerateInterpolator()
zoomValueAnimator.addUpdateListener { animation: ValueAnimator ->
// Called periodically until zoomVelocity is zero.
val zoomVelocity = animation.animatedValue as Float
var zoomFactor = 1.0
zoomFactor = if (zoomIn) zoomFactor + zoomVelocity else zoomFactor - zoomVelocity
// zoomFactor values > 1 will zoom in and values < 1 will zoom out.
camera!!.zoomBy(zoomFactor, zoomOrigin!!)
}
val halfSecond: Long = 500
zoomValueAnimator.setDuration(halfSecond)
return zoomValueAnimator
}
We use a ValueAnimator to interpolate the values from 0.1 to 0. These values should determine the zoom velocity. The AccelerateDecelerateInterpolator provides intermediate values with a slowing down effect at the end. The listener we set to the ValueAnimator is executed periodically until zoomVelocity reaches 0. zoomVelocity is the animated value that we can use to zoom the map. We use it as an argument to set the zoom factor. As zoomVelocity slowly reaches 0, the resulting zoom step will get smaller and smaller.
Above, we use the touch origin that we stored in zoomOrigin to zoom in to the point where the finger has touched the screen. For zooming out, this will be the point between the two finger tap gesture. The camera's zoomBy() method then takes care to perform the discrete zoom step at the specified location.
Note that we provide a boolean value to indicate if the map should be zoomed in or out. This enables us to use the code for both zoom cases: the only difference is that the animated value zoomVelocity is added or subtracted from the current zoom factor.
The zoom factor specifies how far the map is zoomed in or out. A zoom factor of 1 does not change the current zoom level.
The stopAnimations() method from above simply cancels any ongoing animation of the corresponding ValueAnimator instance:
public void stopAnimations() {
if (zoomValueAnimator != null) {
zoomValueAnimator.cancel();
}
}
fun stopAnimations() {
if (zoomValueAnimator != null) {
zoomValueAnimator!!.cancel()
}
}
Now, that's it for our little excursion. Feel free to adapt the above code snippets to your own needs. For example, start by playing around with different interpolators, different animated values, or different animation durations.
Try the example app
Most of the code snippets mentioned above are available in our "Gestures" and example app. You can find this example app on GitHub for your preferred platform.
Updated yesterday






