Mobility TechnologiesでFlutterエンジニアとして働いているTomiと申します。
タクシーアプリ「GO」ではGoogle MapにMarker(Taxi)を動かす場合が多くあります。Flutterではどのように動かせるのかを調査しましたので、この内容を共有します。
Google Map上のMarkerを移動するとき、ネイティブ(Android, iOS)側では表示しているMarkerを参照して、そのpositionを少しつづ変更することで対応できます。 しかしながら、Flutterでは表示しているMarkerを参照することはできません。 本記事では、FlutterでMarkerの移動をどのように実現するのかを解説します。
Google Mapを表示するために、google_maps_flutterライブラリ「22年11月現在v2.2.1」を使います。
$ flutter pub add google_maps_flutter
上記のコマンドを実行してライブラリをインストールします。
そして各プラットフォーム側で設定が必要です。
Flutterプロジェクトを立ち上げるとAndroidのminSdkVersionは16に設定されていますが、Google Mapでは20以上求めており、20以上で設定する必要がります。
// android/app/build.gradle
android {
defaultConfig {
// ...省略
minSdkVersion 20
// ...省略
}
}
そしてAndroidManifest.xmlにGoogle Map Api Keyを設定したら完了です。
<!-- android/app/src/main/AndroidManifest.xml -->
<application>
...省略
<meta-data android:name="com.google.android.geo.API_KEY"
android:value="####"/>
</application>
import UIKit
import Flutter
import GoogleMaps // NEW[1]
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GMSServices.provideAPIKey("YOUR KEY HERE") // NEW[2]
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
上記のNEW[1]にimport文を追加してNEW[2]にGoogle Map Api Keyを入れてください。
// lib/main.dart
import 'package:google_maps_flutter/google_maps_flutter.dart';
...省略
GoogleMap(
mapType: MapType.normal,
initialCameraPosition: const CameraPosition(
target: LatLng(35.68165450744266, 139.76708188461404), // TokyoStation
zoom: 14,
),
);
Flutterアプリを起動するとGoogle Mapが表示していることを確認出来ます。
GoogleMap(
mapType: MapType.normal,
initialCameraPosition: _tokyoStation,
markers: <Marker>{
const Marker(
markerId: MarkerId('driverMarker'),
position: LatLng(35.68128517123827, 139.76714151602386),
)
},
),
GoogleMap WidgetのmarkersパラメーターにMarkerを入れると表示されます。
FlutterでMarkerを動かせるためには少しづつ位置を修正したMarkerを入れたGoogleMapをリビルドします。
今回はTweenアニメーションを使って実装しました。
Marker? _marker;
Future<void> _run() async {
final animationController = AnimationController(
duration: const Duration(seconds: 5),
vsync: this,
);
Tween<double> tween = Tween(begin: 0, end: 1);
_animation = tween.animate(animationController)
..addListener(() async {
final v = _animation!.value;
double lng = v * _endLng + (1 - v) * _startLng;
double lat = v * _endLat + (1 - v) * _startLat;
setState(() {
_marker = Marker(
markerId: const MarkerId('driverMarker'),
position: LatLng(lat, lng),
);
});
});
animationController.forward();
}
上記の_run関数を実行するとアニメーションが動いて_marker 変数を少しつづ目的地まで位置情報を更新します。
build(BuildContext context) {
return Scaffold(
body: GoogleMap(
mapType: MapType.normal,
initialCameraPosition: _tokyoStation,
markers: <Marker>{if (_marker != null) _marker!},
),
floatingActionButton: FloatingActionButton(
onPressed: _run,
child: const Text('Start'),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
);
}
Widget
この_markerをGoogleMapのパラメーターで入れておくとMarkerが動くことを確認できます。
しかしながら、上記のコードはMarkerが動く際に、全てのGoogleMapのビルド関数でリビルドが走ってしまうため、性能的に良くないです。この問題を解決するためにリビルドするWidgetを絞る必要がありましてStreamBuilderを使って制限しました。
final _markerStreamController = StreamController<Marker>();
StreamSink<Marker> get _markerSink => _markerStreamController.sink;
Stream<Marker> get _markerStream => _markerStreamController.stream;
Widget build(BuildContext context) {
return Scaffold(
body: StreamBuilder<Marker>(
stream: _markerStream,
builder: (context, snapshot) {
final maker = snapshot.data;
return GoogleMap(
mapType: MapType.normal,
initialCameraPosition: _tokyoStation,
markers: <Marker>{if (maker != null) maker},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: _run,
child: const Text('Start'),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
);
}
Future<void> _run() async {
final animationController = AnimationController(
duration: const Duration(seconds: 5),
vsync: this,
);
Tween<double> tween = Tween(begin: 0, end: 1);
_animation = tween.animate(animationController)
..addListener(() async {
final v = _animation!.value;
double lng = v * _endLng + (1 - v) * _startLng;
double lat = v * _endLat + (1 - v) * _startLat;
_markerSink.add(
Marker(
markerId: const MarkerId('driverMarker'),
position: LatLng(lat, lng),
),
);
});
animationController.forward();
}
StreamControllerとStreamBuilderを使って該当する部分のみリビルドさせるように修正しました。
これでStartボタンを押したらMarkerが動けるようになりました。
ここまで読んでいただき、ありがとうございました!
興味のある方は 採用ページ も見ていただけると嬉しいです。
Twitter @mot_techtalk のフォローもよろしくお願いします!