이번 시간에는 Flutter Map에서 마커를 생성하는 방법을 알아봅시다.

 

Flutter_Map 같은 경우, Layer 구조로 되어 있는데, 쉽게 말해서 2D 평면이 여러개 겹쳐있다고 보시면 됩니다.

Flutter_Map 여러가지 레이어가 존재하는데, 그중 대표적으로 MarkerLayer가 존재합니다.

 

구현 코드는 다음과 비슷합니다. 

FlutterMap(
    mapController: mapController.mapController,
    options: MapOptions(
      initialCenter: currentLocation ?? latLng.LatLng(51.509364, -0.128928),
      initialZoom: zoom,
    ),
    children: [
      TileLayer(
        urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
        userAgentPackageName: 'com.example.app',
      ),

      MarkerLayer(
        markers: markers,
      ),
    ],
),

 

MarkerLayer는 기본적으로 List<Marker> 형태로 받습니다. 물론, 마커가 한개가 아니라 여러개일 가능성이 크니까요.

 

Marker 타입은 Flutter_Map에서 지원하는 클래스이므로 다음과 같은 형태를 갖습니다.

Marker(
    point: latLng.LatLng(latitude, longitude),
    width: 60,
    height: 60,
    child: myWidget()
    )
)

 

point는 당연히 위도 경도 좌표가 되고, LatLng 형태의 객체를 받습니다. latitude와 longitude는 double 형태에 자료형입니다.

width와 height는 마커의 크기를 나타냅니다.

child는  widget 자식을 받는데, 원하는 Widget으로 나타낼 수 있습니다.

 

이전에 선언했던 currentLocation 변수를 통해 코드를 다음과 같이 수정하면 현재 위치에 마커를 놓을 수 있습니다.

 

Marker(
    point: currentLocation!,
    width: 60,
    height: 60,
    child: CustomMarkerIcon(
      longitude: currentLocation!.longitude,
      latitude: currentLocation!.latitude,
      isPlace: false,
      imagePath: "asset/img/portrait.webp",
      size: Size(400.0, 400.0),
    ),
),

 

저같은 경우, CustomMarkerIcon 이라는 위젯을 만들어 화면에 나타내기로 했습니다.

 

마커 놓기

 

다음 화면과 같이 여러개의 Marker를 놓아서 화면에 예쁘게 나타낼 수 있음을 확인할수 있습니다.

 

CustomMarkerIcon의 코드는 다음과 같습니다.

import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:mapdesign_flutter/LocationInfo/marker_clicked.dart';
import 'dart:async';
import 'dart:ui' as ui;
import 'dart:typed_data';

Future<ui.Image> getImageFromPath(String imagePath) async {
  final Completer<ui.Image> completer = Completer();
  final AssetImage assetImage = AssetImage(imagePath);
  assetImage.resolve(ImageConfiguration()).addListener(
    ImageStreamListener(
          (ImageInfo info, bool _) => completer.complete(info.image),
    ),
  );
  return completer.future;
}

Future<Uint8List> createMarkerImage(String imagePath, Size size) async {
  final ui.PictureRecorder pictureRecorder = ui.PictureRecorder();
  final Canvas canvas = Canvas(pictureRecorder);

  final Radius radius = Radius.circular(size.width / 2);

  final Paint tagPaint = Paint()..color = Colors.blue;
  final double tagWidth = 80.0;

  final Paint shadowPaint = Paint()..color = Colors.blue.withAlpha(100);
  final double shadowWidth = 15.0;

  final Paint borderPaint = Paint()..color = Colors.white;
  final double borderWidth = 3.0;

  final double imageOffset = shadowWidth + borderWidth;

// Add shadow circle
  canvas.drawRRect(
      RRect.fromRectAndCorners(
        Rect.fromLTWH(
            0.0,
            0.0,
            size.width,
            size.height
        ),
        topLeft: radius,
        topRight: radius,
        bottomLeft: radius,
        bottomRight: radius,
      ),
      shadowPaint);

// Add border circle
  canvas.drawRRect(
      RRect.fromRectAndCorners(
        Rect.fromLTWH(
            shadowWidth,
            shadowWidth,
            size.width - (shadowWidth * 2),
            size.height - (shadowWidth * 2)
        ),
        topLeft: radius,
        topRight: radius,
        bottomLeft: radius,
        bottomRight: radius,
      ),
      borderPaint);

// Add tag text
  TextPainter textPainter = TextPainter(textDirection: TextDirection.ltr);
  textPainter.text = TextSpan(
    text: '1',
    style: TextStyle(fontSize: 40.0, color: Colors.white),
  );

  textPainter.layout();
  textPainter.paint(
      canvas,
      Offset(
          size.width - tagWidth / 2 - textPainter.width / 2,
          tagWidth / 2 - textPainter.height / 2
      )
  );

// Oval for the image
  Rect oval = Rect.fromLTWH(
      imageOffset,
      imageOffset,
      size.width - (imageOffset * 2),
      size.height - (imageOffset * 2)
  );

// Add path for oval image
  canvas.clipPath(Path()
    ..addOval(oval));

// Add image
  ui.Image image = await getImageFromPath(imagePath); // Alternatively use your own method to get the image
  paintImage(canvas: canvas, image: image, rect: oval, fit: BoxFit.fitWidth);


  final ui.Image markerAsImage = await pictureRecorder.endRecording().toImage(
      size.width.toInt(),
      size.height.toInt()
  );

  final ByteData? byteData = await markerAsImage.toByteData(format: ui.ImageByteFormat.png);
  return byteData!.buffer.asUint8List();
}

Future<BitmapDescriptor> getMarkerIcon(String imagePath, Size size) async {
  Uint8List markerImage = await createMarkerImage(imagePath, size);
  return BitmapDescriptor.fromBytes(markerImage);
}

class CustomMarkerIcon extends StatefulWidget {
  final String imagePath;
  final Size size;
  final bool isPlace;
  final double latitude;
  final double longitude;
  final String category;
  CustomMarkerIcon({required this.imagePath, required this.size, this.isPlace = false,
    required this.latitude, required this.longitude, this.category = "none"});

  @override
  _CustomMarkerIconState createState() => _CustomMarkerIconState();
}

class _CustomMarkerIconState extends State<CustomMarkerIcon> {
  Uint8List? imageBytes;
  @override
  void initState() {
    super.initState();
    _loadImage();
  }
  Future<dynamic> _getPlaceInfo(){
    if(widget.isPlace){
      return showModalBottomSheet(
          isScrollControlled: true,
          context: context,
          builder: (BuildContext context){
            return Container(
              height: MediaQuery.of(context).size.height,
              child: MarkerClicked(latitude:widget.latitude, longitude: widget.longitude, category: widget.category)
            );
          }
      );
    }
    else {
      return Future.value();
    }
  }
  Future<void> _loadImage() async {
    imageBytes = await createMarkerImage(widget.imagePath, widget.size);
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    if (imageBytes == null) {
      return Container();
    }
    return GestureDetector(
      onTap: _getPlaceInfo,
      child: Image.memory(imageBytes!),
    );
  }
}

 

GestureDetector를 Image.memory 만 사용하시는걸 권장합니다.

제가 사용하는 코드는 마커를 클릭시 별도의 API를 요청하기 때문에 그부분은 따로 빼셔야 합니다.

 

참고자료: https://docs.fleaflet.dev/#demonstration