본문 바로가기

Flutter/12 Clone 'Used Goods app'

[Flutter] Clone - 당근마켓46(Map - 3)

이번에는 지도에 화면 중간을 기준으로 특정 거리 이내에 (작성자 위치기준)위치한 게시글을 화면에 표시해보겠습니다.

개발환경 : 윈도우11, 안드로이드 스튜디오, flutter 3.0.1

 

 

 

구현한 화면은 아래와 같습니다. 붉은 마커는 로그인 사용자 위치이고 검은색 마커는 테스트용 마커입니다.

 

 

 

 

./src/repo/item_service.dart - 메소드 추가, 특정 위치 주변의 게시글 검색(위도/경도 기반으로 검색)

 

Future<List<ItemModel2>> getNearByItems(String userKey, LatLng latLng) async {
  // GeoFlutterFire is an open-source library that allows you to store
  // and query firestore documents based on their geographic location.
  final geo = Geoflutterfire();
  final itemCol = FirebaseFirestore.instance.collection(COL_ITEMS);

  GeoFirePoint center = GeoFirePoint(latLng.latitude, latLng.longitude);
  double radius = 2; // unit is km
  var field = 'geoFirePoint';

  // within - 리턴 타입 Stream, first - 리턴 타입 Future,
  List<DocumentSnapshot<Map<String, dynamic>>> snapshots = await geo
      .collection(collectionRef: itemCol)
      .within(center: center, radius: radius, field: field)
      .first;

  List<ItemModel2> items = [];
  for (var snapshot in snapshots) {
    ItemModel2 itemModel = ItemModel2.fromJson(snapshot.data()!);
    itemModel.itemKey = snapshot.id;
    itemModel.reference = snapshot.reference;
    //todo: remove my own item, 내가 등록한 게시글 제외,
    if (itemModel.userKey != userKey) {
      items.add(itemModel);
    }
  }

  return items;
}

 

 

 

./src/screens/near/map_screen.dart - 이미지 기반 마커 추가(해당 게시글 위치와 검색 기준 위치 간 거리표시), 이미지 클릭시, 해당 게시글로 이동

 

import 'package:apple_market3/src/models/item_model.dart';
import 'package:apple_market3/src/models/user_model.dart';
import 'package:apple_market3/src/repo/item_service.dart';
import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
import 'package:geoflutterfire/geoflutterfire.dart';
import 'package:get/get.dart';
import 'package:latlng/latlng.dart';
import 'package:map/map.dart';

import '../../constants/data_keys.dart';

/*
toLatLng(Offset position) → LatLng
Converts XY coordinates to LatLng.

toOffset(LatLng location) → Offset
Converts LatLng coordinates to XY Offset.
*/

class MapScreen extends StatefulWidget {
  final UserModel1 _userModel;

  const MapScreen(this._userModel, {Key? key}) : super(key: key);

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

class _MapScreenState extends State<MapScreen> {
  late MapController _mapController;

  Offset? _dragStart;
  double _scaleData = 1.0;

  // 터치하는 순간만 실행, 드래그 해도 최초 터치 지점을 표시,
  _scaleStart(ScaleStartDetails details) {
    // focalPoint: Offset(191.2, 426.9), 스크린을 터치한 시작점,
    // localFocalPoint: Offset(191.2, 346.9), pointersCount: 1)
    _dragStart = details.focalPoint;
    _scaleData = 1.0;
    debugPrint('_scaleStart ${_dragStart.toString()} / ${details.toString()}');
  }

  // 드레그 하는 동안 계속 실행됨,
  _scaleUpdate(ScaleUpdateDetails details, MapTransformer transformer) {
    // debugPrint('_scaleUpdate ${details.focalPoint.toString()}');
    var _scaleDiff = details.scale - _scaleData;
    _scaleData = details.scale;
    _mapController.zoom += _scaleDiff;

    if (_scaleDiff > 0) {
      _mapController.zoom += 0.02;
    } else if (_scaleDiff < 0) {
      _mapController.zoom -= 0.02;
    } else {
      final now = details.focalPoint;
      final diff = now - _dragStart!;
      _dragStart = now;
      transformer.drag(diff.dx, diff.dy);
      // debugPrint('_scaleUpdate/diff ${diff.dx}/${diff.dy}');
    }
    setState(() {});
    // debugPrint('_scaleUpdate ${_mapController.center.latitude}/${_mapController.center.longitude}');
  }

  // 마커 위치/색상 설정, x/y 자표로 입력
  Widget _buildMarkerWidget(Offset offset, {Color color = Colors.red}) {
    return Positioned(
      left: offset.dx,
      top: offset.dy,
      width: 24,
      height: 24,
      child: Icon(
        Icons.location_on,
        color: color,
      ),
    );
  }

  Widget _buildImgWidget(Offset offset, ItemModel2 itemModel, LatLng centerLatLng) {
    final distance = GeoFirePoint.kmDistanceBetween(
      to: Coordinates(itemModel.geoFirePoint.latitude, itemModel.geoFirePoint.longitude),
      from: Coordinates(centerLatLng.latitude, centerLatLng.longitude),
    );

    return Positioned(
      left: offset.dx,
      top: offset.dy,
      width: 50,
      height: 100,
      child: InkWell(
        onTap: () {
          debugPrint('${itemModel.userKey}, ${itemModel.title}, distance :[$distance]');
          Get.toNamed(
              ROUTE_ITEM_DETAIL,
              arguments: {'itemKey': itemModel.itemKey},
              // 같은 페이지는 호출시, 중복방지가 기본설정인, false 하면 중복 호출 가능,
              preventDuplicates: false
          );
          // context.beamToNamed('/$LOCATION_ITEM/:${itemModel.itemKey}');
        },
        child: Column(
          children: [
            ExtendedImage.network(
              width: 32,
              height: 32,
              itemModel.imageDownloadUrls[0],
              shape: BoxShape.circle,
              fit: BoxFit.cover,
            ),
            Text(
              '$distance km',
              style: const TextStyle(color: Colors.black, fontSize: 10.0),
            ),
          ],
        ),
      ),
    );
  }

  @override
  void initState() {
    // ------------------- 테스트 데이터 자동 입력 코드 -------------------
    // generateData(widget._userModel.userKey, widget._userModel.geoFirePoint);

    // 맵컨트롤러의 초기 좌표를 설정
    _mapController = MapController(
      location: LatLng(
        widget._userModel.geoFirePoint.latitude,
        widget._userModel.geoFirePoint.longitude,
      ),
    );
    super.initState();
  }

  // 버전이 업데이트 되며서 변경된 위젯명, Map -> TileLayer, MapLayoutBuilder -> MapLayout
  @override
  Widget build(BuildContext context) {
    // debugPrint("************************* >>> build from MapScreen");
    return MapLayout(
      builder: (context, transformer) {
        // 위도/경도 정보를 화면상의 위치(x,y 좌표)로 변경, fromLatLngToXYCoords -> toOffset
        var myLatLng = LatLng(
          widget._userModel.geoFirePoint.latitude,
          widget._userModel.geoFirePoint.longitude,
        );

        final Offset myLocationOnMap = transformer.toOffset(myLatLng);

        final myLocationWidget = _buildMarkerWidget(myLocationOnMap);

        Size _size = MediaQuery.of(context).size;
        final middleOnScreen = Offset(_size.width / 2, _size.height / 2);
        final List centerLocationWidget = [
          _buildMarkerWidget(middleOnScreen, color: Colors.black87),
          _buildMarkerWidget(const Offset(0, 0), color: Colors.black87),
          _buildMarkerWidget(Offset(_size.width - 20, 0), color: Colors.black87),
          _buildMarkerWidget(Offset(0, _size.height - 160), color: Colors.black87),
          _buildMarkerWidget(Offset(_size.width - 20, _size.height - 160), color: Colors.black87),
        ];

        // toLatLng(Offset position) → LatLng
        // Converts XY coordinates to LatLng.
        // fromXYCoordsToLatLng -> toLatLng
        // 화면의 중간점(x,y 좌표)를 위도/경도 로 변환,
        final middleLatLngOnMap = transformer.toLatLng(middleOnScreen);
        // Screen size : [384.0, 838.4]
        // debugPrint('Screen size : [${_size.width}, ${_size.height}]');
        // debugPrint(
        //     'Screen center : [${middleLatLngOnMap.latitude.toString()}] [${middleLatLngOnMap.longitude.toString()}]');

        return FutureBuilder<List<ItemModel2>>(
            future: ItemService().getNearByItems(widget._userModel.userKey, middleLatLngOnMap),
            builder: (context, snapshot) {
              List<Widget> nearByItems = [];
              if (snapshot.hasData) {
                for (var item in snapshot.data!) {
                  final offset = transformer
                      .toOffset(LatLng(item.geoFirePoint.latitude, item.geoFirePoint.longitude));
                  nearByItems.add(_buildImgWidget(offset, item, middleLatLngOnMap));
                }
              }

              return Stack(
                children: [
                  GestureDetector(
                    onScaleStart: _scaleStart,
                    onScaleUpdate: (details) => _scaleUpdate(details, transformer),
                    child: TileLayer( // Map -> TileLayer
                      builder: (context, x, y, z) {
                        //Google Maps
                        final url =
                            'https://www.google.com/maps/vt/pb=!1m4!1m3!1i$z!2i$x!3i$y!2m3!1e0!2sm!3i420120488!3m7!2sen!5e1105!12m4!1e68!2m2!1sset!2sRoadmap!4e0!5m1!1e0!23i4111425';

                        return ExtendedImage.network(
                          url,
                          fit: BoxFit.cover,
                        );
                      },
                    ),
                  ),
                  myLocationWidget,
                  ...centerLocationWidget,
                  ...nearByItems,
                ],
              );
            });
      },
      controller: _mapController,
    );
  }
}