モバイルSDKでもParty Parrot
はじめに
この記事は「もう一つのParty Parrot」の続きです。Mapbox Maps SDK for Android、 Mapbox Maps SDK for iOSおよびMapbox Maps SDK Flutter PluginでParrotをPartyさせる方法について見ていきます。
Partyの開催方法
Mapbox GL JSでは以下のようなコードを記述しました。
- ParrotのGIF動画画像をフレームに分解
- Mapのロード完了を待つ
- Parrotの各フレーム画像をダウンロード
-
addImage
で画像を登録 -
addSource
でGeoJSONソースを登録 -
addLayer
でシンボルレイヤーを作成 - タイマーで50ms毎にシンボルレイヤーの画像を変更
実は、モバイルSDKでも全く同じ手順でPartyできます。ただし、モバイルということで、この記事ではAssetsやバンドルにGeoJSONおよび画像を入れて読み込んでいます。もちろん、これらをネットワーク経由で取得するように実装するのも、もちろんOKです。
それではAndroid、iOS、Flutterの順に実装を見ていきましょう。今回は各プラットフォーム向けにサンプルを作成しました。ただし、挙動を示すための簡単なサンプルで、エラー処理などは行っていません。製品に使用する際にはご注意ください。
Android
サンプルコード
以下のコードがサンプルです。ダウンロードして使用する際にはapp/src/main/res/values/strings.xml
のYOUR_MAPBOX_PUBLIC_TOKEN
の部分に自分のパブリックトークンを設定してください。
自分でプロジェクトを作る際には以下のInstallationガイドをご参照ください。
処理内容
地図を表示する場所にMapView
タグを配置します。サンプルではactivity_main.xml
に以下を指定しています。中心座標等もここで設定していますが、コードから設定することもできます。
<com.mapbox.maps.MapView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:mapbox="http://schemas.android.com/apk/res-auto"
android:id="@+id/mapView"
android:layout_width="match_parent"
android:layout_height="match_parent"
mapbox:mapbox_cameraTargetLat="35.6811649"
mapbox:mapbox_cameraTargetLng="139.763906"
mapbox:mapbox_cameraZoom="14.0"
/>
次にKotlinのコードを見ていきます。地図のロードにはloadStyle
系のメソッドを使用します。ここではloadStyleUri
を使用し、第一引数にスタイルのURLを指定しています。第二引数が地図のロードが完了されたときに呼び出されるコールバック関数をしています。
mapView?.getMapboxMap()?.loadStyleUri(Style.LIGHT){
...
}
コールバックの中では以下の処理を行います
- 画像の登録
- GeoJSONソースの登録
- シンボルレイヤーの作成
- タイマーで50ms毎にシンボルレイヤーの画像を変更
まずは画像の登録です。drawable
にframe0.png
〜frame9.png
の画像が入っているので、読み込んでStyle#addImage
で登録します。
it.addImage("parrot0", BitmapFactory.decodeResource(resources, R.drawable.frame0))
it.addImage("parrot1", BitmapFactory.decodeResource(resources, R.drawable.frame1))
it.addImage("parrot2", BitmapFactory.decodeResource(resources, R.drawable.frame2))
it.addImage("parrot3", BitmapFactory.decodeResource(resources, R.drawable.frame3))
it.addImage("parrot4", BitmapFactory.decodeResource(resources, R.drawable.frame4))
it.addImage("parrot5", BitmapFactory.decodeResource(resources, R.drawable.frame5))
it.addImage("parrot6", BitmapFactory.decodeResource(resources, R.drawable.frame6))
it.addImage("parrot7", BitmapFactory.decodeResource(resources, R.drawable.frame7))
it.addImage("parrot8", BitmapFactory.decodeResource(resources, R.drawable.frame8))
it.addImage("parrot9", BitmapFactory.decodeResource(resources, R.drawable.frame9))
次にGeoJSONソースを登録します。party.json
がassets
ディレクトリに入っているので、以下のようにGeoJsonSource
として読み込みます。さらにaddSource
でソースを登録します。
val source = GeoJsonSource.Builder("party-source").url("asset://party.json").build()
it.addSource(source)
GeoJSONソースからシンボルレイヤーを作成します。初期画像はparrot0
(frame0.png
)で、サイズも32x32に縮小します。addLayer
でレイヤーを作成します。
val layer = SymbolLayer("party-layer", "party-source")
layer.iconImage("parrot0")
layer.iconSize(0.25)
it.addLayer(layer)
最後にタイマーで50ms毎にiconImage
の値を更新します。ここで、Maps SDKに対する操作は必ずメインスレッドで行わなければならないことに注意してください。これはSDKがスレッドセーフではないことに起因します。たとえば、ワーカースレッドからの操作(例えばレイヤーの追加・削除)を行った場合、データ競合が発生する可能性があります。
var counter = 0;
val handler = Handler(Looper.getMainLooper())
handler.postDelayed(object: Runnable{
override fun run() {
handler.postDelayed(this, 50)
layer.iconImage("parrot${(++counter) % 10}")
}
}, 100)
結果は以下のとおりです。
おまけ - DSL
AndroidはDSLと呼ばれる記法でもレイヤーの作成が可能です。具体的には以下のように記述します。
mapView?.getMapboxMap()?.loadStyle(
style(styleUri = Style.LIGHT) {
+image("parrot0") {
bitmap(BitmapFactory.decodeResource(resources, R.drawable.frame0))
}
+image("parrot1") {
bitmap(BitmapFactory.decodeResource(resources, R.drawable.frame1))
}
+image("parrot2") {
bitmap(BitmapFactory.decodeResource(resources, R.drawable.frame2))
}
+image("frame3") {
bitmap(BitmapFactory.decodeResource(resources, R.drawable.frame3))
}
+image("parrot4") {
bitmap(BitmapFactory.decodeResource(resources, R.drawable.frame4))
}
+image("parrot5") {
bitmap(BitmapFactory.decodeResource(resources, R.drawable.frame5))
}
+image("parrot6") {
bitmap(BitmapFactory.decodeResource(resources, R.drawable.frame6))
}
+image("parrot7") {
bitmap(BitmapFactory.decodeResource(resources, R.drawable.frame7))
}
+image("parrot8") {
bitmap(BitmapFactory.decodeResource(resources, R.drawable.frame8))
}
+image("parrot9") {
bitmap(BitmapFactory.decodeResource(resources, R.drawable.frame9))
}
+geoJsonSource("party-source") {
url("asset://party.json")
}
+symbolLayer("party-layer", "party-source") {
iconImage("frame0")
iconSize(0.25)
}
}
) {
var counter = 0;
val handler = Handler(Looper.getMainLooper())
handler.postDelayed(object: Runnable{
override fun run() {
handler.postDelayed(this, 50)
val layer = it.getLayer("party-layer") as SymbolLayer
layer.iconImage("parrot${(++counter) % 10}")
}
}, 100)
}
iOS
サンプルコード
以下のコードがサンプルです。ダウンロードして使用する際にはParty Parrot/ViewController.swift
のYOUR_MAPBOX_PUBLIC_TOKEN
の部分に自分のパブリックトークンを設定してください。
自分でプロジェクトを作る際には以下のInstallationガイドをご参照ください。
処理内容
MapView
はコードの中で作成し、ViewController
の子として表示します。
let resourceOptions = ResourceOptions(accessToken: YOUR_MAPBOX_PUBLIC_TOKEN)
let centerCoordinate = CLLocationCoordinate2D(latitude: 35.6811649, longitude: 139.763906)
let mapInitOptions = MapInitOptions(
resourceOptions: resourceOptions,
cameraOptions: CameraOptions(center: centerCoordinate, zoom: 14.0),
styleURI: .light)
mapView = MapView(frame: view.bounds, mapInitOptions: mapInitOptions)
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.view.addSubview(mapView)
地図のロード完了イベント発生時に行う処理はonNext
のコールバックとして記述します。onNext
第一引数で指定されたイベントが発生したときに一回だけ実行します。ちなみに、onNext
の他にonEvery
があり、イベントが発生すると毎回実行する処理を記述することができます。
mapView.mapboxMap.onNext(event: .mapLoaded) { _ in
...
}
コールバックの中では以下の処理を行います
- 画像の登録
- GeoJSONソースの登録
- シンボルレイヤーの作成
- タイマーで50ms毎にシンボルレイヤーの画像を変更
まずは画像の登録です。Assets.xcassets
にframe0
〜frame9
としての画像が登録されているので、読み込んでaddImage
で登録します。
try! self.mapView.mapboxMap.style.addImage(UIImage(named: "frame0")!, id: "parrot0")
try! self.mapView.mapboxMap.style.addImage(UIImage(named: "frame1")!, id: "parrot1")
try! self.mapView.mapboxMap.style.addImage(UIImage(named: "frame2")!, id: "parrot2")
try! self.mapView.mapboxMap.style.addImage(UIImage(named: "frame3")!, id: "parrot3")
try! self.mapView.mapboxMap.style.addImage(UIImage(named: "frame4")!, id: "parrot4")
try! self.mapView.mapboxMap.style.addImage(UIImage(named: "frame5")!, id: "parrot5")
try! self.mapView.mapboxMap.style.addImage(UIImage(named: "frame6")!, id: "parrot6")
try! self.mapView.mapboxMap.style.addImage(UIImage(named: "frame7")!, id: "parrot7")
try! self.mapView.mapboxMap.style.addImage(UIImage(named: "frame8")!, id: "parrot8")
try! self.mapView.mapboxMap.style.addImage(UIImage(named: "frame9")!, id: "parrot9")
次にGeoJSONソースを登録します。party.json
がassets
ディレクトリに入っているので、以下のようにGeoJsonSource
として読み込みます。さらにaddSource
でソースを登録します。
guard let featureCollection = try? self.decodeGeoJSON(from: "party") else { return }
var source = GeoJSONSource()
source.data = .featureCollection(featureCollection)
try! self.mapView.mapboxMap.style.addSource(source, id: "party-source")
GeoJSONソースからシンボルレイヤーを作成します。初期画像はparrot0
(frame0.png
)で、サイズも32x32に縮小します。addLayer
でレイヤーを作成します。
var layer = SymbolLayer(id: "party-layer")
layer.source = "party-source"
layer.iconImage = .constant(.name("parrot0"))
layer.iconSize = .constant(0.25)
try! self.mapView.mapboxMap.style.addLayer(layer)
最後にタイマーで50ms毎にiconImage
の値を更新します。ここで、Maps SDKに対する操作は必ずメインスレッドで行わなければならないことに注意してください。これはSDKがスレッドセーフではないことに起因します。たとえば、ワーカースレッドからの操作(例えばレイヤーの追加・削除)を行った場合、データ競合が発生する可能性があります。
var counter = 0;
Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true, block: { _ in
counter += 1
try! self.mapView.mapboxMap.style.updateLayer(withId: "party-layer", type: SymbolLayer.self) { layer in
layer.iconImage = .constant(.name("parrot\(counter % 10)"))
}
})
結果は以下のとおりです。
おまけ - ヘルパー関数
GeoJSONをバンドルから読み込む際に以下のヘルパー関数を使用しました。これはSDKのサンプルのコードを拝借しました。
internal func decodeGeoJSON(from fileName: String) throws -> FeatureCollection? {
guard let path = Bundle.main.path(forResource: fileName, ofType: "json") else {
preconditionFailure("File '\(fileName)' not found.")
}
let filePath = URL(fileURLWithPath: path)
var featureCollection: FeatureCollection?
do {
let data = try Data(contentsOf: filePath)
featureCollection = try JSONDecoder().decode(FeatureCollection.self, from: data)
} catch {
print("Error parsing data: \(error)")
}
return featureCollection
}
Flutter
サンプルコード
以下のコードがサンプルです。ダウンロードして使用する際にはlib/main.dart
のYOUR_MAPBOX_PUBLIC_TOKEN
の部分に自分のパブリックトークンを設定してください。
自分でプロジェクトを作る際には以下のInstallationガイドをご参照ください。
処理内容
以下を参考にMapWidget
を作成します。
onMapCreated
に地図がロード完了した際のコールバックを記述します。ここでは以下のように別の関数として定義しています。
_onMapCreated(MapboxMap mapboxMap) async {
...
}
コールバックの中では以下の処理を行います
- 画像の登録
- GeoJSONソースの登録
- シンボルレイヤーの作成
- タイマーで50ms毎にシンボルレイヤーの画像を変更
まずは画像の登録です。assets
にframe0.png
〜frame9.png
の画像が入っているので、読み込んでaddImage
で登録します。
await mapboxMap.style.addStyleImage("parrot0", 1.0, await _getImage("assets/frame0.png"), false, [], [], null);
await mapboxMap.style.addStyleImage("parrot1", 1.0, await _getImage("assets/frame1.png"), false, [], [], null);
await mapboxMap.style.addStyleImage("parrot2", 1.0, await _getImage("assets/frame2.png"), false, [], [], null);
await mapboxMap.style.addStyleImage("parrot3", 1.0, await _getImage("assets/frame3.png"), false, [], [], null);
await mapboxMap.style.addStyleImage("parrot4", 1.0, await _getImage("assets/frame4.png"), false, [], [], null);
await mapboxMap.style.addStyleImage("parrot5", 1.0, await _getImage("assets/frame5.png"), false, [], [], null);
await mapboxMap.style.addStyleImage("parrot6", 1.0, await _getImage("assets/frame6.png"), false, [], [], null);
await mapboxMap.style.addStyleImage("parrot7", 1.0, await _getImage("assets/frame7.png"), false, [], [], null);
await mapboxMap.style.addStyleImage("parrot8", 1.0, await _getImage("assets/frame8.png"), false, [], [], null);
await mapboxMap.style.addStyleImage("parrot9", 1.0, await _getImage("assets/frame9.png"), false, [], [], null);
次にGeoJSONソースを登録します。party.json
がassets
ディレクトリに入っているので、以下のようにGeoJsonSource
として読み込みます。さらにaddSource
でソースを登録します。
var geojson = await rootBundle.loadString('assets/party.json');
var source = GeoJsonSource(id: "party-source", data: geojson);
await mapboxMap.style.addSource(source);
GeoJSONソースからシンボルレイヤーを作成します。初期画像はparrot0
(frame0.png
)で、サイズも32x32に縮小します。addLayer
でレイヤーを作成します。
var layer = SymbolLayer(
id: "party-layer",
sourceId: "party-source",
iconImage: "parrot0",
iconSize: 0.25,
);
await mapboxMap.style.addLayer(layer);
最後にタイマーで50ms毎にiconImage
の値を更新します。
var counter = 0;
Timer.periodic(const Duration(milliseconds: 50), (timer) async {
layer.iconImage = "parrot${(++counter) % 10}";
await mapboxMap.style.updateLayer(layer);
});
結果は以下のとおりです。
おまけ - ヘルパー関数
画像をAssetから読み込む際に以下のヘルパー関数を使用しました。これはSDKのサンプルのコードを参考にしました。
Future<MbxImage> _getImage(String path) async {
final ByteData bytes = await rootBundle.load(path);
final Uint8List image = bytes.buffer.asUint8List();
return new MbxImage(width: 128, height: 128, data: image);
}
もっとParty
各サンプルでparty.json
をparty1000.json
に変更するとたくさんPartyできます!
まとめ
プラットフォーム毎に多少の違いはあれど、基本的に同じ処理方法でParty Parrotが実装できることがわかりました。Party Parrotに限らず、ソース/レイヤー/ソースに関連する操作はプラットフォーム関係なく同じ様な方法で操作できるように設計されています。
Discussion