この記事では、Fusetoolsというプロトタイピングツールの
公式パッケージの1つであるFuse.Mapsと、その利用時のTipsについて紹介します。
Fusetoolsの基本的な知識がある方を前提としています。
この記事を通して実現できるもの
- Fusetoolsアプリ上での地図表示。
- 地図上でのマーカー表示を含む。
- 地図上へのUIパーツの配置。(UIパーツ例: 検索窓, メニュー)
- Uberや食べログなど、地図表示を用いるアプリでほぼ避けられない表示形式。
Fuse.Maps パッケージとは
Fusetoolsが公式で提供しているパッケージの1つで、
アプリ上での地図表示を可能とするMapViewクラスを内包。
地図上でのピンの表示も当然可能で、例えばGoogle Places APIと連動させ
Google Map同様の地図をNativeアプリ上で描画することが可能。
地図の回転や、ズーム、当然、現在位置表示にも対応。
今回は、アプリ内での地図表示のための一連の処理を説明しますが、
外部の地図アプリを開くための FuseJS/Maps というモジュールも用意されています。
環境
- Fusetools ver1.4.0 (2017/11/30時点で最新)
使い方
プロジェクトファイルの編集
Fusetoolsのプロジェクトルートに存在する、"プロジェクト名.unoproj"ファイルにて
以下の通り、利用パッケージとしてFuse.Mapsを宣言。
現在地情報が必要な場合は、Fuse.GeoLocationも宣言します。
加えて、Androidでの地図表示向けに、GoogleのApi Keyが必要となる。
以下の具合で、Android.Geo.ApiKey にてApiKeyを設定する。
{
...,
"Packages": [
...
"Fuse.GeoLocation",
"Fuse.Maps",
...
],
...,
"Android": {
"Geo": {
"ApiKey": "ご自身のGoogle API Key"
}
}
}
UXファイル, JSファイルの編集
UXファイル(Fusetools独自のXML記法のHTMLのようなもの)にて、
以下のようにNativeViewHostタグを親とし、その下に各要素を配置する。
<NativeViewHost>
<DockPanel>
<Panel Dock="Top">
<!-- 例えば、ここで定義した検索窓は、地図上に表示される -->
</Panel>
<Panel Layer="Background">
<MapView ShowCompass="true" ShowMyLocation="true" ShowMyLocationButton="true" Latitude="{latitude}" Longitude="{longitude}" Zoom="14" LocationTapped="{onMapTapped}">
<Each Items="{markerList}">
<MapMarker Latitude="{latitude}" Longitude="{longitude}" Label="{name}" Tapped="{onMarkerTapped}" />
</Each>
</MapView>
</Panel>
</DockPanel>
</NativeViewHost>
以下の通り、対応するjsファイル内では、位置情報の取得処理や
var Observable = require("FuseJS/Observable");
// 位置情報の取得
var GeoLocation = require("FuseJS/GeoLocation");
var currentLocation = GeoLocation.location;
var latitude = Observable(currentLocation.latitude);
var longitude = Observable(currentLocation.longitude);
// Google Place API関連
var googlePlaceApiUrl = 'https://maps.googleapis.com/maps/api/place/nearbysearch/json?';
var googleApiKey = 'ご自身のGoogle API Key'
var radiusMeter = 500; // place API の探索半径(メートル)
// 地図上に表示するマーカーオブジェクトを格納するリスト
var markerList = Observable();
// 選択されたマーカーを保持するオブジェクト
var selectedMarker = Observable();
function GooglePlaceMarker(google_palce_result) {
// 一部の代表的なパラメータをセット
this.place_id = google_palce_result['place_id'];
this.name = google_palce_result['name'];
this.price_level = (google_palce_result['price_level']) ? google_palce_result['price_level'] : '-';
this.rating = (google_palce_result['rating']) ? google_palce_result['rating'] : '-';
this.latitude = google_palce_result['geometry']['location']['lat'];
this.longitude = google_palce_result['geometry']['location']['lng'];
this.open_now = (google_palce_result['opening_hours']) ? google_palce_result['opening_hours']['open_now'] : null;
this.permanently_closed = google_palce_result['permanently_closed'];
this.vicinity = (google_palce_result['vicinity']) ? google_palce_result['vicinity'] : '';
this.icon = google_palce_result['icon'];
// マーカーがタップされた際のイベントハンドラー
this.onMarkerTapped = function(self) {
console.log(JSON.stringify(self.data));
selectedMarker.value = self.data;
};
};
function searchGooglePlace() {
var tmpMarkerList = [];
var url = googlePlaceApiUrl + 'location=' + latitude.value + ',' + longitude.value
+ '&radius=' + radiusMeter + '&types=food&key=' + googleApiKey;
fetch(url)
.then(function(response) {
return new Promise(function(resolve, reject) {
if (response.ok) {
resolve(response.json());
} else {
reject(response);
}
});
}).then(function(json) {
json.results.forEach(function(result) {
tmpMarkerList.push(new GooglePlaceMarker(result));
});
// Observableのリストの総入れ替えは、replaceAllでの実行が確実。
markerList.replaceAll(tmpMarkerList);
}).catch(function(e) {
console.log(e.status, e.statusText, e.toString());
});
}
function onMapTapped() {
// マーカーの選択解除
selectedMarker.clear();
}
(function() {
// 地図上への初期表示
searchGooglePlace();
})();
module.exports = {
latitude: latitude,
longitude: longitude,
markerList: markerList,
selectedMarker: selectedMarker,
onSearchClicked: searchGooglePlace,
onMapTapped: onMapTapped,
};
どこに落とし穴があったか
普通に開発者が地図とUIパーツを、Fusetoolsのタグ名を頼りに組もうとすると、
まず、以下のように、MapViewだけを納めたNativeViewHostを作り、
その外に、地図上に表示したい検索窓やメニュー等のパーツを実装すると思います。
しかし、これでは想定通りに動きません。
というのも、NativeViewHostは、UI上最前面に描画されるレイヤーとなるらしく、
この外でUIパーツをいくら定義しようと、ユーザが触れる/見れるところには出てこないのです。
なので、少し気持ち悪いですが、上述のサンプルコード(uxファイル)のように、
NativeViewHost内 にて、必要となるパーツを配置する必要が出てきます。
<Panel Layer="Overlay">
<!-- 地図の上に表示させたいUIパーツ -->
</Panel>
<NativeViewHost>
<MapView ShowCompass="true" ShowMyLocation="true" ShowMyLocationButton="true" Latitude="{latitude}" Longitude="{longitude}" Zoom="14" LocationTapped="{onMapTapped}">
<Each Items="{markerList}">
<MapMarker Latitude="{latitude}" Longitude="{longitude}" Label="{name}" Tapped="{onMarkerTapped}" />
</Each>
</MapView>
</NativeViewHost>
おまけ
Fusetoolsを選ぶべき理由
- プロトタイプと本制作の垣根がもはやなくなる。
- 簡単に、でも、こだわれば独自で綺麗なiOS/Androidアプリを一挙に制作可能。
- 基本的な、Native機能にはアクセスできる。
- WebRTC等は未対応だった気がします。
- デザイナーとエンジニアの連携にも思想としてこだわっている(らしい)。
- 最近、UIビルダーも導入され、エンジニア以外にも敷居が低下。
- 何よりも動作が軽快。
- Androidでも、iOS並みの挙動を期待できる。(これは結構大きい)
- 低レイヤーから独自実装しているので、高い描画パフォーマンスを出せているらしい。
- XMLベースの独自記法であるものの、慣れれば直感的でわかりやすい。
- 豊富な動作サンプル。
- プラットフォーム別(Android / iOS)の記述を必要としない(今のところ経験していない)。
- コミュニティが活発。
- 公式フォーラムに質問を投げかけると、1~2日でFusetoolsのエンジニアから直々に返答がある。(英語)
- 改善要望も、同フォーラムにて容易に提案可能なため、役職問わず意見が回収・検討されやすい。
- アップデート頻度の高さ(いい意味で)。
- 個人で利用するには安価。
- 料金プランは近い将来に、変更の可能性があるらしいものの。
トレンド
久々のFusetoolsに関する記事ということで、
Fusetoolsのトレンド(vs Prott, Origami Studio)を調べてみました。
上昇傾向ではあるものの、伸び悩んではいるようですね。頑張って欲しい。
ノルウェーと韓国での利用が盛んで、他の地域では拡大途上のようです。
参考資料
- https://www.fusetools.com/docs/fuse/controls/mapview
- https://www.fusetools.com/docs/fuse/controls/nativeviewhost
- https://www.fusetools.com/community/forums/howto_discussions/nativeviewhost_is_rendered_in_front_of_whole_app
次回はおそらく、Fusetools上での
画像の切り出し範囲選択(クライアントからの画像投稿用)の実装サンプルを紹介します。
それでは。