/MapView

A Fast, memory efficient Android library to display tiled maps, with support for markers, paths, and rotation.

Primary LanguageKotlinApache License 2.0Apache-2.0

Maven Central GitHub License

Looking for the Compose version? Check this out.

MapView

MapView is a fast, memory efficient Android library to display tiled maps with minimal effort.

An example of setting up:

val mapView = MapView(context)
val tileStreamProvider = TileStreamProvider { row, col, zoomLvl ->
    FileInputStream(File("path/{zoomLvl}/{row}/{col}.jpg")) // or it can be a remote HTTP fetch
}

val config = MapViewConfiguration(levelCount = 7, fullWidth = 25000, fullHeight = 12500,
                                  tileSize = 256, tileStreamProvider = tileStreamProvider)
                                  .setMaxScale(2f)

/* Configuration */
mapView.configure(config)

MapView shows only the visible part of a tiled map, and supports flinging, dragging, scaling, and rotating. It's also possible to add markers and paths.

This project holds the source code of this library, plus a demo app (which is useful to get started). To test the demo, just clone the repo and launch the demo app from Android Studio.

MapView supports map rotation

To be consistent with previous versions, this is disabled by default. To enable it, use MapViewConfiguration.enableRotation(). You will find a code example inside the demo RotatingMapFragment.

When enabling rotation, the MapView handles rotation gestures by default. If you only want to rotate the map through APIs, then you should use enableRotation(handleRotationGesture = false). The MapView has a new API setAngle:

/**
 * Programmatically set the rotation angle of the MapView, in decimal degrees.
 * It should be called after the [MapView] configuration and after the [MapView] has been laid out.
 * Attempts to set the angle before [MapView] has been laid out will be ignored.
 */
fun MapView.setAngle(angle: AngleDegree)

Migrating from 2.x.x

3.x.x introduced the following breaking changes:

  • The domain name of the library was changed to ovh.plrapps. MapView is now directly published on mavenCentral.
  • The interface ReferentialOwner has been replaced with ReferentialListener. Instead of expecting ReferentialOwners to supply a default value for ReferentialData, ReferentialListener only has a onReferentialChanged(refData: ReferentialData) method. Migrating to this new interface should be straightforward. There's an example of usage inside the RotatingMapFragment demo.

Installation

Add this to your module's build.gradle

implementation 'ovh.plrapps:mapview:3.1.8'

In addition, update the module's build.gradle file (for each module that uses MapView), as shown below:

android {
  ...
  // Configure only for each module that uses Java 8
  // language features (either in its source code or
  // through dependencies).
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
  // For Kotlin projects
  kotlinOptions {
    jvmTarget = "1.8"
    freeCompilerArgs = ['-Xjvm-default=all-compatibility']
  }
}

Origin and motivation

As a long time contributor to TileView, I wanted to see the performance we would get using idiomatic Kotlin (coroutines, flows). The result was beyond my expectations. The overall design can be seen here. Special attention has been given to efficiency (using non-blocking algorithm to avoiding thread contention). We get smooth animations and high fps.

Thanks for Mike (@moagrius), as this library wouldn't exist without his first contributions.

Principles

Deep-zoom map

MapView is optimized to display maps that have several levels, like this:

Each next level is twice bigger than the former, and provides more details. Overall, this looks like a pyramid. Another common name is "deep-zoom" map. This library comes with a demo app made of a set of various use-cases such as using markers, paths, rotating the map, etc. All examples use the same map stored in the assets. If you wonder what a deep-zoom maps looks like, you have a great example there.

MapView can also be used with single level maps.

Usage

To use the MapView, you have to follow these steps:

  1. Create a MapView instance
val mapView = MapView(context)
  1. Create a TileStreamProvider. See below for the details.
  2. Create a MapViewConfiguration. See below for the details.
  3. Apply the configuration
mapView.configure(config)

For more insight, you can have a look at the source of the various demos.

Convention

MapView uses the convention that the last level is at scale 1. So all levels have scales between 0 and 1. Even though you don't have to be aware of the details, it's important to know that. For example, if you set the max scale to 2, it means that the last level will be allowed to be upscaled to twice its original size (since the last level is at scale 1). This convention allows for a simple configuration.

Technical documentation

The MapView needs to be configured - more on that below. Once configured, you can do a lot of things with your MapView instance. MapView is a subclass of GestureLayout, which has many features. You can:

  • add listeners to events like pan, fling, zoom..
  • programmatically scroll and center to a position
  • respond to various touch events by subclassing MapView and overload related methods declared in GestureLayout

This list isn't complete. You can explore the capabilities in the source of GestureLayout.

MapViewConfiguration

The MapView must be configured using a MapViewConfiguration. It holds the mandatory parameters to build a MapView.

Then, you can set optional properties by calling available methods on your MapViewConfiguration instance. Here is an example:

val config = MapViewConfiguration(levelCount = 7, fullWidth = 25000, fullHeight = 12500,
                                  tileSize = 256, tileStreamProvider = tileStreamProvider)
                                  .setMaxScale(2f)

See documentation here. Below is a description of mandatory parameters:

levelCount

The provided MapViewConfiguration.levelCount will define the zoomLevels index that the provided MapViewConfiguration.tileStreamProvider will be given for its TileStreamProvider.zoomLevels. The zoomLevels will be in the range [0 ; MapViewConfiguration.levelCount-1].

fullWidth and fullHeight

These are respectively the width and height in pixels of the map at scale 1 (that is, the width and height of the last level). In other words, if you put together all the tiles of the last level, you would obtain a big image. fullWidth and fullHeight are dimensions in pixels of this big image.

tileSize

The size of the tiles in pixels, which are assumed to be squares and always of the same size for all levels. For now, MapView doesn't support rectangular tiles or tiles of heterogeneous sizes.

tileStreamProvider

See the section below.

TileStreamProvider

The MapView will request tiles using the convention that each levels has its tiles organized like this:

MapView isn't opinionated about the origination of tiles. This is the purpose of the TileStreamProvider:

fun interface TileStreamProvider {
    fun getTileStream(row: Int, col: Int, zoomLvl: Int): InputStream?
}

Your implementation of this interface does the necessary coordinate translation (if required). This is where you do your HTTP request if you have remote tiles, or fetch from a local database (or file system).

Tile caching

The MapView leverages bitmap pooling to reduce the pressure on the garbage collector. However, there's no tile caching by default - this is an implementation detail of the supplied TileStreamProvider.

ReferentialListener

When the scale and/or the rotation of the MapView change, some of the child views might have to change accordingly. For that purpose, you can register a ReferentialListener to the MapView.

A ReferentialListener is an interface:

fun interface ReferentialListener {
    fun onReferentialChanged(refData: ReferentialData)
}

And ReferentialData holds several useful properties:

data class ReferentialData(var rotationEnabled: Boolean = true,
                           var angle: AngleDegree = 0f,
                           var scale: Float = 0f,
                           var centerX: Double = 0.0,
                           var centerY: Double = 0.0) : Parcelable

A ReferentialListener should be registered to the MapView:

mapView.addReferentialListener(refOwner)
// If you need to unregister it:
mapView.removeReferentialListener(refOwner)

From inside your ReferentialListener implementation, you can have any logic you want. You can rotate some markers, rotate complex views taking into account the centerX and centerY properties, etc.

There's an example of usage at RotatingMapFragment.

Create a deep-zoom map

If you don't already have such a map and you need to make one from a big image, follow this tutorial.

How do I..

Follow this cheat sheet.

API documentation

API documentation has its own wiki page.