Flutterでカスタムマーカーを作った

## 作れるものイメージ
![スクリーンショット 2024-11-08 215203.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/844247/380ba929-b076-11ff-06dc-2fd5d66b49bc.png)
## 作り方
### PictureRecorderとCanvasを使います
今回はmarkerに数字を与えたく、引数に`index`を与えられるような実装にしてみました

```custom_marker_icon.dart
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';

class CustomMarkerIcon {
  static Future<BitmapDescriptor> createMarkerIcon(int index) async {
    // マーカーのサイズを定義
    const int markerWidth = 50; // 幅
    const double circleRadius = 15; // 円の半径
    const double triangleHeight = 30; // 三角形の高さ
    const double triangleBaseWidth = circleRadius * 2; // 三角形の底辺の幅（円の直径に合わせる）
    const double overlapOffset = circleRadius / 2; // 円と三角形の重なりを調整するオフセット

    // 描画を開始
    final ui.PictureRecorder pictureRecorder = ui.PictureRecorder();
    final Canvas canvas = Canvas(pictureRecorder);

    // 円を描画（やや下に配置して三角形と重ねる）
    final Paint paint = Paint()..color = Colors.blue;
    const Offset circleCenter =
        Offset(markerWidth / 2, circleRadius + overlapOffset);
    canvas.drawCircle(circleCenter, circleRadius, paint);

    // 逆三角形を描画（やや上に配置して円と重ねる）
    final Path trianglePath = Path()
      ..moveTo((markerWidth - triangleBaseWidth) / 2,
          circleRadius * 2 - overlapOffset) // 左上
      ..lineTo((markerWidth + triangleBaseWidth) / 2,
          circleRadius * 2 - overlapOffset) // 右上
      ..lineTo(markerWidth / 2,
          circleRadius * 2 + triangleHeight - overlapOffset) // 頂点
      ..close();
    canvas.drawPath(trianglePath, paint);

    // 円の中央にテキスト（index）を描画
    final TextPainter textPainter = TextPainter(
      text: TextSpan(
        text: "$index",
        style: const TextStyle(
          fontSize: 12.0, 
          color: Colors.white,
          fontWeight: FontWeight.bold,
        ),
      ),
      textDirection: TextDirection.ltr,// お作法的に必要
    );
    textPainter.layout();
    final Offset textOffset = Offset(
      (markerWidth - textPainter.width) / 2,
      circleCenter.dy - textPainter.height / 2,
    );
    textPainter.paint(canvas, textOffset);

    // Canvasを画像に変換
    final ui.Image markerImage = await pictureRecorder.endRecording().toImage(
          markerWidth,
          (circleRadius * 2 + triangleHeight - overlapOffset).toInt(),
        );
    final ByteData? byteData =
        await markerImage.toByteData(format: ui.ImageByteFormat.png);

    if (byteData == null) return BitmapDescriptor.defaultMarker;

    // ByteDataからBitmapDescriptorを作成
    return BitmapDescriptor.bytes(byteData.buffer.asUint8List());
  }
}
```
## 使い方
```dart
import 'package:google_maps_flutter/google_maps_flutter.dart';

final markers = <Marker>{};
markers.add(
    Marker(
        position: const LatLng(35.6811673, 139.7670516),
        markerId: MarkerId("$markerId"),
        icon: await CustomMarkerIcon.createMarkerIcon(markerId)
    )
);

// GoogleMapウィジェットのmarkersフィールドにセットして使用
GoogleMap(
    markers: markers
)
```
### ちょっと解説
Canvasを使って、図形を重ねているだけです。
かなり計算が多く、どんな形でも行けるわけではありませんが適宜変更すればそれっぽいものが作れるのではないでしょうか？

## 参考
https://note.com/yuh_programming/n/nbbe28500fce9

https://qiita.com/ling350181/items/32b95563c40a1f8af20c

https://api.flutter.dev/flutter/dart-ui/Canvas-class.html