flutterでgoogle map上に2点間を結ぶ円弧のアニメーションを表示します。飛行機のフライト経路のイメージです。
まずは完成形です。東京タワーとスカイツリーを円弧で結んでいます。
重要なポイントは下記2点。
######google mapの緯度経度に対応する画面の座標を取得する
######2点間を結ぶ円弧の座標を求める
ちなみに、google mapのpolylineは使用しません。
使いどころとしては、Directions APIにお金をかけてまでpolylineを描きたくないけど、それっぽいUIにしたい時に有用です。
#google mapの緯度経度に対応する画面の座標を取得する
GoogleMapControllerのgetScreenCoordinateを使います。緯度経度を渡してやると、google mapの表示領域の左上を(0,0)として、x座標、y座標がピクセルで取得できます。
GoogleMap(
onMapCreated: (controller) async {
final _start = await controller.getScreenCoordinate(
LatLng(35.65872867304855, 139.745454355318));
final _end = await controller.getScreenCoordinate(
LatLng(35.710245623812234, 139.8107647706607));
これで円弧開始点と終了点の(x,y)を取得できました。
google mapの役割はこれだけです。polylineは使用しません。
#2点間を結ぶ円弧の座標を求める
取得した円弧開始点と終了点の(x,y)を使って、円弧を描く座標を求めます。
計算式は下記です。三角関数を使って求めます。
[変数説明]
開始点 : (_sx,_sy)
終了点 : (_ex,_ey)
円弧を描く座標 : (x,y)
θ : _angle
final s = (_sx + _ex) / 2;
final t = (_sy + _ey) / 2;
final A = math.sqrt(math.pow((_sx - _ex), 2) + math.pow((_sy - _ey), 2)) / 2;
final B = math.sqrt(math.pow((_sx - _ex), 2) + math.pow((_sy - _ey), 2)) / 6;
final COS = (_ex - _sx) / math.sqrt(math.pow((_sx - _ex), 2) + math.pow((_sy - _ey), 2));
final SIN = (_ey - _sy) / math.sqrt(math.pow((_sx - _ex), 2) + math.pow((_sy - _ey), 2));
final x = A * math.cos(_angle) * COS - B * math.sin(_angle) * SIN + s;
final y = A * math.cos(_angle) * SIN + B * math.sin(_angle) * COS + t;
_angleを0から3.14まで変化させて円弧を描く全ての座標を取得します。
仕上
AnimationControllerで_angleを変化させながら、google mapにオーバレイするように円弧を描きます。flutterエンジニアには説明不要でしょう。ソースをご覧ください。
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'dart:math' as math;
class MapView extends StatefulWidget {
@override
_MapViewState createState() => _MapViewState();
}
class _MapViewState extends State<MapView> with TickerProviderStateMixin {
List<Marker> _marker = [];
late Animation _animation;
late AnimationController _controller;
late Tween _tween;
double _angle = 0.0;
double _sx = 0.0;
double _sy = 0.0;
double _ex = 0.0;
double _ey = 0.0;
final List<double> _xList = [];
final List<double> _yList = [];
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void initState() {
_marker.add(Marker(
markerId: MarkerId('start'),
position: LatLng(35.65872867304855, 139.745454355318),
zIndex: 11,
));
_marker.add(Marker(
markerId: MarkerId('end'),
position: LatLng(35.710245623812234, 139.8107647706607),
zIndex: 11,
));
super.initState();
}
@override
Widget build(BuildContext context) {
return _googleMap();
}
Widget _googleMap() {
return Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Stack(
children: [
GoogleMap(
onMapCreated: (controller) async {
final _start = await controller.getScreenCoordinate(
LatLng(35.65872867304855, 139.745454355318));
final _end = await controller.getScreenCoordinate(
LatLng(35.710245623812234, 139.8107647706607));
final _devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
_sx = _start.x.toDouble() / _devicePixelRatio;
_sy = _start.y.toDouble() / _devicePixelRatio;
_ex = _end.x.toDouble() / _devicePixelRatio;
_ey = _end.y.toDouble() / _devicePixelRatio;
_xList.add(_sx);
_yList.add(_sy);
_createAnimation();
},
initialCameraPosition: CameraPosition(
target: LatLng(35.67911303080764, 139.786195735142),
zoom: 12.0,
),
markers: _marker.toSet(),
mapType: MapType.normal,
myLocationEnabled: false,
zoomGesturesEnabled: false,
scrollGesturesEnabled: false,
myLocationButtonEnabled: false,
),
CustomPaint(
painter: DrawLinePainter(
xList: _xList,
yList: _yList,
sx: _sx,
sy: _sy,
ex: _ex,
ey: _ey))
],
));
}
void _createAnimation() {
_tween = Tween(begin: 3.14, end: 0);
_controller = AnimationController(
vsync: this, duration: Duration(milliseconds: 2000));
_animation = _tween.animate(
CurvedAnimation(parent: _controller, curve: Curves.easeOut))
..addListener(() {
setState(() {
print(_animation.value);
if (_animation.value is int) {
_angle = _animation.value.toDouble();
} else {
_angle = _animation.value;
}
final s = (_sx + _ex) / 2;
final t = (_sy + _ey) / 2;
final A =
math.sqrt(math.pow((_sx - _ex), 2) + math.pow((_sy - _ey), 2)) /
2;
final B =
math.sqrt(math.pow((_sx - _ex), 2) + math.pow((_sy - _ey), 2)) /
6;
final COS = (_ex - _sx) /
math.sqrt(math.pow((_sx - _ex), 2) + math.pow((_sy - _ey), 2));
final SIN = (_ey - _sy) /
math.sqrt(math.pow((_sx - _ex), 2) + math.pow((_sy - _ey), 2));
final x = A * math.cos(_angle) * COS - B * math.sin(_angle) * SIN + s;
final y = A * math.cos(_angle) * SIN + B * math.sin(_angle) * COS + t;
_xList.add(x);
_yList.add(y);
});
})
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
_xList.clear();
_yList.clear();
_xList.add(_sx);
_yList.add(_sy);
_controller.reset();
_controller.forward();
}
});
_controller.forward();
}
}
class DrawLinePainter extends CustomPainter {
final List<double> xList;
final List<double> yList;
final double sx;
final double sy;
final double ex;
final double ey;
DrawLinePainter(
{required this.xList,
required this.yList,
required this.sx,
required this.sy,
required this.ex,
required this.ey});
@override
void paint(Canvas canvas, Size size) {
if (xList.isNotEmpty) {
final Paint line = new Paint()
..color = Colors.black
..strokeCap = StrokeCap.square
..style = PaintingStyle.stroke
..strokeWidth = 3.0;
for (var i = 0; i < xList.length - 1; i++) {
canvas.drawLine(Offset(xList[i], yList[i]),
Offset(xList[i + 1], yList[i + 1]), line);
}
final Paint line2 = new Paint()
..color = Colors.grey.withOpacity(0.5)
..strokeCap = StrokeCap.square
..style = PaintingStyle.fill
..strokeWidth = 3.0;
canvas.drawLine(Offset(sx, sy), Offset(ex, ey), line2);
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
#注意点
####getScreenCoordinateのピクセル値について
androidでは取得した値をそのまま使えません。
MediaQuery.of(context).devicePixelRatioで割ることで、画面のdpを取得できます。
iOSでは取得した値をそのまま使えます。
####getScreenCoordinateの安定感について
androidでは常に安定して正確な値が取得できますが、iOSではとんでもない桁数の値が取得できてしまい、円弧が描けないことがあります。現状、原因不明です。
####google map zoomGesturesEnabledの無効化
zoomされると円弧がずれます。