2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-04-23

flutterでgoogle map上に2点間を結ぶ円弧のアニメーションを表示します。飛行機のフライト経路のイメージです。
まずは完成形です。東京タワーとスカイツリーを円弧で結んでいます。
sample.PNG

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

ちなみに、google mapのpolylineは使用しません。
使いどころとしては、Directions APIにお金をかけてまでpolylineを描きたくないけど、それっぽいUIにしたい時に有用です。
#google mapの緯度経度に対応する画面の座標を取得する
GoogleMapControllergetScreenCoordinateを使います。緯度経度を渡してやると、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されると円弧がずれます。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?