[Flutter] 현재 위치의 일정 반경 안에 있는 특정 좌표에 마커 표시(구글 맵)

박재빈·2024년 10월 28일
1

Flutter를 사용해서 구글 맵에서 현재 위치와 일정 반경 안에 있는 특정 좌표에 마커를 표시하는 방법을 정리한 글입니다.

1. 프로젝트 설정

pubspec.yaml파일에 필요한 패키지를 추가합니다.

dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.8
  google_maps_flutter: ^2.9.0
  geolocator: ^13.0.1
  location: ^7.0.1

해당 패키지들을 사용하기 위한 설정은 아래의 pub.dev 에서 확인하고 설정해주시면 됩니다.

google_maps_flutter
https://pub.dev/packages/google_maps_flutter

location
https://pub.dev/packages/location

geolocator
https://pub.dev/packages/geolocator

2. 위치 추적 및 마커 표시

2.1 기본 코드 작성

map_screen.dart

import 'dart:typed_data';

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:geolocator/geolocator.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:location/location.dart';
import 'package:map_route_test/components/custom_marker.dart';
import 'dart:ui' as ui;

class MapScreen extends StatefulWidget {
  const MapScreen({super.key});

  @override
  State<MapScreen> createState() => _MapScreenState();
}

class _MapScreenState extends State<MapScreen> {
  late GoogleMapController _controller;
  Location _location = Location();
  late User _me;
  Map<String, Marker> _userMarkers = {};
  Map<String, BitmapDescriptor> _customMarkerIcons = {};

  List<User> _otherUsers = [];

  int _callCount = 0;

  @override
  void initState() {
    super.initState();
    _getMyUserInfo();
    _getOtherUsers();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _initializeLocationTracking();
    });
  }

  void _getOtherUsers() {
    _otherUsers.add(User(
        id: 2,
        name: 'User2',
        imageUrl: '이미지 URL',
        latitude: 35.1994,
        longitude: 129.0055));
    _otherUsers.add(User(
        id: 3,
        name: 'User3',
        imageUrl: '이미지 URL',
        latitude: 35.2035,
        longitude: 129.0039));
    _otherUsers.add(User(
        id: 4,
        name: 'User4',
        imageUrl: '이미지 URL',
        latitude: 35.2053,
        longitude: 129.0043));
    _otherUsers.add(User(
        id: 5,
        name: 'User5',
        imageUrl: '이미지 URL',
        latitude: 35.2070,
        longitude: 129.0048));
    _otherUsers.add(User(
        id: 6,
        name: 'User6',
        imageUrl: '이미지 URL',
        latitude: 35.2100,
        longitude: 129.0055));
  }

  void _getMyUserInfo() {
    _me = User(
        id: 1,
        name: 'Me',
        imageUrl: '이미지 URL',
        latitude: 35.2115,
        longitude: 129.0045);
    _buildCustomMarker(_me);
  }

  void _initializeLocationTracking() async {
    if (await requestLocationPermission()) {
      _location.onLocationChanged
          .listen((LocationData currentLocation) {
        setState(() {
          print("latitude : ${currentLocation.latitude}");
          print("longitude : ${currentLocation.longitude}");
          _callCount = _callCount + 1;
          _me = _me.copyWith(
              latitude: currentLocation.latitude,
              longitude: currentLocation.longitude);
          _userMarkers.clear();
          _updateUserMarker(_me);
          for (var otherUser in _otherUsers) {
            var distance = _calculateDistance(
                currentLocation.latitude!,
                currentLocation.longitude!,
                otherUser.latitude,
                otherUser.longitude);
            if (distance <= 1000) {
              print("거리 내부 : ${otherUser.id}");
              _updateUserMarker(otherUser);
            }
          }
        });
      });
    }
  }

  // 사용자 위치에 마커 업데이트
  void _updateUserMarker(User user) {
    final icon = _customMarkerIcons[user.id.toString()];
    print('icon user ${user.id}: $icon');
    if (icon == null) return; // 아이콘이 없으면 무시

    final position = LatLng(user.latitude, user.longitude);

    final newMarker = Marker(
      markerId: MarkerId(user.id.toString()),
      position: position,
      icon: icon,
    );

    _userMarkers[user.id.toString()] = newMarker;
  }

  // 캡처한 CustomMarker를 비트맵으로 변환하는 함수
  Future<BitmapDescriptor> _captureMarkerImage(
      GlobalKey globalKey, User user) async {
    // 캡처 로직 구현
    try {
      RenderRepaintBoundary boundary =
          globalKey.currentContext!.findRenderObject() as RenderRepaintBoundary;
      ui.Image image = await boundary.toImage(pixelRatio: 1.0);
      ByteData? byteData =
          await image.toByteData(format: ui.ImageByteFormat.png);
      return BitmapDescriptor.bytes(byteData!.buffer.asUint8List());
    } catch (e) {
      throw Exception('위젯 렌더링에 실패했습니다. ${user.id} : $e');
    }
  }

  double _calculateDistance(double startLatitude, double startLongitude,
      double endLatitude, double endLongitude) {
    return Geolocator.distanceBetween(
        startLatitude, startLongitude, endLatitude, endLongitude);
  }

  Widget _buildCustomMarker(User user) {
    print('_build ${user.id}');
    // if(_customMarkerIcons.containsKey(user.id.toString())) {
    //   return const SizedBox.shrink();
    // }

    return CustomMarker(
      imageUrl: user.imageUrl,
      onRendering: (globalKey) async {
        print('user ${user.id} onRendering');
        final icon = await _captureMarkerImage(globalKey, user);
        print('onRendering $icon');
        setState(() {
          _customMarkerIcons[user.id.toString()] = icon;
        });
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          GoogleMap(
            initialCameraPosition: CameraPosition(
                target: LatLng(_me.latitude, _me.longitude), zoom: 15),
            markers: Set<Marker>.of(_userMarkers.values),
            onMapCreated: (controller) {
              _controller = controller;
            },
            myLocationEnabled: true,
          ),
          Positioned(
            left: 20,
            bottom: 20,
            child: Container(
              padding: const EdgeInsets.all(12),
              color: Colors.white,
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text('call count : $_callCount'),
                  Text('latitude : ${_me.latitude}'),
                  Text('longitude : ${_me.longitude}'),
                ],
              ),
            ),
          ),
          // CustomMarker 추가 (예를 들어, 미리 렌더링된 위젯으로 보여줌)
          // Positioned(
          //   top: -9999,
          //   left: -9999,
          //   child: _buildCustomMarker(_me),
          // ),
          // for (var otherUser in _otherUsers)
          //   Positioned(
          //     top: -9999,
          //     left: -9999,
          //     child: _buildCustomMarker(otherUser),
          //   ),
          Positioned(
            top: 0,
            left: 0,
            child: Column(
              children: [
                _buildCustomMarker(_me),
                for(var otherUser in _otherUsers)
                  _buildCustomMarker(otherUser),
              ],
            ),
          )
        ],
      ),
    );
  }
}

class User {
  final int id;
  final String name;
  final String imageUrl;
  final double latitude;
  final double longitude;

  User({
    required this.id,
    required this.name,
    required this.imageUrl,
    required this.latitude,
    required this.longitude,
  });

  // copyWith 메서드
  User copyWith({
    int? id,
    String? name,
    String? imageUrl,
    double? latitude,
    double? longitude,
  }) {
    return User(
      id: id ?? this.id,
      name: name ?? this.name,
      imageUrl: imageUrl ?? this.imageUrl,
      latitude: latitude ?? this.latitude,
      longitude: longitude ?? this.longitude,
    );
  }
}

현재 위치 변경 추적

  • _initializeLocationTracking()를 통해서 위치 변경을 추적합니다.
  • _location.onLocationChanged.listen(...)는 현재 위치를 수신합니다.
  • 수신한 현재 위치를 통해서 다른 사용자와의 거리를 계산하여 1000m 이내의 사용자를 마커로 표시합니다.

커스텀 마커 생성

  • _captureMarkerImageCustomMarker 위젯을 캡처하여 BitmapDescriptor으로 변환하고 구글 맵에 표시합니다.
  • CustomMarker을 캡처하여 반환된 BitmapDescriptor_customMarkerIcons에 담습니다.
  • _customMarkerIcons는 마커를 생성할 떄 user.id를 key로 하여 찾은 후 사용합니다.

거리 계산

  • _calculateDistanceGeolocator.distanceBetween을 사용해 두 좌표 간의 거리를 계산합니다.
  • 반경 내에 있는 사용자만 마커를 추가합니다.

custom_marker.dart

import 'package:flutter/material.dart';

class CustomMarker extends StatefulWidget {
  final String imageUrl;
  final Function(GlobalKey globalKey) onRendering;

  const CustomMarker({super.key, required this.imageUrl, required this.onRendering});

  @override
  State<CustomMarker> createState() => _CustomMarkerState();
}

class _CustomMarkerState extends State<CustomMarker> {
  final GlobalKey _globalKey = GlobalKey();

  @override
  void initState() {
    super.initState();
    _loadNetworkImage();
  }
  
  void _loadNetworkImage() {
    final ImageStream stream = NetworkImage(widget.imageUrl).resolve(ImageConfiguration());
    stream.addListener(ImageStreamListener((image, synchronousCall) {
      WidgetsBinding.instance.addPostFrameCallback((_) {
        widget.onRendering(_globalKey);
      });
    }, onError: (exception, stackTrace) {
      print('$exception');
    }, onChunk: (event) {
      print('$event');
    },));
  }

  @override
  Widget build(BuildContext context) {
    return RepaintBoundary(
      key: _globalKey,
      child: Container(
        width: 50,
        height: 50,
        decoration: BoxDecoration(
            color: Colors.white,
            shape: BoxShape.circle,
            border: Border.all(color: Colors.redAccent, width: 4)),
        child: ClipOval(
          child: Image.network(
            widget.imageUrl,
            fit: BoxFit.cover,
          ),
        ),
      ),
    );
  }
}

RepaintBoundary

GoogleMap의 markers에 필요한 Marker객체에 icon은 BitmapDescriptor 타입입니다.
그래서 위젯을 마커로 사용하기 위해서는 위젯을 BitmapDescriptor으로 변환할 필요가 있습니다. 그래서 위젯을 이미지로 찍어낼 수 있는 RepaintBoundary위젯을 사용합니다.

onRendering

  • CustomMarker가 빌드된 후 이미지가 로드되면 onRendering를 호출합니다.

결과 이미지

  • 현재 위치를 변경했을때 사라진 마커와 생긴 마커를 확인 할 수 있습니다.
  • 안드로이드 에뮬레이터에서 위치 설정은 ...을 클릭 후 지도에서 검색 또는 포인트를 찍어 SET LOCATION을 하면 설정할 수 있습니다.

0개의 댓글