flutterでgoogle map上に2点間を結ぶ円弧のアニメーションを表示する

flutterでgoogle map上に2点間を結ぶ円弧のアニメーションを表示します。飛行機のフライト経路のイメージです。
まずは完成形です。東京タワーとスカイツリーを円弧で結んでいます。
<img width="300" alt="sample.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278927/06ff8bca-244a-6374-ccb2-b3cb35062f27.gif">

重要なポイントは下記２点。
######google mapの緯度経度に対応する画面の座標を取得する
######２点間を結ぶ円弧の座標を求める

ちなみに、google mapのpolylineは使用しません。
使いどころとしては、Directions APIにお金をかけてまでpolylineを描きたくないけど、それっぽいUIにしたい時に有用です。
#google mapの緯度経度に対応する画面の座標を取得する
*GoogleMapController*の*getScreenCoordinate*を使います。緯度経度を渡してやると、google mapの表示領域の左上を(0,0)として、x座標、y座標がピクセルで取得できます。

```dart
  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は使用しません。

#２点間を結ぶ円弧の座標を求める
取得した円弧開始点と終了点の(x,y)を使って、円弧を描く座標を求めます。
計算式は下記です。三角関数を使って求めます。

[変数説明]
開始点 : (_sx,_sy)
終了点 : (_ex,_ey)
円弧を描く座標 : (x,y)
θ : _angle

```dart
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エンジニアには説明不要でしょう。ソースをご覧ください。

```dart
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されると円弧がずれます。