Skip to content

Commit f25319c

Browse files
evil159pjleonard37
andauthored
Viewport (#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 73e9719 commit f25319c

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
@@ -64,6 +67,7 @@ class MapboxMapController(
6467
private val attributionController: AttributionController
6568
private val scaleBarController: ScaleBarController
6669
private val compassController: CompassController
70+
private val viewportController: ViewportController
6771

6872
private val eventHandler: MapboxEventHandler
6973

@@ -145,6 +149,7 @@ class MapboxMapController(
145149
attributionController = AttributionController(mapView)
146150
scaleBarController = ScaleBarController(mapView)
147151
compassController = CompassController(mapView)
152+
viewportController = ViewportController(mapView.viewport, mapView.camera, context, mapboxMap)
148153

149154
changeUserAgent(pluginVersion)
150155

@@ -160,6 +165,7 @@ class MapboxMapController(
160165
AttributionSettingsInterface.setUp(messenger, attributionController, this.channelSuffix)
161166
ScaleBarSettingsInterface.setUp(messenger, scaleBarController, this.channelSuffix)
162167
CompassSettingsInterface.setUp(messenger, compassController, this.channelSuffix)
168+
_ViewportMessenger.setUp(messenger, viewportController, this.channelSuffix)
163169

164170
methodChannel = MethodChannel(messenger, "plugins.flutter.io.$channelSuffix")
165171
methodChannel.setMethodCallHandler(this)
@@ -197,6 +203,7 @@ class MapboxMapController(
197203
mapView = null
198204
mapboxMap = null
199205
methodChannel.setMethodCallHandler(null)
206+
200207
StyleManager.setUp(messenger, null, channelSuffix)
201208
_CameraManager.setUp(messenger, null, channelSuffix)
202209
Projection.setUp(messenger, null, channelSuffix)
@@ -209,6 +216,7 @@ class MapboxMapController(
209216
CompassSettingsInterface.setUp(messenger, null, channelSuffix)
210217
ScaleBarSettingsInterface.setUp(messenger, null, channelSuffix)
211218
AttributionSettingsInterface.setUp(messenger, null, channelSuffix)
219+
_ViewportMessenger.setUp(messenger, null, channelSuffix)
212220
}
213221

214222
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
@@ -246,6 +254,9 @@ class MapboxMapController(
246254
result.success(byteArray)
247255
}
248256
}
257+
"mapView#submitViewSizeHint" -> {
258+
result.success(null) // no-op on this platform
259+
}
249260
else -> {
250261
result.notImplemented()
251262
}

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)