Skip to content

Commit 52e1409

Browse files
evil159pjleonard37
authored andcommitted
Viewport (mapbox#770)
* Viewport draft * Simple map example * format * more viewport * declarative viewport draft * Declarative viewport on iOS and Android * Fix rebase issues * Docs, example + bug fix * add changelog entry * typo * Remove unused code * lint * lint * Fix en-/de- coding for CameraViewportState * Fix exception when converting CameraOptions for CameraViewportState on Android * Add note about location requirement for FollowPuckViewportState * Add tests for viewport states * lint * Update example/lib/viewport_example.dart Co-authored-by: Patrick Leonard <[email protected]> * Center camera over Disneyland in simple map example * Disable overview state viewport test on Android due to a bug * Update example/integration_test/viewport_test.dart Co-authored-by: Patrick Leonard <[email protected]> --------- Co-authored-by: Patrick Leonard <[email protected]>
1 parent 075624f commit 52e1409

34 files changed

+2888
-62
lines changed

CHANGELOG.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,24 @@
1-
> [!IMPORTANT]
2-
> Configuring Mapbox's secret token is no longer required when installing our SDKs.
1+
### main
2+
3+
* 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.
4+
```dart
5+
MapWidget(
6+
viewport: CameraViewportState(
7+
center: Point(coordinates: Position(-117.918976, 33.812092)),
8+
zoom: 15.0,
9+
),
10+
);
11+
```
312

413
### 2.4.1
514

615
* Fix annotation click listeners not working.
716

817
### 2.4.0
918

19+
> [!IMPORTANT]
20+
> Configuring Mapbox's secret token is no longer required when installing our SDKs.
21+
1022
* Update Maps SDK to 11.8.0
1123
* 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.
1224
* Introduce experimental property `MapboxMap.styleGlyphURL`. Use this property to apply custom fonts to the map at runtime, without modifying the base style.

android/src/main/kotlin/com/mapbox/maps/mapbox_maps/MapboxMapController.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ import com.mapbox.maps.mapbox_maps.pigeons._AnimationManager
2626
import com.mapbox.maps.mapbox_maps.pigeons._CameraManager
2727
import com.mapbox.maps.mapbox_maps.pigeons._LocationComponentSettingsInterface
2828
import com.mapbox.maps.mapbox_maps.pigeons._MapInterface
29+
import com.mapbox.maps.mapbox_maps.pigeons._ViewportMessenger
30+
import com.mapbox.maps.plugin.animation.camera
31+
import com.mapbox.maps.plugin.viewport.viewport
2932
import io.flutter.embedding.android.FlutterActivity
3033
import io.flutter.plugin.common.BinaryMessenger
3134
import io.flutter.plugin.common.MethodCall
@@ -65,6 +68,7 @@ class MapboxMapController(
6568
private val attributionController: AttributionController
6669
private val scaleBarController: ScaleBarController
6770
private val compassController: CompassController
71+
private val viewportController: ViewportController
6872

6973
private val eventHandler: MapboxEventHandler
7074

@@ -148,6 +152,7 @@ class MapboxMapController(
148152
scaleBarController = ScaleBarController(mapView)
149153
compassController = CompassController(mapView)
150154
viewAnnotationController = ViewAnnotationController(mapView, context)
155+
viewportController = ViewportController(mapView.viewport, mapView.camera, context, mapboxMap)
151156

152157
changeUserAgent(pluginVersion)
153158

@@ -163,6 +168,7 @@ class MapboxMapController(
163168
AttributionSettingsInterface.setUp(messenger, attributionController, this.channelSuffix)
164169
ScaleBarSettingsInterface.setUp(messenger, scaleBarController, this.channelSuffix)
165170
CompassSettingsInterface.setUp(messenger, compassController, this.channelSuffix)
171+
_ViewportMessenger.setUp(messenger, viewportController, this.channelSuffix)
166172

167173
methodChannel = MethodChannel(messenger, "plugins.flutter.io.$channelSuffix")
168174
methodChannel.setMethodCallHandler(this)
@@ -200,6 +206,7 @@ class MapboxMapController(
200206
mapView = null
201207
mapboxMap = null
202208
methodChannel.setMethodCallHandler(null)
209+
203210
StyleManager.setUp(messenger, null, channelSuffix)
204211
_CameraManager.setUp(messenger, null, channelSuffix)
205212
Projection.setUp(messenger, null, channelSuffix)
@@ -212,6 +219,7 @@ class MapboxMapController(
212219
CompassSettingsInterface.setUp(messenger, null, channelSuffix)
213220
ScaleBarSettingsInterface.setUp(messenger, null, channelSuffix)
214221
AttributionSettingsInterface.setUp(messenger, null, channelSuffix)
222+
_ViewportMessenger.setUp(messenger, null, channelSuffix)
215223
}
216224

217225
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
@@ -261,6 +269,9 @@ class MapboxMapController(
261269
result.success(byteArray)
262270
}
263271
}
272+
"mapView#submitViewSizeHint" -> {
273+
result.success(null) // no-op on this platform
274+
}
264275
else -> {
265276
result.notImplemented()
266277
}

android/src/main/kotlin/com/mapbox/maps/mapbox_maps/MapboxMapFactory.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class MapboxMapFactory(
2929
val cameraOptions = params["cameraOptions"] as com.mapbox.maps.mapbox_maps.pigeons.CameraOptions?
3030
val channelSuffix = params["channelSuffix"] as Long
3131
val textureView = params["textureView"] as? Boolean ?: false
32-
val styleUri = params["styleUri"] as? String ?: Style.MAPBOX_STREETS
32+
val styleUri = params["styleUri"] as? String ?: Style.STANDARD
3333
val pluginVersion = params["mapboxPluginVersion"] as String
3434
val eventTypes = params["eventTypes"] as List<Long>
3535

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
package com.mapbox.maps.mapbox_maps
2+
3+
import android.animation.Animator
4+
import android.animation.AnimatorListenerAdapter
5+
import android.content.Context
6+
import android.view.animation.PathInterpolator
7+
import com.google.gson.GsonBuilder
8+
import com.mapbox.common.Cancelable
9+
import com.mapbox.geojson.Polygon
10+
import com.mapbox.geojson.gson.GeoJsonAdapterFactory
11+
import com.mapbox.maps.CameraOptions
12+
import com.mapbox.maps.MapboxMap
13+
import com.mapbox.maps.ScreenCoordinate
14+
import com.mapbox.maps.logE
15+
import com.mapbox.maps.mapbox_maps.pigeons._DefaultViewportTransitionOptions
16+
import com.mapbox.maps.mapbox_maps.pigeons._EasingViewportTransitionOptions
17+
import com.mapbox.maps.mapbox_maps.pigeons._FlyViewportTransitionOptions
18+
import com.mapbox.maps.mapbox_maps.pigeons._FollowPuckViewportStateBearing
19+
import com.mapbox.maps.mapbox_maps.pigeons._FollowPuckViewportStateOptions
20+
import com.mapbox.maps.mapbox_maps.pigeons._OverviewViewportStateOptions
21+
import com.mapbox.maps.mapbox_maps.pigeons._ViewportMessenger
22+
import com.mapbox.maps.mapbox_maps.pigeons._ViewportStateStorage
23+
import com.mapbox.maps.mapbox_maps.pigeons._ViewportStateType
24+
import com.mapbox.maps.mapbox_maps.pigeons._ViewportTransitionStorage
25+
import com.mapbox.maps.mapbox_maps.pigeons._ViewportTransitionType
26+
import com.mapbox.maps.plugin.animation.CameraAnimationsPlugin
27+
import com.mapbox.maps.plugin.animation.MapAnimationOptions
28+
import com.mapbox.maps.plugin.viewport.CompletionListener
29+
import com.mapbox.maps.plugin.viewport.ViewportPlugin
30+
import com.mapbox.maps.plugin.viewport.data.DefaultViewportTransitionOptions
31+
import com.mapbox.maps.plugin.viewport.data.FollowPuckViewportStateBearing
32+
import com.mapbox.maps.plugin.viewport.data.FollowPuckViewportStateOptions
33+
import com.mapbox.maps.plugin.viewport.data.OverviewViewportStateOptions
34+
import com.mapbox.maps.plugin.viewport.state.ViewportState
35+
import com.mapbox.maps.plugin.viewport.state.ViewportStateDataObserver
36+
import com.mapbox.maps.plugin.viewport.transition.ViewportTransition
37+
38+
class ViewportController(
39+
private val viewportPlugin: ViewportPlugin,
40+
private val cameraPlugin: CameraAnimationsPlugin,
41+
private val context: Context,
42+
private val mapboxMap: MapboxMap
43+
) : _ViewportMessenger {
44+
45+
override fun transition(
46+
stateStorage: _ViewportStateStorage,
47+
transitionStorage: _ViewportTransitionStorage?,
48+
callback: (Result<Boolean>) -> Unit
49+
) {
50+
try {
51+
val state = viewportPlugin.viewportStateFromFLTState(stateStorage, context, mapboxMap)
52+
if (state == null) {
53+
callback(Result.success(true))
54+
return
55+
}
56+
val transition = viewportPlugin.transitionFromFLTTransition(transitionStorage, cameraPlugin)
57+
viewportPlugin.transitionTo(state, transition) { success ->
58+
callback(Result.success(success))
59+
}
60+
} catch (error: Exception) {
61+
logE("Viewport", "Could not create viewport state ouf of options: $stateStorage")
62+
callback(Result.success(false))
63+
}
64+
}
65+
}
66+
67+
fun ViewportPlugin.transitionFromFLTTransition(
68+
transitionStorage: _ViewportTransitionStorage?,
69+
cameraPlugin: CameraAnimationsPlugin
70+
): ViewportTransition {
71+
return when (transitionStorage?.type) {
72+
_ViewportTransitionType.DEFAULT_TRANSITION ->
73+
(transitionStorage.options as? _DefaultViewportTransitionOptions)
74+
?.let { makeDefaultViewportTransition(it.toOptions()) }
75+
76+
_ViewportTransitionType.FLY ->
77+
(transitionStorage.options as? _FlyViewportTransitionOptions)
78+
?.let {
79+
GenericViewportTransition { cameraOptions, completion ->
80+
val options = MapAnimationOptions.Builder()
81+
if (it.durationMs != null) {
82+
options.duration(it.durationMs)
83+
}
84+
cameraPlugin.flyTo(
85+
cameraOptions, options.build(),
86+
object : AnimatorListenerAdapter() {
87+
override fun onAnimationEnd(animation: Animator) {
88+
completion.onComplete(true)
89+
}
90+
91+
override fun onAnimationCancel(animation: Animator) {
92+
completion.onComplete(false)
93+
}
94+
}
95+
)
96+
}
97+
}
98+
99+
_ViewportTransitionType.EASING ->
100+
(transitionStorage.options as? _EasingViewportTransitionOptions)
101+
?.let {
102+
GenericViewportTransition { cameraOptions, completion ->
103+
val options = MapAnimationOptions.Builder()
104+
.duration(it.durationMs)
105+
.interpolator(
106+
PathInterpolator(
107+
it.a.toFloat(),
108+
it.b.toFloat(),
109+
it.c.toFloat(),
110+
it.d.toFloat()
111+
)
112+
)
113+
.build()
114+
cameraPlugin.easeTo(
115+
cameraOptions, options,
116+
object : AnimatorListenerAdapter() {
117+
override fun onAnimationEnd(animation: Animator) {
118+
completion.onComplete(true)
119+
}
120+
121+
override fun onAnimationCancel(animation: Animator) {
122+
completion.onComplete(false)
123+
}
124+
}
125+
)
126+
}
127+
}
128+
129+
null -> null
130+
} ?: makeImmediateViewportTransition()
131+
}
132+
133+
typealias AnimationRunner = (CameraOptions, CompletionListener) -> Unit
134+
135+
class GenericViewportTransition(private val runAnimation: AnimationRunner) : ViewportTransition {
136+
137+
override fun run(to: ViewportState, completionListener: CompletionListener): Cancelable {
138+
return to.observeDataSource { cameraOptions ->
139+
runAnimation(cameraOptions) { animationPosition ->
140+
completionListener.onComplete(animationPosition)
141+
}
142+
return@observeDataSource false
143+
}
144+
}
145+
}
146+
147+
fun _DefaultViewportTransitionOptions.toOptions(): DefaultViewportTransitionOptions {
148+
return DefaultViewportTransitionOptions.Builder()
149+
.maxDurationMs(maxDurationMs)
150+
.build()
151+
}
152+
153+
fun ViewportPlugin.viewportStateFromFLTState(
154+
stateStorage: _ViewportStateStorage,
155+
context: Context,
156+
mapboxMap: MapboxMap
157+
): ViewportState? {
158+
return when (stateStorage.type) {
159+
_ViewportStateType.IDLE -> idle().let { null }
160+
_ViewportStateType.FOLLOW_PUCK ->
161+
makeFollowPuckViewportState((stateStorage.options as _FollowPuckViewportStateOptions).toOptions())
162+
163+
_ViewportStateType.OVERVIEW ->
164+
makeOverviewViewportState(
165+
(stateStorage.options as _OverviewViewportStateOptions).toOptions(
166+
context
167+
)
168+
)
169+
170+
_ViewportStateType.STYLE_DEFAULT -> StyleDefaultViewportState(mapboxMap)
171+
_ViewportStateType.CAMERA -> CameraViewportState(
172+
(stateStorage.options as com.mapbox.maps.mapbox_maps.pigeons.CameraOptions).toCameraOptions(
173+
context
174+
),
175+
mapboxMap
176+
)
177+
}
178+
}
179+
180+
fun _FollowPuckViewportStateOptions.toOptions(): FollowPuckViewportStateOptions {
181+
val bearing: FollowPuckViewportStateBearing? = when (this.bearing) {
182+
_FollowPuckViewportStateBearing.HEADING -> FollowPuckViewportStateBearing.SyncWithLocationPuck
183+
_FollowPuckViewportStateBearing.COURSE -> FollowPuckViewportStateBearing.SyncWithLocationPuck
184+
_FollowPuckViewportStateBearing.CONSTANT -> {
185+
if (bearingValue == null) {
186+
logE(
187+
"Viewport",
188+
"Invalid FollowPuckViewportStateOptions, bearing mode is CONSTANT but bearingValue is null"
189+
)
190+
}
191+
192+
bearingValue?.let { FollowPuckViewportStateBearing.Constant(it) }
193+
}
194+
195+
null -> null
196+
}
197+
198+
return FollowPuckViewportStateOptions.Builder()
199+
.zoom(zoom)
200+
.bearing(bearing)
201+
.pitch(pitch)
202+
.build()
203+
}
204+
205+
fun _OverviewViewportStateOptions.toOptions(context: Context): OverviewViewportStateOptions {
206+
val geometry = GsonBuilder()
207+
.registerTypeAdapterFactory(GeoJsonAdapterFactory.create())
208+
.create()
209+
.fromJson(geometry, Polygon::class.java)
210+
return OverviewViewportStateOptions.Builder()
211+
.geometry(geometry)
212+
.padding(padding?.toEdgeInsets(context))
213+
.geometryPadding(geometryPadding.toEdgeInsets(context))
214+
.bearing(bearing)
215+
.pitch(pitch)
216+
.maxZoom(maxZoom)
217+
.offset(offset?.toScreenCoordinate(context) ?: ScreenCoordinate(0.0, 0.0))
218+
.animationDurationMs(animationDurationMs)
219+
.build()
220+
}
221+
222+
class CameraViewportState(private val options: CameraOptions, private val mapboxMap: MapboxMap) :
223+
ViewportState {
224+
225+
override fun observeDataSource(viewportStateDataObserver: ViewportStateDataObserver): Cancelable {
226+
viewportStateDataObserver.onNewData(options)
227+
return Cancelable { }
228+
}
229+
230+
override fun startUpdatingCamera() {
231+
mapboxMap.setCamera(options)
232+
}
233+
234+
override fun stopUpdatingCamera() {}
235+
}
236+
237+
class StyleDefaultViewportState(private val mapboxMap: MapboxMap) : ViewportState {
238+
private var token: Cancelable? = null
239+
240+
private fun observeStyleDefaultCamera(handler: (CameraOptions) -> Unit): Cancelable {
241+
if (mapboxMap.isStyleLoaded()) {
242+
handler(mapboxMap.styleManager.styleDefaultCamera)
243+
return Cancelable { }
244+
}
245+
246+
return mapboxMap.subscribeStyleLoaded {
247+
handler(mapboxMap.styleManager.styleDefaultCamera)
248+
}
249+
}
250+
251+
override fun observeDataSource(viewportStateDataObserver: ViewportStateDataObserver): Cancelable {
252+
return observeStyleDefaultCamera { viewportStateDataObserver.onNewData(it) }
253+
}
254+
255+
override fun startUpdatingCamera() {
256+
token = observeStyleDefaultCamera { mapboxMap.setCamera(it) }
257+
}
258+
259+
override fun stopUpdatingCamera() {
260+
token?.cancel()
261+
}
262+
}

0 commit comments

Comments
 (0)