본문 바로가기

Flutter/12 Clone 당근마켓

[Flutter] Clone - 당근마켓47 번외(googleMap - 1)

지난번에 구현한 구글 지도를 다른 패키지를 사용해서 구현해 보겠습니다. 로딩 및 drag 기능을 추가해보겠습니다.

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

 

 

 

구현 화면은 아래와 같습니다. - 게시글 제목이 같은 동일한 게시글입니다. 다만, 이미지는 랜덤이라서 다를수 있습니다.

첫번째 스크린 - map/latlng 패키지로 구현

두번째 스크린 - google_maps_flutter 패키지로 구현

세번째 스크린 - 지도에서 클릭한 마커의 상세 페이지

 

 

 

 

새로운 패키지(google_maps_flutter)에는 강력한 기능들이 많지만 오늘은 기본적인 부분만 확인해 보았습니다.

google_maps_flutter 패키지로 구현한 화면은 화면 이동이 조금 더 부드럽다, 하지만 이미지 사이즈를 변경하려면 비트맵이미지를 변경해야해서, 이미지 편집이 복잡하고 제약이 많다.

map/latlng 패키지로 구현한 경우는 일반 위젯을 마커로 사용이 가능해서(위도/경도 정보를 x,y 정보로 변경이 필요하지만) decoration 을 이용해서 쉽게 이미지 편집이 용이하고, 다른 위젯과 합쳐서 다양한 정보를 표시가 가능하다.

테스트 장치는 삼성 A12 입니다.

 

 

 

관련 패키지들

 

map: ^1.3.3
latlng: ^0.2.0
google_maps_flutter: ^2.1.12

 

 

 

./src/models/item_model.dart - 변수추가

 

class ItemModel2 {
  String itemKey;
  String userKey;
  String userPhone;
  List<String> imageDownloadUrls;
  String title;
  String category;
  num price;
  bool negotiable;
  String detail;
  String address;
  GeoFirePoint geoFirePoint;
  DateTime createdDate;
  DocumentReference? reference;
  Uint8List? iconBytes;

 

 

 

./src/screens/main_screen.dart - bottomNavigationBarItem 추가

 

body: IndexedStack(
  index: _bottomSelectedIndex,
  children: <Widget>[
    const ItemsScreen(),
    (UserController.to.userModel.value == null)
        ? Container()
        : MapScreen(UserController.to.userModel.value!),
    (UserController.to.userModel.value == null)
        ? Container()
        : GoogleMapScreen(UserController.to.userModel.value!),
    Container(color: Colors.accents[3]),
    Container(color: Colors.accents[5]),
    Container(color: Colors.accents[7]),
  ],
),
bottomNavigationBar: BottomNavigationBar(
  // 아이콘이 선택되지 않아도 label 이 보이게 하는 옵션
  // shifting 으로 설정하면 클릭시에만 label 이 보임,
  type: BottomNavigationBarType.fixed,
  // 아이콘이 클릭되면 onTap 이 실행되고, 이걸 currentIndex 에 전달해야 함
  onTap: (index) {
    setState(() {
      debugPrint('BottomNavigationBar(index): $index');
      _bottomSelectedIndex = index;
    });
  },
  // 클릭된 화면으로 이동하려면 매핑해야함
  currentIndex: _bottomSelectedIndex,
  // free icons : flaticon.com 에서 다운로드
  items: [
    // 아이콘이 클릭되면 onTap 이 실행됨,
    BottomNavigationBarItem(
      icon: ImageIcon(AssetImage(_bottomSelectedIndex == 0
          ? 'assets/imgs/house_filled.png'
          : 'assets/imgs/house.png')),
      label: 'home',
    ),
    BottomNavigationBarItem(
      icon: ImageIcon(AssetImage(_bottomSelectedIndex == 1
          ? 'assets/imgs/near-me_filled.png'
          : 'assets/imgs/near-me.png')),
      label: 'near',
    ),
    BottomNavigationBarItem(
      icon: ImageIcon(AssetImage(_bottomSelectedIndex == 2
          ? 'assets/imgs/near-me_filled.png'
          : 'assets/imgs/near-me.png')),
      label: 'near2',
    ),
    BottomNavigationBarItem(
      icon: ImageIcon(AssetImage(_bottomSelectedIndex == 3
          ? 'assets/imgs/chat_filled.png'
          : 'assets/imgs/chat.png')),
      label: 'chat',
    ),
    BottomNavigationBarItem(
      // backgroundColor: Theme.of(context).bottomNavigationBarTheme.selectedItemColor,
      icon: ImageIcon(AssetImage(_bottomSelectedIndex == 4
          ? 'assets/imgs/user_filled.png'
          : 'assets/imgs/user.png')),
      label: 'me',
    ),
  ],
),

 

 

 

./src/repo/item_service.dart - 신규 화면에 필요한 함수 추가

 

Future<List<ItemModel2>> getNearByItemsGoogle(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.fromSnapshot(snapshot);
    ItemModel2 itemModel = ItemModel2.fromJson(snapshot.data()!);
    itemModel.itemKey = snapshot.id;
    itemModel.reference = snapshot.reference;
    String imgUrl = itemModel.imageDownloadUrls[0];
    itemModel.iconBytes = await getBytesFromNetwork(imgUrl: imgUrl, width: 48);
    // (await NetworkAssetBundle(Uri.parse(imgUrl)).load(imgUrl)).buffer.asUint8List();
    //todo: remove my own item
    // print(
    //     'myUserKey[${userKey}], itemUserKey[${itemModel.userKey}][${itemModel.geoFirePoint.latitude}][${itemModel.geoFirePoint.longitude}]');
    if (itemModel.userKey != userKey) {
      items.add(itemModel);
    }
  }

  return items;
}

// 이미지 사이즈 변경 가능, for asset ,
Future<Uint8List> getBytesFromAsset({required String path, required int width}) async {
  ByteData data = await rootBundle.load(path);
  ui.Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List(), targetWidth: width);
  ui.FrameInfo fi = await codec.getNextFrame();
  return (await fi.image.toByteData(format: ui.ImageByteFormat.png))!.buffer.asUint8List();
}

// 이미지 사이즈 변경 가능, for networkImage
Future<Uint8List> getBytesFromNetwork({required String imgUrl, required int width}) async {
  ByteData data = await NetworkAssetBundle(Uri.parse(imgUrl)).load(imgUrl);
  ui.Codec codec = await ui.instantiateImageCodec(
    data.buffer.asUint8List(),
    targetWidth: width,
    targetHeight: width,
  );
  ui.FrameInfo fi = await codec.getNextFrame();
  return (await fi.image.toByteData(format: ui.ImageByteFormat.png))!.buffer.asUint8List();
}

 

 

 

./src/screens/near/google_map_screen.dart - 다른 패키지로 만든 화면

 

import 'dart:async';

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

// LatLng 함수가 google_maps_flutter 와 충돌되어 별칭 추가. 변경 이유는 실사용 부분 참고,
import 'package:latlng/latlng.dart' as map_lat_lng;
import 'package:get/get.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';

import '../../constants/data_keys.dart';
import '../../models/item_model.dart';
import '../../repo/item_service.dart';
import '../../utils/logger.dart';

class GoogleMapScreen extends StatefulWidget {
  final UserModel1 _userModel;

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

  @override
  State<GoogleMapScreen> createState() => _GoogleMapScreenState();
}

class _GoogleMapScreenState extends State<GoogleMapScreen> {
  final Completer<GoogleMapController> _googleMapController = Completer();
  MapType _googleMapType = MapType.normal;
  int _mapType = 0;
  final Set<Marker> _markers = {};
  late final CameraPosition _initialCameraPosition;
  Marker? userMarker;

  @override
  void initState() {
    // 초기 맵 위치 설정
    _initialCameraPosition = CameraPosition(
      target: LatLng(
        widget._userModel.geoFirePoint.latitude,
        widget._userModel.geoFirePoint.longitude,
      ),
      zoom: 14,
    );

    userMarker = setMarker(
      latLng: LatLng(
        widget._userModel.geoFirePoint.latitude,
        widget._userModel.geoFirePoint.longitude,
      ),
      color: BitmapDescriptor.hueBlue,
    );
    _markers.add(userMarker!);
    super.initState();
  }

  @override
  void dispose() {
    // TODO: implement dispose
    super.dispose();
  }

  Marker setMarker({required LatLng latLng, required double color}) {
    return Marker(
      markerId: const MarkerId('myInitPosition'),
      position: latLng,
      infoWindow: const InfoWindow(title: 'My Position', snippet: 'Where am I?'),
      icon: BitmapDescriptor.defaultMarkerWithHue(color),
    );
  }

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

  void _onMapCreated(GoogleMapController controller) {
    // 이제 이 콘트롤을 프로그램애서 사용하기 위한 준비 완료.
    _googleMapController.complete(controller);
  }

  // 지도 타입 설정,
  void _changeMapType() {
    setState(() {
      _mapType++;
      _mapType = _mapType % 4;

      switch (_mapType) {
        case 0:
          _googleMapType = MapType.normal;
          break;
        case 1:
          _googleMapType = MapType.satellite;
          break;
        case 2:
          _googleMapType = MapType.terrain;
          break;
        case 3:
          _googleMapType = MapType.hybrid;
          break;
        default:
          _googleMapType = MapType.normal;
          break;
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    // 여기서 mapLatLng 별칭 처리한 이유는 getNearByItemsGoogle 함수에서 사용하는 LatLng 버전이 달라서임,
    // 해당 LatLng 타입은 google_maps_flutter.dart 것이 아니고 latlng.dart 것을 참고하기때문입니다,
    var myLatLng = map_lat_lng.LatLng(
      widget._userModel.geoFirePoint.latitude,
      widget._userModel.geoFirePoint.longitude,
    );

    // 화면 중심을 표시하려하는데, 자꾸 아랫쪽으로 치우친다.
    Size _size = MediaQuery.of(context).size;
    final middleOnScreen = Offset(_size.width / 2, _size.height / 2);
    // logger.d('middleOnScreen: $middleOnScreen');

    return Scaffold(
      body: FutureBuilder<List<ItemModel2>>(
        future: ItemService().getNearByItemsGoogle(widget._userModel.userKey, myLatLng),
        builder: (context, snapshot) {
          _markers.clear();
          // 로그인 사용자 위치를 화면에 추가합니다.
          _markers.add(userMarker!);
          // _markers.add(setMarker(latLng: _currentCenter, color: BitmapDescriptor.hueOrange));

          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, myLatLng));
              _markers.add(
                Marker(
                  // markerId: MarkerId(foundPlaces[i]['id']),
                  markerId: MarkerId(item.itemKey),
                  position: LatLng(
                    item.geoFirePoint.latitude,
                    item.geoFirePoint.longitude,
                  ),
                  infoWindow: InfoWindow(
                    title: item.title,
                    snippet: 'test',
                  ),
                  icon: BitmapDescriptor.fromBytes(item.iconBytes!),
                  //Icon for Marker
                  onTap: () {
                    logger.d('Marker clicked:[${item.title}]');
                    Get.toNamed(ROUTE_ITEM_DETAIL,
                        arguments: {'itemKey': item.itemKey},
                        // 같은 페이지는 호출시, 중복방지가 기본설정인, false 하면 중복 호출 가능,
                        preventDuplicates: false);
                  },
                ),
              );
            }
          }

          return Stack(
            children: <Widget>[
              GoogleMap(
                mapType: _googleMapType,
                initialCameraPosition: _initialCameraPosition,
                onMapCreated: _onMapCreated,
                myLocationEnabled: true,
                markers: _markers,
              ),
              Container(
                margin: const EdgeInsets.only(top: 60, right: 10),
                alignment: Alignment.topRight,
                child: Column(
                  children: <Widget>[
                    FloatingActionButton.extended(
                      heroTag: 'btn_googleMap',
                      label: Text('$_googleMapType'),
                      icon: const Icon(Icons.map),
                      elevation: 8,
                      backgroundColor: Colors.red[400],
                      onPressed: _changeMapType,
                    ),
                  ],
                ),
              ),
              _buildMarkerWidget(middleOnScreen),
            ],
          );
        },
      ),
    );
  }
}