https://github.com/cjk09083/ftweather
기존에 개발한 마커 추가방법은 현재 NaverMaps Camera에서 중앙 좌표를 가져와서 맵 중앙에 마커를 추가하는 방식이였다.
하지만 이 방법은 마커가 추가될 위치를 미리 확인하기 어려운 문제가 있다.
따라서 이번 포스팅에서는 맵을 클릭시 클릭한 곳을 특정한 이미지로 표시하고 addMarker 버튼 클릭시 표시된 좌표에 마커를 추가할 수 있도록 수정하였다.
1. onTab 이벤트에 마커 생성 함수 추가
먼저 선택한 좌표에 생성될 NMarker selAreaMarker를 정의하고 NaverMap의 onTap 이벤트에서 좌표를 수신받아 마커를 추가한다.
마커 생성시 기존에 생성되어있는 selAreaMarker가 있었다면 이를 제거하고 현재 선택한 좌표에 다시 생성한다.
수정된 파일들은 아래와 같다.
# MapModel..dart
class MapModel extends ChangeNotifier {
// 선택된 좌표 타입 0:마커 생성 불가능, 1:가능
int selectType = 0;
// 선택 좌표 표시 마커
NMarker selAreaMarker = NMarker(id: "select", position: const NLatLng(0,0));
// 마커 생성 및 목록에 추가하는 메서드
void _createMarker(String name) async {
// NaverMapController가 초기화되었는지 확인
if (_controller != null) {
// 현재 카메라 위치 가져오기
// NCameraPosition currentCameraPosition = await _controller!.getCameraPosition();
// NLatLng cameraLatLng = currentCameraPosition.target;
NLatLng newMarkerPos = selAreaMarker.position;
log("$TAG : New Marker Pos $newMarkerPos");
final now = DateTime.now();
final createdAt = now.toString();
String infoText = "$name\n"
"lat: ${newMarkerPos.latitude.toStringAsFixed(7)}\n"
"lon: ${newMarkerPos.longitude.toStringAsFixed(7)}\n"
"$createdAt";
// 현재 위치에 마커 추가
final marker = NMarker(
id: createdAt,
position: newMarkerPos,
caption: NOverlayCaption(text: name),
);
final onMarkerInfoWindow = NInfoWindow.onMarker(
id: marker.info.id,
text: infoText
);
// Position position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
// log("$TAG : Real Pos $position");
marker.setOnTapListener((NMarker marker) {
// selectMap(remake: false);
closeInfoAll();
marker.openInfoWindow(onMarkerInfoWindow);
});
// 마커 목록에 추가
_markers.add(marker);
_infoList.add(InfoData(marker.info.id, infoText));
_infoWindows.add(onMarkerInfoWindow);
_controller!.addOverlay(marker);
// 기본 맵 선택 마커 제거
_controller!.deleteOverlay(selAreaMarker.info);
selectType = 0;
_showTappedPos = false;
// 마커 리스트 -> json String으로 변환
await saverMarkers();
}
// 마커가 추가되었음을 알림
notifyListeners();
}
void selectMap({bool remake = false, NLatLng latLng = const NLatLng(0.0, 0.0)}) {
// 열린 InfoWindow 닫기
closeInfoAll();
// 기존 마커 제거
if(selAreaMarker.isAdded) {
_controller!.deleteOverlay(selAreaMarker.info);
selectType = 0;
}
// 새로운 좌표에 마커 재생성
if (remake) {
selAreaMarker = NMarker(id: DateTime.now().toString(), position: latLng);
_controller!.addOverlay(selAreaMarker);
selectType = 1;
}
notifyListeners();
}
# MapWidgetdart
NaverMap(
options: option,
onMapReady: (controller) {
model.setController(controller);
},
onMapTapped: (point, latLng) {
log("$TAG onMapTapped point: $point, latLng: $latLng");
model.selectMap(remake: true , latLng: latLng);
},
onSymbolTapped: (symbol){
log("$TAG onSymbolTapped symbol: ${symbol.caption}");
// model.selectMap(remake: true , latLng: symbol.position);
},
),
2. 마커 아이콘 변경 & 선택한 좌표 표시
사용자가 쉽게 알아볼 수 있도록 좌표선택마커는 기존 마커와 다른 아이콘을 사용하도록 하자.
아이콘에 적용할 마커 이미지는 https://www.flaticon.com/free-icon/focus_565826 에서 다운로드 받았다.
마커 이미지를 적용하기 위해
Flutter Project 내에 assets 폴더를 생성하고 그 안에 focus.png 파일을 옮겨둔다.
pubspec.yaml 파일을 아래와 같이 수정한다.
flutter:
uses-material-design: true
# To add assets to your application, add an assets section, like this:
assets:
- assets/
MapModel.dart의 selectMap 함수에서 마커 생성 부분을 아래와 같이 수정한다.
selAreaMarker = NMarker(id: DateTime.now().toString(), position: latLng,
anchor: const NPoint(0.5,0.5),
size: const Size(35,35),
iconTintColor: Colors.red,
icon: const NOverlayImage.fromAssetImage('assets/focus.png'),
);
또한 맵 클릭시 선택한 좌표를 유저가 바로 확인할 수 있도록 맵 우측 상단에 좌표를 나타내는 위젯을 추가하였다.
위젯은 아래와 같이 Positioned 위젯을 사용하고 model 내에 showTappedPos 변수를 만들어서 유효한 좌표일때만 표시하도록 하였다.
Widget build(BuildContext context) {
final model = Provider.of<MapModel>(context, listen: true);
log("showTappedPos: ${model.showTappedPos} ");
log("selAreaMarker: ${model.selAreaMarker.position} ");
return Expanded(
flex: 6,
child: Padding(
padding: const EdgeInsets.all(5.0),
child: Stack(
children: [
NaverMap(...),
if (model.showTappedPos)
Positioned(
top: 0,
right: 0,
child: Container(
padding: const EdgeInsets.all(8.0),
decoration: const BoxDecoration(
color: Colors.white,
),
child: Center(
child: Text(
"위도: ${model.selAreaMarker.position.latitude.toStringAsFixed(7)},"
" 경도: ${model.selAreaMarker.position.longitude.toStringAsFixed(7)}",
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
),
),
Positioned(...),
],
),
),
);
}
이때 변경된 좌표가 위젯에 적용될 수 있도록 MapWidget.dart의 build 에서 MapModel Provider를 생성할때 listen:true로 설정해준다.
수정 결과 아래와 같이 맵을 클릭할때마다 클릭한 좌표가 빨간색 마커로 표시된다.
3. 마커 추가 Dialog 분리
기존 마커 추가시 아래 그림(좌측)처럼 새로 생성될 마커의 이름만 입력후 추가하기 를 누르는 구조였다.
이때 추가될 좌표를 마지막으로 확인하고, 취소/추가 선택을 할 수 있도록 UI를 개선하였다.
또한 자동으로 이름 입력칸에 focus가 가서 키보드가 나타나도록 해서 편의성을 향상시켰다.
마지막으로 개선된 Dialog의 코드가 길어져서 이를 따로 파일로 분리하였다.
새로 생성&수정한 파일들은 아래와 같다.
# MarkderDialog.dart
분리해서 생성한 Dialog 파일이다.
import 'package:flutter/material.dart';
class MarkerDialog extends StatelessWidget {
final GlobalKey<FormState> formKey;
final double lat;
final double lon;
final TextEditingController nameController;
final Function? onConfirm;
const MarkerDialog({
Key? key,
required this.formKey,
required this.lat,
required this.lon,
required this.nameController,
required this.onConfirm,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final FocusNode focusNode = FocusNode();
focusNode.requestFocus(); // 자동 포커스 요청
return AlertDialog(
title: const Text("Marker 추가"),
content: Form(
key: formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text("위도: ${lat.toStringAsFixed(7)}"),
Text("경도: ${lon.toStringAsFixed(7)}"),
TextFormField(
controller: nameController,
decoration: const InputDecoration(
labelText: "이름",
),
focusNode: focusNode,
validator: (value) {
if (value == null || value.isEmpty) {
return "이름을 입력해주세요.";
}
return null;
},
),
],
),
),
actions: [
Row(
children: [
Expanded(
flex: 1,
child: Padding(
padding: const EdgeInsets.all(3.0),
child: ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: const Text('취소'),
),
),
),
Expanded(
flex: 1,
child: Padding(
padding: const EdgeInsets.all(3.0),
child: ElevatedButton(
onPressed: () {
if (formKey.currentState!.validate()) {
Navigator.of(context).pop();
onConfirm!();
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blueAccent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: const Text('추가하기'),
),
),
),
],
),
],
);
}
}
# MapModel.dart
addMarker 함수에서 선택한 좌표가 유효한지 검사하고, 유효하다면 MarkerDialog를 호출하도록 수정하였다.
// 현재 위치에 마커를 추가하는 메서드
Future<void> addMarker(BuildContext context) async {
// 로그 출력
log("$TAG : addMarker");
final TextEditingController nameController = TextEditingController();
final formKey = GlobalKey<FormState>();
if(!selAreaMarker.isVisible || selectType == 0){
log("유효하지 않은 좌표");
return;
}
showDialog(
context: context,
builder: (BuildContext context) {
return MarkerDialog(
formKey: formKey,
lat: selAreaMarker.position.latitude,
lon: selAreaMarker.position.longitude,
nameController: nameController,
onConfirm: () {_createMarker(nameController.text);},
);
},
);
}
수정 결과 아래와 같이 UI가 개선되었다. 기존 UI [좌측] -> 개선된 UI [우측]
4. flutterToast 알림 추가
addMarker 함수에서 선택한 좌표가 유효하지 않을때 toast 메세지로 이를 알리도록 추가하였다.
flutterToast 메세지를 사용하기 위해서는 먼저 pubspec.yaml에 아래와 같이 플러그인을 추가해주고 Pub get 해준다.
dependencies:
flutter:
sdk: flutter
fluttertoast: ^8.0.8
이후 프로젝트내에서 쉽게 사용할 수 있도록 main.dart에 전역 함수를 추가한다.
void fToast(String msg ,{ double size = 16.0, Color color = Colors.red}){
Fluttertoast.showToast(
msg: msg,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.CENTER,
backgroundColor: Colors.blueAccent,
textColor: color,
fontSize: size
);
}
생성한 함수를 이용해 Toast 메세지를 생성 및 출력한다.
# MapModel.dart
Future<void> addMarker(BuildContext context) async {
// 로그 출력
log("$TAG : addMarker");
final TextEditingController nameController = TextEditingController();
final formKey = GlobalKey<FormState>();
if(!selAreaMarker.isVisible || selectType == 0){
fToast("맵에서 위치를 선택해주세요."); // Toast 메세지 함수 호출
return;
}
showDialog(
context: context,
builder: (BuildContext context) {
return MarkerDialog(
formKey: formKey,
lat: selAreaMarker.position.latitude,
lon: selAreaMarker.position.longitude,
nameController: nameController,
onConfirm: () {_createMarker(nameController.text);},
);
},
);
}
수정 후 유효하지 않은 좌표를 선택한 상태로 마커 추가시 아래와 같은 메세지가 출력된다.
+ 230428 맵 상단의 좌표 위젯을 Positioned에서 AnimatedPositioned로 변경 + border추가
Widget build(BuildContext context) {
final model = Provider.of<MapModel>(context, listen: true);
log("showTappedPos: ${model.showTappedPos} ");
log("selAreaMarker: ${model.selAreaMarker.position} ");
return Expanded(
flex: 6,
child: Padding(
padding: const EdgeInsets.all(5.0),
child: Stack(
children: [
NaverMap(...),
AnimatedPositioned(
top: model.showTappedPos ? 0 : -50,
right: 0,
duration: const Duration(milliseconds: 300),
child: Container(
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(
color: Colors.black,
width: 1.0,
),
borderRadius: BorderRadius.circular(10),
),
child: Center(
child: Text(
"위도: ${model.selAreaMarker.position.latitude.toStringAsFixed(7)},"
" 경도: ${model.selAreaMarker.position.longitude.toStringAsFixed(7)}",
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
),
),
Positioned(...),
],
),
),
);
}
'프로젝트들 > 날씨앱' 카테고리의 다른 글
[날씨앱] P11 Flutter 위젯에 Slide Action 추가 (0) | 2023.04.28 |
---|---|
[날씨앱] P10 Flutter ListView 드래그로 순서 변경 (0) | 2023.04.24 |
[날씨앱] P8 CameraMove 추가 & NaverMaps 플러그인 변경 (0) | 2023.04.19 |
[날씨앱] P7. Flutter Shared Preferences 추가 (0) | 2023.04.14 |
[날씨앱] P6. Flutter ListView 추가 (0) | 2023.04.14 |