본문 바로가기

Flutter/12 Clone 'Used Goods app'

[Flutter] Clone - 당근마켓45(Map - 2)

이번에는 구글 지도에 마커를 추가해보겠습니다.

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

 

 

 

구현한 화면은 아래와 같습니다.

 

 

 

 

./src/screens/near/map_screen.dart - userModel 을 파라미터로 전달하여 사용자 위치에 마커를 표시

 

import 'package:apple_market3/src/models/user_model.dart';
import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
import 'package:latlng/latlng.dart';
import 'package:map/map.dart';

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,
      ),
    );
  }

  @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
        final Offset myLocationOnMap = transformer.toOffset(LatLng(
          widget._userModel.geoFirePoint.latitude,
          widget._userModel.geoFirePoint.longitude,
        ));
        debugPrint('user Location [${widget._userModel.geoFirePoint.latitude}, ${widget._userModel.geoFirePoint.longitude}]');

        final myLocationWidget = _buildMarkerWidget(myLocationOnMap);//, color: Colors.black87);

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

        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,
          ],
        );
      },
      controller: _mapController,
    );
  }
}

 

 

 

./src/screens/main_screen.dart - 지도 화면 추가

 

body: IndexedStack(
  index: _bottomSelectedIndex,
  children: <Widget>[
    const ItemsScreen(),
    (UserController.to.userModel.value == null)
        ? Container()
        : MapScreen(UserController.to.userModel.value!),
    Container(color: Colors.accents[3]),
    Container(color: Colors.accents[5]),
    Container(color: Colors.accents[7]),
  ],
),

 

 

 

./src/screens/home/items_screen.dart - FutureBuilder 가 정상적으로 완료후에 데이터가 없는 경우, shimmer 를 보여주는데, 이 경우는 메인화면으로 전환되는게 적절하여 조건문 수정함.

 

return FutureBuilder<List<ItemModel2>>(
    // future: Future.delayed(const Duration(seconds: 2), () => 100),
    future: ItemService().getItems(UserController.to.user.value!.uid),
    //(화면전화 이전에 매핑되므로 사용가능)FirebaseAuth.instance.currentUser!.uid
    //(화면전화 이전에 매핑되므로 사용가능)UserController.to.user.value!.uid
    //(화면전화 이후에 매핑되므로 사용불가)UserController.to.userModel.value!.userKey
    builder: (context, snapshot) {
      return AnimatedSwitcher(
        duration: const Duration(seconds: 1),
        // child: (snapshot.connectionState == ConnectionState.done)
        child: (snapshot.hasData) //  && snapshot.data!.isNotEmpty
            ? _listView(imgSize, snapshot.data!)
            : _shimmerListView(imgSize),
      );
    });
    

변경전
child: (snapshot.hasData && snapshot.data!.isNotEmpty)
변경후
child: (snapshot.hasData)