Skip to content

Commit 0f244cc

Browse files
evil159lantah-1pjleonard37
authored
Snapshotter (#512)
* feat: Add snapshot for Android * feat: Add snapshot for iOS * chore: add script for snapshot * Replace offscreen snapshotting with the static one * Make snapshotter return image instead of byte list * Add changelog entry * Fix Android build error * Remove debug code from the snapshotter example * Add integrations tests for snapshotter * Change snapshot return type to int list * Remove debug print * Apply suggestions from code review Co-authored-by: Patrick Leonard <pjleonard37@users.noreply.github.com> * MapWidget snapshotting (#513) * Add method to capture snapshot from mapbox map * Change snapshot type to int list * Add integration test * Update example/lib/snapshotter.dart Co-authored-by: Patrick Leonard <pjleonard37@users.noreply.github.com> * Remove commented out code --------- Co-authored-by: lantah <lsxt10@qq.com> Co-authored-by: Patrick Leonard <pjleonard37@users.noreply.github.com>
1 parent e530497 commit 0f244cc

40 files changed

+3641
-1778
lines changed

CHANGELOG.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,47 @@ mapboxMap?.location.updateSettings(LocationComponentSettings(
1616
LocationPuck(locationPuck2D: DefaultLocationPuck2D(topImage: list, shadowImage: Uint8List.fromList([]))))
1717
);
1818
```
19+
##### Snapshots
20+
21+
###### Standalone snapshotter
22+
23+
Show multiple maps at the same time with no performance penalty. With the all new `Snapshotter` you can get image snapshots of the map, styled the same way as `MapWidget`.
24+
25+
The `Snapshotter` class is highly configurable. You can set the final result at the time of construction using the `MapSnapshotOptions`. Once you've configured your snapshot, you can start the snapshotting process.
26+
27+
One of the key features of the `Snapshotter` class is the `style` object. This object can be manipulated to set different styles for your snapshot, as well as to apply runtime styling to the style, giving you the flexibility to create a snapshot that fits your needs.
28+
29+
```dart
30+
final snapshotter = await Snapshotter.create(
31+
options: MapSnapshotOptions(
32+
size: Size(width: 400, height: 400),
33+
pixelRatio: MediaQuery.of(context).devicePixelRatio),
34+
onStyleLoadedListener: (_) {
35+
// apply runtime styling
36+
final layer = CircleLayer(id: "circle-layer", sourceId: "poi-source");
37+
snapshotter?.style.addLayer(layer);
38+
},
39+
);
40+
snapshotter.style.setStyleURI(MapboxStyles.STANDARD);
41+
snapshotter.setCamera(CameraOptions(center: Point(...)));
42+
43+
...
44+
45+
final snapshotImage = await snapshotter.start()
46+
```
47+
##### Map wiget snapshotting
48+
49+
Create snapshots of the map displayed in the `MapWidget` with `MapboxMap.snapshot()`. This new feature allows you to capture a static image of the current map view.
50+
51+
The `snapshot()` method captures the current state of the Mapbox map, including all visible layers, markers, and user interactions.
52+
53+
To use the snapshot() method, simply call it on your Mapbox map instance. The method will return a Future that resolves to the image of the current map view.
54+
55+
```dart
56+
final snapshotImage = await mapboxMap.snapshot();
57+
```
58+
59+
Please note that the `snapshot()` method works best if the Mapbox Map is fully loaded before capturing an image. If the map is not fully loaded, the method might return a blank image.
1960

2061
#### ⚠️ Breaking changes
2162

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.mapbox.maps.mapbox_maps
2+
3+
import com.mapbox.maps.Snapshotter
4+
5+
// ⚠️ Danger zone ⚠️
6+
7+
fun Snapshotter.styleManager(): com.mapbox.maps.StyleManager {
8+
return javaClass.getDeclaredField("coreSnapshotter").let {
9+
it.isAccessible = true
10+
return@let it.get(this) as com.mapbox.maps.StyleManager
11+
}
12+
}
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
package com.mapbox.maps.mapbox_maps
2+
3+
import com.google.gson.Gson
4+
import com.google.gson.GsonBuilder
5+
import com.google.gson.JsonElement
6+
import com.google.gson.JsonPrimitive
7+
import com.google.gson.JsonSerializationContext
8+
import com.google.gson.JsonSerializer
9+
import com.google.gson.TypeAdapter
10+
import com.google.gson.TypeAdapterFactory
11+
import com.google.gson.reflect.TypeToken
12+
import com.google.gson.stream.JsonReader
13+
import com.google.gson.stream.JsonWriter
14+
import com.mapbox.common.Cancelable
15+
import com.mapbox.maps.CameraChangedCallback
16+
import com.mapbox.maps.MapIdleCallback
17+
import com.mapbox.maps.MapLoadedCallback
18+
import com.mapbox.maps.MapLoadingErrorCallback
19+
import com.mapbox.maps.Observable
20+
import com.mapbox.maps.RenderFrameFinishedCallback
21+
import com.mapbox.maps.RenderFrameStartedCallback
22+
import com.mapbox.maps.ResourceRequestCallback
23+
import com.mapbox.maps.SourceAddedCallback
24+
import com.mapbox.maps.SourceDataLoadedCallback
25+
import com.mapbox.maps.SourceRemovedCallback
26+
import com.mapbox.maps.StyleDataLoadedCallback
27+
import com.mapbox.maps.StyleImageMissingCallback
28+
import com.mapbox.maps.StyleImageRemoveUnusedCallback
29+
import com.mapbox.maps.StyleLoadedCallback
30+
import com.mapbox.maps.mapbox_maps.pigeons._MapEvent
31+
import io.flutter.plugin.common.BinaryMessenger
32+
import io.flutter.plugin.common.MethodCall
33+
import io.flutter.plugin.common.MethodChannel
34+
import java.lang.reflect.Type
35+
import java.util.Date
36+
37+
class MapboxEventHandler(
38+
private val eventProvider: Observable,
39+
binaryMessenger: BinaryMessenger
40+
) : MethodChannel.MethodCallHandler {
41+
private val channel: MethodChannel
42+
private val cancellables = HashSet<Cancelable>()
43+
private val gson = GsonBuilder()
44+
.registerTypeAdapter(Date::class.java, MicrosecondsDateTypeAdapter)
45+
.registerTypeAdapterFactory(EnumOrdinalTypeAdapterFactory)
46+
.create()
47+
48+
init {
49+
channel = MethodChannel(binaryMessenger, "com.mapbox.maps.flutter.map_events")
50+
channel.setMethodCallHandler(this)
51+
}
52+
53+
override fun onMethodCall(methodCall: MethodCall, result: MethodChannel.Result) {
54+
if (methodCall.method == "subscribeToEvents" && methodCall.arguments is List<*>) {
55+
cancellables.forEach { it.cancel() }
56+
cancellables.clear()
57+
58+
val rawEventTypes = methodCall.arguments as List<Int>
59+
60+
rawEventTypes
61+
.map { _MapEvent.ofRaw(it) }
62+
.filterNotNull()
63+
.forEach { subscribeToEvent(it) }
64+
result.success(null)
65+
} else {
66+
result.notImplemented()
67+
}
68+
}
69+
70+
private fun subscribeToEvent(event: _MapEvent) {
71+
when (event) {
72+
_MapEvent.MAP_LOADED -> eventProvider.subscribe(
73+
MapLoadedCallback {
74+
channel.invokeMethod(event.methodName, gson.toJson(it))
75+
}
76+
).also { cancellables.add(it) }
77+
_MapEvent.MAP_LOADING_ERROR -> eventProvider.subscribe(
78+
MapLoadingErrorCallback {
79+
channel.invokeMethod(event.methodName, gson.toJson(it))
80+
}
81+
).also { cancellables.add(it) }
82+
_MapEvent.STYLE_LOADED -> eventProvider.subscribe(
83+
StyleLoadedCallback {
84+
channel.invokeMethod(event.methodName, gson.toJson(it))
85+
}
86+
).also { cancellables.add(it) }
87+
_MapEvent.STYLE_DATA_LOADED -> eventProvider.subscribe(
88+
StyleDataLoadedCallback {
89+
channel.invokeMethod(event.methodName, gson.toJson(it))
90+
}
91+
).also { cancellables.add(it) }
92+
_MapEvent.CAMERA_CHANGED -> eventProvider.subscribe(
93+
CameraChangedCallback {
94+
channel.invokeMethod(event.methodName, gson.toJson(it))
95+
}
96+
).also { cancellables.add(it) }
97+
_MapEvent.MAP_IDLE -> eventProvider.subscribe(
98+
MapIdleCallback {
99+
channel.invokeMethod(event.methodName, gson.toJson(it))
100+
}
101+
).also { cancellables.add(it) }
102+
_MapEvent.SOURCE_ADDED -> eventProvider.subscribe(
103+
SourceAddedCallback {
104+
channel.invokeMethod(event.methodName, gson.toJson(it))
105+
}
106+
).also { cancellables.add(it) }
107+
_MapEvent.SOURCE_REMOVED -> eventProvider.subscribe(
108+
SourceRemovedCallback {
109+
channel.invokeMethod(event.methodName, gson.toJson(it))
110+
}
111+
).also { cancellables.add(it) }
112+
_MapEvent.SOURCE_DATA_LOADED -> eventProvider.subscribe(
113+
SourceDataLoadedCallback {
114+
channel.invokeMethod(event.methodName, gson.toJson(it))
115+
}
116+
).also { cancellables.add(it) }
117+
_MapEvent.STYLE_IMAGE_MISSING -> eventProvider.subscribe(
118+
StyleImageMissingCallback {
119+
channel.invokeMethod(event.methodName, gson.toJson(it))
120+
}
121+
).also { cancellables.add(it) }
122+
_MapEvent.STYLE_IMAGE_REMOVE_UNUSED -> eventProvider.subscribe(
123+
StyleImageRemoveUnusedCallback {
124+
channel.invokeMethod(event.methodName, gson.toJson(it))
125+
}
126+
).also { cancellables.add(it) }
127+
_MapEvent.RENDER_FRAME_STARTED -> eventProvider.subscribe(
128+
RenderFrameStartedCallback {
129+
channel.invokeMethod(event.methodName, gson.toJson(it))
130+
}
131+
).also { cancellables.add(it) }
132+
_MapEvent.RENDER_FRAME_FINISHED -> eventProvider.subscribe(
133+
RenderFrameFinishedCallback {
134+
channel.invokeMethod(event.methodName, gson.toJson(it))
135+
}
136+
).also { cancellables.add(it) }
137+
_MapEvent.RESOURCE_REQUEST -> eventProvider.subscribe(
138+
ResourceRequestCallback {
139+
channel.invokeMethod(event.methodName, gson.toJson(it))
140+
}
141+
).also { cancellables.add(it) }
142+
}
143+
}
144+
}
145+
146+
object EnumOrdinalTypeAdapterFactory : TypeAdapterFactory {
147+
override fun <T : Any?> create(gson: Gson?, type: TypeToken<T>?): TypeAdapter<T>? {
148+
if (type == null || !type.rawType.isEnum) {
149+
return null
150+
}
151+
152+
return EnumOrdinalTypeAdapter()
153+
}
154+
}
155+
156+
object MicrosecondsDateTypeAdapter : JsonSerializer<Date> {
157+
override fun serialize(
158+
src: Date,
159+
typeOfSrc: Type?,
160+
context: JsonSerializationContext?
161+
): JsonElement {
162+
return JsonPrimitive(src.time * 1000)
163+
}
164+
}
165+
166+
class EnumOrdinalTypeAdapter<T>() : TypeAdapter<T>() {
167+
override fun write(out: JsonWriter?, value: T) {
168+
out?.value((value as Enum<*>).ordinal)
169+
}
170+
override fun read(`in`: JsonReader?): T {
171+
throw NotImplementedError("Not supported")
172+
}
173+
}
174+
175+
private val _MapEvent.methodName: String
176+
get() = "event#$ordinal"

0 commit comments

Comments
 (0)