Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
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
16 changes: 14 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
> [!IMPORTANT]
> Configuring Mapbox's secret token is no longer required when installing our SDKs.
### main

* Added viewport support to `MapWidget`. Control the camera’s initial position and behavior by specifying a ViewportState subclass in the viewport parameter. This allows for centering on specific locations, following the user’s position, or showing an overview of a geometry. If no viewport is provided, the map uses its default camera settings.
```dart
MapWidget(
viewport: CameraViewportState(
center: Point(coordinates: Position(-117.918976, 33.812092)),
zoom: 15.0,
),
);
```

### 2.4.1

* Fix annotation click listeners not working.

### 2.4.0

> [!IMPORTANT]
> Configuring Mapbox's secret token is no longer required when installing our SDKs.

* Update Maps SDK to 11.8.0
* Updated the minimum required Flutter SDK to version 3.22.3 and Dart to version 3.4.4. With the fix for Virtual Display hosting mode on Android in Flutter 3.22, we’ve changed the default map view hosting mode to Virtual Display composition. This update should eliminate the brief visibility of the map after it has been dismissed.
* Introduce experimental property `MapboxMap.styleGlyphURL`. Use this property to apply custom fonts to the map at runtime, without modifying the base style.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ import com.mapbox.maps.mapbox_maps.pigeons._AnimationManager
import com.mapbox.maps.mapbox_maps.pigeons._CameraManager
import com.mapbox.maps.mapbox_maps.pigeons._LocationComponentSettingsInterface
import com.mapbox.maps.mapbox_maps.pigeons._MapInterface
import com.mapbox.maps.mapbox_maps.pigeons._ViewportMessenger
import com.mapbox.maps.plugin.animation.camera
import com.mapbox.maps.plugin.viewport.viewport
import io.flutter.embedding.android.FlutterActivity
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MethodCall
Expand Down Expand Up @@ -64,6 +67,7 @@ class MapboxMapController(
private val attributionController: AttributionController
private val scaleBarController: ScaleBarController
private val compassController: CompassController
private val viewportController: ViewportController

private val eventHandler: MapboxEventHandler

Expand Down Expand Up @@ -145,6 +149,7 @@ class MapboxMapController(
attributionController = AttributionController(mapView)
scaleBarController = ScaleBarController(mapView)
compassController = CompassController(mapView)
viewportController = ViewportController(mapView.viewport, mapView.camera, context, mapboxMap)

changeUserAgent(pluginVersion)

Expand All @@ -160,6 +165,7 @@ class MapboxMapController(
AttributionSettingsInterface.setUp(messenger, attributionController, this.channelSuffix)
ScaleBarSettingsInterface.setUp(messenger, scaleBarController, this.channelSuffix)
CompassSettingsInterface.setUp(messenger, compassController, this.channelSuffix)
_ViewportMessenger.setUp(messenger, viewportController, this.channelSuffix)

methodChannel = MethodChannel(messenger, "plugins.flutter.io.$channelSuffix")
methodChannel.setMethodCallHandler(this)
Expand Down Expand Up @@ -197,6 +203,7 @@ class MapboxMapController(
mapView = null
mapboxMap = null
methodChannel.setMethodCallHandler(null)

StyleManager.setUp(messenger, null, channelSuffix)
_CameraManager.setUp(messenger, null, channelSuffix)
Projection.setUp(messenger, null, channelSuffix)
Expand All @@ -209,6 +216,7 @@ class MapboxMapController(
CompassSettingsInterface.setUp(messenger, null, channelSuffix)
ScaleBarSettingsInterface.setUp(messenger, null, channelSuffix)
AttributionSettingsInterface.setUp(messenger, null, channelSuffix)
_ViewportMessenger.setUp(messenger, null, channelSuffix)
}

override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
Expand Down Expand Up @@ -246,6 +254,9 @@ class MapboxMapController(
result.success(byteArray)
}
}
"mapView#submitViewSizeHint" -> {
result.success(null) // no-op on this platform
}
else -> {
result.notImplemented()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class MapboxMapFactory(
val cameraOptions = params["cameraOptions"] as com.mapbox.maps.mapbox_maps.pigeons.CameraOptions?
val channelSuffix = params["channelSuffix"] as Long
val textureView = params["textureView"] as? Boolean ?: false
val styleUri = params["styleUri"] as? String ?: Style.MAPBOX_STREETS
val styleUri = params["styleUri"] as? String ?: Style.STANDARD
val pluginVersion = params["mapboxPluginVersion"] as String
val eventTypes = params["eventTypes"] as List<Long>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
package com.mapbox.maps.mapbox_maps

import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.content.Context
import android.view.animation.PathInterpolator
import com.google.gson.GsonBuilder
import com.mapbox.common.Cancelable
import com.mapbox.geojson.Polygon
import com.mapbox.geojson.gson.GeoJsonAdapterFactory
import com.mapbox.maps.CameraOptions
import com.mapbox.maps.MapboxMap
import com.mapbox.maps.ScreenCoordinate
import com.mapbox.maps.logE
import com.mapbox.maps.mapbox_maps.pigeons._DefaultViewportTransitionOptions
import com.mapbox.maps.mapbox_maps.pigeons._EasingViewportTransitionOptions
import com.mapbox.maps.mapbox_maps.pigeons._FlyViewportTransitionOptions
import com.mapbox.maps.mapbox_maps.pigeons._FollowPuckViewportStateBearing
import com.mapbox.maps.mapbox_maps.pigeons._FollowPuckViewportStateOptions
import com.mapbox.maps.mapbox_maps.pigeons._OverviewViewportStateOptions
import com.mapbox.maps.mapbox_maps.pigeons._ViewportMessenger
import com.mapbox.maps.mapbox_maps.pigeons._ViewportStateStorage
import com.mapbox.maps.mapbox_maps.pigeons._ViewportStateType
import com.mapbox.maps.mapbox_maps.pigeons._ViewportTransitionStorage
import com.mapbox.maps.mapbox_maps.pigeons._ViewportTransitionType
import com.mapbox.maps.plugin.animation.CameraAnimationsPlugin
import com.mapbox.maps.plugin.animation.MapAnimationOptions
import com.mapbox.maps.plugin.viewport.CompletionListener
import com.mapbox.maps.plugin.viewport.ViewportPlugin
import com.mapbox.maps.plugin.viewport.data.DefaultViewportTransitionOptions
import com.mapbox.maps.plugin.viewport.data.FollowPuckViewportStateBearing
import com.mapbox.maps.plugin.viewport.data.FollowPuckViewportStateOptions
import com.mapbox.maps.plugin.viewport.data.OverviewViewportStateOptions
import com.mapbox.maps.plugin.viewport.state.ViewportState
import com.mapbox.maps.plugin.viewport.state.ViewportStateDataObserver
import com.mapbox.maps.plugin.viewport.transition.ViewportTransition

class ViewportController(
private val viewportPlugin: ViewportPlugin,
private val cameraPlugin: CameraAnimationsPlugin,
private val context: Context,
private val mapboxMap: MapboxMap
) : _ViewportMessenger {

override fun transition(
stateStorage: _ViewportStateStorage,
transitionStorage: _ViewportTransitionStorage?,
callback: (Result<Boolean>) -> Unit
) {
try {
val state = viewportPlugin.viewportStateFromFLTState(stateStorage, context, mapboxMap)
if (state == null) {
callback(Result.success(true))
return
}
val transition = viewportPlugin.transitionFromFLTTransition(transitionStorage, cameraPlugin)
viewportPlugin.transitionTo(state, transition) { success ->
callback(Result.success(success))
}
} catch (error: Exception) {
logE("Viewport", "Could not create viewport state ouf of options: $stateStorage")
callback(Result.success(false))
}
}
}

fun ViewportPlugin.transitionFromFLTTransition(
transitionStorage: _ViewportTransitionStorage?,
cameraPlugin: CameraAnimationsPlugin
): ViewportTransition {
return when (transitionStorage?.type) {
_ViewportTransitionType.DEFAULT_TRANSITION ->
(transitionStorage.options as? _DefaultViewportTransitionOptions)
?.let { makeDefaultViewportTransition(it.toOptions()) }

_ViewportTransitionType.FLY ->
(transitionStorage.options as? _FlyViewportTransitionOptions)
?.let {
GenericViewportTransition { cameraOptions, completion ->
val options = MapAnimationOptions.Builder()
if (it.durationMs != null) {
options.duration(it.durationMs)
}
cameraPlugin.flyTo(
cameraOptions, options.build(),
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
completion.onComplete(true)
}

override fun onAnimationCancel(animation: Animator) {
completion.onComplete(false)
}
}
)
}
}

_ViewportTransitionType.EASING ->
(transitionStorage.options as? _EasingViewportTransitionOptions)
?.let {
GenericViewportTransition { cameraOptions, completion ->
val options = MapAnimationOptions.Builder()
.duration(it.durationMs)
.interpolator(
PathInterpolator(
it.a.toFloat(),
it.b.toFloat(),
it.c.toFloat(),
it.d.toFloat()
)
)
.build()
cameraPlugin.easeTo(
cameraOptions, options,
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
completion.onComplete(true)
}

override fun onAnimationCancel(animation: Animator) {
completion.onComplete(false)
}
}
)
}
}

null -> null
} ?: makeImmediateViewportTransition()
}

typealias AnimationRunner = (CameraOptions, CompletionListener) -> Unit

class GenericViewportTransition(private val runAnimation: AnimationRunner) : ViewportTransition {

override fun run(to: ViewportState, completionListener: CompletionListener): Cancelable {
return to.observeDataSource { cameraOptions ->
runAnimation(cameraOptions) { animationPosition ->
completionListener.onComplete(animationPosition)
}
return@observeDataSource false
}
}
}

fun _DefaultViewportTransitionOptions.toOptions(): DefaultViewportTransitionOptions {
return DefaultViewportTransitionOptions.Builder()
.maxDurationMs(maxDurationMs)
.build()
}

fun ViewportPlugin.viewportStateFromFLTState(
stateStorage: _ViewportStateStorage,
context: Context,
mapboxMap: MapboxMap
): ViewportState? {
return when (stateStorage.type) {
_ViewportStateType.IDLE -> idle().let { null }
_ViewportStateType.FOLLOW_PUCK ->
makeFollowPuckViewportState((stateStorage.options as _FollowPuckViewportStateOptions).toOptions())

_ViewportStateType.OVERVIEW ->
makeOverviewViewportState(
(stateStorage.options as _OverviewViewportStateOptions).toOptions(
context
)
)

_ViewportStateType.STYLE_DEFAULT -> StyleDefaultViewportState(mapboxMap)
_ViewportStateType.CAMERA -> CameraViewportState(
(stateStorage.options as com.mapbox.maps.mapbox_maps.pigeons.CameraOptions).toCameraOptions(
context
),
mapboxMap
)
}
}

fun _FollowPuckViewportStateOptions.toOptions(): FollowPuckViewportStateOptions {
val bearing: FollowPuckViewportStateBearing? = when (this.bearing) {
_FollowPuckViewportStateBearing.HEADING -> FollowPuckViewportStateBearing.SyncWithLocationPuck
_FollowPuckViewportStateBearing.COURSE -> FollowPuckViewportStateBearing.SyncWithLocationPuck
_FollowPuckViewportStateBearing.CONSTANT -> {
if (bearingValue == null) {
logE(
"Viewport",
"Invalid FollowPuckViewportStateOptions, bearing mode is CONSTANT but bearingValue is null"
)
}

bearingValue?.let { FollowPuckViewportStateBearing.Constant(it) }
}

null -> null
}

return FollowPuckViewportStateOptions.Builder()
.zoom(zoom)
.bearing(bearing)
.pitch(pitch)
.build()
}

fun _OverviewViewportStateOptions.toOptions(context: Context): OverviewViewportStateOptions {
val geometry = GsonBuilder()
.registerTypeAdapterFactory(GeoJsonAdapterFactory.create())
.create()
.fromJson(geometry, Polygon::class.java)
return OverviewViewportStateOptions.Builder()
.geometry(geometry)
.padding(padding?.toEdgeInsets(context))
.geometryPadding(geometryPadding.toEdgeInsets(context))
.bearing(bearing)
.pitch(pitch)
.maxZoom(maxZoom)
.offset(offset?.toScreenCoordinate(context) ?: ScreenCoordinate(0.0, 0.0))
.animationDurationMs(animationDurationMs)
.build()
}

class CameraViewportState(private val options: CameraOptions, private val mapboxMap: MapboxMap) :
ViewportState {

override fun observeDataSource(viewportStateDataObserver: ViewportStateDataObserver): Cancelable {
viewportStateDataObserver.onNewData(options)
return Cancelable { }
}

override fun startUpdatingCamera() {
mapboxMap.setCamera(options)
}

override fun stopUpdatingCamera() {}
}

class StyleDefaultViewportState(private val mapboxMap: MapboxMap) : ViewportState {
private var token: Cancelable? = null

private fun observeStyleDefaultCamera(handler: (CameraOptions) -> Unit): Cancelable {
if (mapboxMap.isStyleLoaded()) {
handler(mapboxMap.styleManager.styleDefaultCamera)
return Cancelable { }
}

return mapboxMap.subscribeStyleLoaded {
handler(mapboxMap.styleManager.styleDefaultCamera)
}
}

override fun observeDataSource(viewportStateDataObserver: ViewportStateDataObserver): Cancelable {
return observeStyleDefaultCamera { viewportStateDataObserver.onNewData(it) }
}

override fun startUpdatingCamera() {
token = observeStyleDefaultCamera { mapboxMap.setCamera(it) }
}

override fun stopUpdatingCamera() {
token?.cancel()
}
}
Loading