diff --git a/CHANGELOG.md b/CHANGELOG.md index 4768623b0..35ede7019 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.0.0-dev.10 +- fix bugs ([#69](https://github.com/note11g/flutter_naver_map/issues/69)) +- internal refactoring (code/struct improvement) +- add [docs](https://note11.dev/flutter_naver_map/) + ## 1.0.0-dev.9 - fix bugs ([#51](https://github.com/note11g/flutter_naver_map/issues/51), [#53](https://github.com/note11g/flutter_naver_map/issues/53), [#54](https://github.com/note11g/flutter_naver_map/issues/54), [#55](https://github.com/note11g/flutter_naver_map/issues/55), [#60](https://github.com/note11g/flutter_naver_map/issues/60)) - add `NaverMapViewOptions.copyWith` diff --git a/README.md b/README.md index 468730486..90a9cb35f 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,16 @@ # flutter_naver_map 1.0 Dev Preview -NaverMap SDK for Flutter (1.0-dev.9) +NaverMap SDK for Flutter (1.0-dev.10) 플러터 3.0 이상, dart 2.18.4 이상을 사용하셔야 합니다. android는 5.1 이상, iOS는 11.0 이상을 지원합니다. +**[개발 문서 바로가기](https://note11.dev/flutter_naver_map)** + +(작업 중이므로 불완전한 문서들이 있을 수 있습니다.) ### 필독 (iOS) -1.0.0-dev.3 버전을 사용하셨거나, 이전버전(0.10)을 사용하셨던 분들은, +1.0.0-dev.3 이하 버전을 사용하셨거나, 이전버전(0.10)을 사용하셨던 분들은, iOS에서 네이버맵 구버전으로 빌드될 수 있습니다. 현재 라이브러리에서 사용하는 버전은 3.16.2로, 다음과 같은 명령어 실행이 필요합니다. @@ -25,7 +28,7 @@ Naver Cloud Platform 에서 앱을 등록하고, Android / iOS 플랫폼을 등 ```yaml dependencies: - flutter_naver_map: ^1.0.0-dev.9 + flutter_naver_map: ^1.0.0-dev.10 ``` ### Android @@ -116,4 +119,4 @@ Widget build(BuildContext context) { - [ ] API DOCS 작성 (작성중) - [ ] 예제 작성 - [ ] 테스트 작성 -- [ ] 클러스터링 구현 (추가 패키지로 제공될 예정입니다) +- [ ] 클러스터링 구현 (추가 패키지로 제공될 예정입니다) \ No newline at end of file diff --git a/android/src/main/kotlin/dev/note11/flutter_naver_map/flutter_naver_map/controller/overlay/OverlayController.kt b/android/src/main/kotlin/dev/note11/flutter_naver_map/flutter_naver_map/controller/overlay/OverlayController.kt index 81c9bf2bf..a75850179 100644 --- a/android/src/main/kotlin/dev/note11/flutter_naver_map/flutter_naver_map/controller/overlay/OverlayController.kt +++ b/android/src/main/kotlin/dev/note11/flutter_naver_map/flutter_naver_map/controller/overlay/OverlayController.kt @@ -1,7 +1,6 @@ package dev.note11.flutter_naver_map.flutter_naver_map.controller.overlay import android.content.Context -import android.util.Log import com.naver.maps.geometry.LatLng import com.naver.maps.map.overlay.* import dev.note11.flutter_naver_map.flutter_naver_map.controller.overlay.handler.* @@ -33,7 +32,7 @@ import io.flutter.plugin.common.MethodChannel internal class OverlayController( private val channel: MethodChannel, private val context: Context, -) : LocationOverlayHandler, MarkerHandler, InfoWindowHandler, CircleOverlayHandler, +) : OverlaySender, LocationOverlayHandler, MarkerHandler, InfoWindowHandler, CircleOverlayHandler, GroundOverlayHandler, PolygonOverlayHandler, PolylineOverlayHandler, PathOverlayHandler, MultipartPathOverlayHandler, ArrowheadPathOverlayHandler { /* ----- channel ----- */ @@ -41,6 +40,12 @@ internal class OverlayController( channel.setMethodCallHandler(::handler) } + /* ----- sender ----- */ + override fun onOverlayTapped(info: NOverlayInfo) { + val query = NOverlayQuery(info, methodName = OverlayHandler.onTapName).query + channel.invokeMethod(query, null) + } + /* ----- overlay storage ----- */ private val overlays: MutableMap = mutableMapOf() @@ -48,9 +53,8 @@ internal class OverlayController( override fun saveOverlay(overlay: Overlay, info: NOverlayInfo) { info.saveAtOverlay(overlay) detachOverlay(info) - val query = NOverlayQuery(info, methodName = OverlayHandler.onTapName).query overlay.setOnClickListener { - channel.invokeMethod(query, null) + onOverlayTapped(info) return@setOnClickListener true } overlays[info] = overlay diff --git a/android/src/main/kotlin/dev/note11/flutter_naver_map/flutter_naver_map/controller/overlay/OverlaySender.kt b/android/src/main/kotlin/dev/note11/flutter_naver_map/flutter_naver_map/controller/overlay/OverlaySender.kt new file mode 100644 index 000000000..402e9388c --- /dev/null +++ b/android/src/main/kotlin/dev/note11/flutter_naver_map/flutter_naver_map/controller/overlay/OverlaySender.kt @@ -0,0 +1,7 @@ +package dev.note11.flutter_naver_map.flutter_naver_map.controller.overlay + +import dev.note11.flutter_naver_map.flutter_naver_map.model.map.info.NOverlayInfo + +internal interface OverlaySender { + fun onOverlayTapped(info: NOverlayInfo) +} \ No newline at end of file diff --git a/android/src/main/kotlin/dev/note11/flutter_naver_map/flutter_naver_map/view/NaverMapView.kt b/android/src/main/kotlin/dev/note11/flutter_naver_map/flutter_naver_map/view/NaverMapView.kt index 8dd921296..e96c72ad3 100644 --- a/android/src/main/kotlin/dev/note11/flutter_naver_map/flutter_naver_map/view/NaverMapView.kt +++ b/android/src/main/kotlin/dev/note11/flutter_naver_map/flutter_naver_map/view/NaverMapView.kt @@ -148,11 +148,7 @@ internal class NaverMapView( mapView.onPause() } - override fun onActivityStopped(activity: Activity) { - if (activity != this.activity) return - - mapView.onStop() - } + override fun onActivityStopped(activity: Activity) = Unit override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { if (activity != this.activity) return diff --git a/example/ios/Podfile b/example/ios/Podfile index 313ea4a15..6cb1ed918 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -37,5 +37,13 @@ end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) + + target.build_configurations.each do |config| + config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ + '$(inherited)', + ## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse] + 'PERMISSION_LOCATION=1', + ] + end end end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 144a67b5f..867f280a2 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -3,8 +3,6 @@ PODS: - flutter_naver_map (1.0.0): - Flutter - NMapsMap (= 3.16.2) - - geolocator_apple (1.2.0): - - Flutter - integration_test (0.0.1): - Flutter - NMapsGeometry (1.0.1) @@ -13,13 +11,15 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - permission_handler_apple (9.0.4): + - Flutter DEPENDENCIES: - Flutter (from `Flutter`) - flutter_naver_map (from `.symlinks/plugins/flutter_naver_map/ios`) - - geolocator_apple (from `.symlinks/plugins/geolocator_apple/ios`) - integration_test (from `.symlinks/plugins/integration_test/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`) + - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) SPEC REPOS: trunk: @@ -31,22 +31,22 @@ EXTERNAL SOURCES: :path: Flutter flutter_naver_map: :path: ".symlinks/plugins/flutter_naver_map/ios" - geolocator_apple: - :path: ".symlinks/plugins/geolocator_apple/ios" integration_test: :path: ".symlinks/plugins/integration_test/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/ios" + permission_handler_apple: + :path: ".symlinks/plugins/permission_handler_apple/ios" SPEC CHECKSUMS: Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 flutter_naver_map: d1ee15e7a2fe5360a19640648ceed5abf79d82aa - geolocator_apple: cc556e6844d508c95df1e87e3ea6fa4e58c50401 integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5 NMapsGeometry: 53c573ead66466681cf123f99f698dc8071a4b83 NMapsMap: aaa64717249b06ae82c3a3addb3a01f0e33100ab path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852 + permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce -PODFILE CHECKSUM: 7368163408c647b7eb699d0d788ba6718e18fb8d +PODFILE CHECKSUM: 45d27258f3a62ba7066db214d3af977e6c8dd7b4 COCOAPODS: 1.11.3 diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index a67d5be69..eb766e25f 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -26,12 +26,10 @@ $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS - NSLocationAlwaysAndWhenInUseUsageDescription - need location permission to show user's currnet location in map - NSLocationAlwaysUsageDescription - need location permission to show user's currnet location in map - NSLocationWhenInUseUsageDescription - need location permission to show user's currnet location in map + NSLocationUsageDescription + need location permission to show user's currnet location in map + NSLocationWhenInUseUsageDescription + need location permission to show user's currnet location in map UIApplicationSupportsIndirectInputEvents UILaunchStoryboardName @@ -41,15 +39,13 @@ UISupportedInterfaceOrientations UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown UIViewControllerBasedStatusBarAppearance diff --git a/example/lib/design/custom_widget.dart b/example/lib/design/custom_widget.dart new file mode 100644 index 000000000..5b6bf2611 --- /dev/null +++ b/example/lib/design/custom_widget.dart @@ -0,0 +1,530 @@ +import 'package:flutter/material.dart'; +import 'package:pull_to_refresh_flutter3/pull_to_refresh_flutter3.dart'; + +import 'theme.dart'; + +class SelectorWithTitle extends StatelessWidget { + final String title; + final String description; + final EasySelectorWidget Function(BuildContext context) selector; + + const SelectorWithTitle(this.title, + {Key? key, required this.description, required this.selector}) + : super(key: key); + + @override + Widget build(BuildContext context) { + final selector = this.selector(context); + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 24), + child: Row(children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: getTextTheme(context).titleMedium), + const SizedBox(height: 2), + Text(description, style: getTextTheme(context).bodySmall), + ])), + Expanded(flex: selector.expand ? 2 : 0, child: selector), + ])); + } +} + +mixin EasySelectorWidget on Widget { + bool get expand => true; +} + +class EasyDropdown extends StatelessWidget + with EasySelectorWidget { + final List items; + final T value; + final void Function(T newValue) onChanged; + + const EasyDropdown({ + Key? key, + required this.items, + required this.value, + required this.onChanged, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: getColorTheme(context).outline, + borderRadius: BorderRadius.circular(8)), + padding: const EdgeInsets.only(right: 8), + child: DropdownButton( + dropdownColor: getColorTheme(context).background, + underline: Container(), + isExpanded: true, + borderRadius: BorderRadius.circular(8), + elevation: 4, + selectedItemBuilder: (context) => items + .map((e) => Container( + width: double.infinity, + alignment: Alignment.centerLeft, + margin: const EdgeInsets.symmetric(horizontal: 16), + child: Text(e.name, + style: getTextTheme(context).bodyMedium, + maxLines: 1, + overflow: TextOverflow.ellipsis))) + .toList(), + items: items + .map((e) => DropdownMenuItem( + value: e, + child: Text(e.name, + style: getTextTheme(context).bodyMedium, + maxLines: 1, + overflow: TextOverflow.ellipsis))) + .toList(), + value: value, + onChanged: (v) => onChanged(v as T)), + ); + } +} + +class EasySlider extends StatelessWidget with EasySelectorWidget { + final double min; + final double max; + final double value; + final double? defaultValue; + final int? divisions; + final int floatingPoint; + + final double width; + final void Function(double newValue) onChanged; + + bool get showAsInt => floatingPoint == 0; + + const EasySlider({ + Key? key, + this.min = 0, + this.max = 80, + this.divisions = 10, + required this.value, + this.defaultValue, + this.floatingPoint = 0, + this.width = 160, + required this.onChanged, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: width, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onDoubleTapDown: + defaultValue != null ? (_) => onChanged(defaultValue!) : null, + child: + Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: SliderTheme( + data: SliderThemeData( + trackHeight: 8, + thumbColor: Theme.of(context).colorScheme.primary, + activeTrackColor: + Theme.of(context).colorScheme.primaryContainer, + inactiveTrackColor: Theme.of(context).colorScheme.outline, + activeTickMarkColor: Colors.white, + thumbShape: + const RoundSliderThumbShape(enabledThumbRadius: 10), + overlayShape: SliderComponentShape.noThumb, + showValueIndicator: ShowValueIndicator.always), + child: Slider( + min: min, + max: max, + value: value, + divisions: divisions, + // 8배수 단위로 맞추고 싶음. + label: + "${showAsInt ? value.round() : value.toStringAsFixed(floatingPoint)}", + onChanged: onChanged)), + ), + Stack( + children: [ + Center( + child: Text( + showAsInt + ? "${value.toInt()}" + : value.toStringAsFixed(floatingPoint), + style: getTextTheme(context).bodySmall?.copyWith( + fontWeight: FontWeight.w700, + color: getColorTheme(context).primary)), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Text( + showAsInt ? "${min.toInt()}" : min.toStringAsFixed(1), + style: getTextTheme(context).bodySmall), + Text( + showAsInt ? "${max.toInt()}" : max.toStringAsFixed(1), + style: getTextTheme(context).bodySmall), + ]), + ], + ), + ]), + )); + } + + @override + bool get expand => false; +} + +class TextSwitcher extends StatelessWidget { + final String title; + final String description; + final bool value; + final Function(bool newValue) onChanged; + final bool enable; + + const TextSwitcher({ + Key? key, + required this.title, + required this.description, + required this.value, + required this.onChanged, + this.enable = true, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Material( + color: value + ? getColorTheme(context).primaryContainer + : getColorTheme(context).outline, + borderRadius: BorderRadius.circular(12), + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: enable ? () => onChanged(!value) : null, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(title, + style: enable + ? getTextTheme(context).titleMedium + : getTextTheme(context) + .titleMedium + ?.copyWith(color: getColorTheme(context).secondary), + overflow: TextOverflow.fade, + softWrap: false, + maxLines: 1), + const SizedBox(height: 2), + Text(description, + style: getTextTheme(context).bodySmall, + overflow: TextOverflow.fade, + softWrap: false, + maxLines: 1), + ], + ))); + } +} + +class SimpleButton extends StatelessWidget { + final String text; + final Color? color; + final EdgeInsets margin; + final void Function() action; + + const SimpleButton( + {Key? key, + required this.text, + this.margin = const EdgeInsets.symmetric(horizontal: 24, vertical: 8), + this.color, + required this.action}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: margin, + child: Material( + color: color ?? getColorTheme(context).primary, + borderRadius: BorderRadius.circular(12), + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: action, + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + child: Text(text, + style: getTextTheme(context).labelLarge, + overflow: TextOverflow.fade, + softWrap: false, + textAlign: TextAlign.center, + maxLines: 1)))), + ); + } +} + +class SimpleTitle extends StatelessWidget { + final String title; + final String? description; + final EdgeInsets padding; + final Axis direction; + + const SimpleTitle( + this.title, { + Key? key, + this.description, + this.padding = const EdgeInsets.only(left: 24, top: 12), + this.direction = Axis.horizontal, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: padding, + child: direction == Axis.horizontal + ? Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Text(title, style: getTextTheme(context).titleMedium), + if (description != null) + Padding( + padding: const EdgeInsets.only(left: 4), + child: Text(description!, + style: getTextTheme(context).bodySmall)), + ]) + : Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(title, style: getTextTheme(context).titleMedium), + if (description != null) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Text(description!, + style: getTextTheme(context).bodySmall)), + ])); + } +} + +class SliverTitle extends StatelessWidget { + final String title; + final String? description; + final EdgeInsets padding; + final Axis direction; + + const SliverTitle( + this.title, { + Key? key, + this.description, + this.padding = const EdgeInsets.only(left: 24, top: 12), + this.direction = Axis.horizontal, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SliverPadding( + padding: padding, + sliver: direction == Axis.horizontal + ? SliverRow([ + Text(title, style: getTextTheme(context).titleMedium), + if (description != null) + Padding( + padding: const EdgeInsets.only(left: 4), + child: Text(description!, + style: getTextTheme(context).bodySmall)), + ], + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic) + : SliverColumn([ + Text(title, style: getTextTheme(context).titleMedium), + if (description != null) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Text(description!, + style: getTextTheme(context).bodySmall)), + ], crossAxisAlignment: CrossAxisAlignment.start)); + } +} + +class SliverColumn extends SliverToBoxAdapter { + SliverColumn( + List children, { + super.key, + MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start, + CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center, + }) : super( + child: Column( + mainAxisAlignment: mainAxisAlignment, + crossAxisAlignment: crossAxisAlignment, + children: children)); +} + +class SliverRow extends SliverToBoxAdapter { + SliverRow( + List children, { + super.key, + MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start, + CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center, + TextBaseline? textBaseline, + }) : super( + child: Row( + mainAxisAlignment: mainAxisAlignment, + crossAxisAlignment: crossAxisAlignment, + textBaseline: textBaseline, + children: children)); +} + +class ReLoader extends StatelessWidget { + final void Function() reload; + final Duration reloadTime; + final String text; + final String actionText; + final Widget child; + + ReLoader({ + Key? key, + required this.reload, + required this.text, + this.actionText = "초기화", + this.reloadTime = const Duration(milliseconds: 500), + required this.child, + }) : super(key: key); + + final refreshController = RefreshController(); + + @override + Widget build(BuildContext context) { + return SmartRefresher( + controller: refreshController, + onRefresh: () { + reload(); + refreshController.refreshCompleted(); + }, + header: ClassicHeader( + completeDuration: reloadTime, + completeText: "$actionText가 완료되었습니다", + releaseText: "$text $actionText", + idleText: "$text $actionText하려면 계속해서 당겨주세요", + ), + child: child); + } +} + +class BottomPadding extends StatelessWidget { + const BottomPadding({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: + EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom)); + } +} + +class SliverBottomPadding extends StatelessWidget { + const SliverBottomPadding({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return SliverPadding( + padding: + EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom)); + } +} + +class Balloon extends StatelessWidget { + final Size? size; + final EdgeInsets padding; + final Widget child; + + const Balloon({ + Key? key, + this.size, + required this.padding, + required this.child, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Material( + type: MaterialType.transparency, + child: SizedBox.fromSize( + size: size, + child: CustomPaint( + // size: size ?? Size.zero, + painter: BalloonPainter(backgroundColor: Colors.pink), + child: Container( + // color: Colors.green, + alignment: Alignment.bottomCenter, + padding: padding, + child: child, + ), + ), + ), + ); + } +} + +class BalloonPainter extends CustomPainter { + BalloonPainter({ + required this.backgroundColor, + }); + + final Color backgroundColor; + + static const radius = 16.0; + static const tailRadius = 4.0; + static const tailHeight = 12.0; + static const tailWidth = 12.0 + tailRadius; + + @override + void paint(Canvas canvas, Size size) { + const corner = Radius.circular(radius); + const tailCorner = Radius.circular(tailRadius); + const tailPosition = 28.0; // 현재는 가운데 (1/2) + final targetWidth = size.width; + final targetHeight = size.height + tailHeight; + const startPoint = Offset(radius, tailHeight); + const firstTailPoint = Offset(tailPosition - (tailWidth / 2), tailHeight); + const secondTailPoint = Offset(tailPosition, 0); + final thirdTailPoint = Offset(secondTailPoint.dx + 2, secondTailPoint.dy); + const fourthTailPoint = Offset(tailPosition + (tailWidth / 2), tailHeight); + final firstRoundEndPoint = + Offset(size.width, radius + tailHeight); // 오른쪽 위 라운드 끝 지점. + final secondRoundEndPoint = Offset(size.width - radius, targetHeight); + final thirdRoundEndPoint = Offset(0, targetHeight - radius); + + final path = Path() + + /// 왼쪽 선 그리기 시작 + ..moveTo(startPoint.dx, startPoint.dy) // 첫번째 포인트로 이동 (왼쪽 위, 라운드 직후) + ..lineTo(firstTailPoint.dx, firstTailPoint.dy) // 첫번째 꼬리 시작점까지 그리기 + ..lineTo(secondTailPoint.dx, secondTailPoint.dy) // 꼬리 윗점까지 그리기 + ..arcToPoint(thirdTailPoint, radius: tailCorner) //꼬리를 둥글게 + ..lineTo(fourthTailPoint.dx, fourthTailPoint.dy) // 꼬리 끝점까지 그리기 + ..lineTo(targetWidth - radius, tailHeight) // 오른쪽 위 꼭지점까지 그리기 (라운드 직전) + /// 오른쪽 선 그리기 시작 + ..arcToPoint(firstRoundEndPoint, radius: corner) // 코너 그리기 + ..lineTo(size.width, targetHeight - radius) // 오른쪽 아래 꼭지점까지 그리기 (라운드 직전) + /// 아래쪽 선 그리기 시작 + ..arcToPoint(secondRoundEndPoint, radius: corner) // 코너 그리기 + ..lineTo(radius, targetHeight) // 왼쪽 아래 꼭지점까지 그리기 (라운드 직전) + /// 왼쪽 선 그리기 시작 + ..arcToPoint(thirdRoundEndPoint, radius: corner) // 코너 그리기 + ..lineTo(0, radius + tailHeight) // 왼쪽 위 꼭지점까지 그리기 (라운드 직전) + /// 선 닫기 + ..arcToPoint(startPoint, radius: corner) + ..close(); + + canvas.drawPath( + path, + Paint() + ..color = backgroundColor + ..style = PaintingStyle.fill); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return true; + } +} diff --git a/example/lib/design/map_function_item.dart b/example/lib/design/map_function_item.dart new file mode 100644 index 000000000..499b5cb65 --- /dev/null +++ b/example/lib/design/map_function_item.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; + +import 'theme.dart'; + +typedef MapFunctionItemOnTap = void Function(MapFunctionItem item); +typedef MapFunctionItemOnBack = void Function(); + +class MapFunctionItem { + final String title; + final String? description; + final MapFunctionItemOnTap? onTap; + final Widget Function(bool canScroll)? page; + final MapFunctionItemOnBack? onBack; + + final bool? isScrollPage; + + MapFunctionItem({ + required this.title, + this.description, + this.page, + required this.onTap, + this.onBack, + this.isScrollPage, + }); + + bool get needItemOnTap => onTap == null; + + Widget getItemWidget(BuildContext context) { + return InkWell( + onTap: onTap != null ? () => onTap!.call(this) : null, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 24, vertical: description != null ? 12 : 20), + child: + Row(crossAxisAlignment: CrossAxisAlignment.center, children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: Theme.of(context).textTheme.titleSmall), + if (description != null) + Padding( + padding: const EdgeInsets.only(top: 4), + child: SizedBox( + width: double.infinity, + child: Text(description!, + style: getTextTheme(context) + .bodyMedium + ?.copyWith( + color: + getColorTheme(context).secondary), + overflow: TextOverflow.ellipsis, + maxLines: 1))) + ])), + const Icon(Icons.arrow_forward_ios_rounded, size: 20), + ]))); + } + + Widget getPageWidget(BuildContext context, {bool canScroll = true}) { + assert(page != null); + return Column(children: [ + Row(crossAxisAlignment: CrossAxisAlignment.center, children: [ + IconButton( + onPressed: onBack, + padding: const EdgeInsets.all(16), + icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 20)), + Text(title, style: getTextTheme(context).titleLarge), + ]), + page!(canScroll) + ]); + } + + MapFunctionItem copyWith({ + String? title, + String? description, + MapFunctionItemOnTap? onTap, + Widget Function(bool canScroll)? page, + MapFunctionItemOnBack? onBack, + bool? isScrollPage, + }) { + return MapFunctionItem( + title: title ?? this.title, + description: description ?? this.description, + onTap: onTap ?? this.onTap, + page: page ?? this.page, + onBack: onBack ?? this.onBack, + isScrollPage: isScrollPage ?? this.isScrollPage, + ); + } +} diff --git a/example/lib/design/theme.dart b/example/lib/design/theme.dart new file mode 100644 index 000000000..060efb713 --- /dev/null +++ b/example/lib/design/theme.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; + +class ExampleAppTheme { + static final lightThemeData = ThemeData( + primarySwatch: Colors.green, + colorScheme: ColorScheme.light( + primary: Colors.green, + secondary: Colors.grey, + background: Colors.white, + onBackground: Colors.black, + outline: Colors.grey.shade200, + primaryContainer: const Color(0xFFD2FFB4), + ), + textTheme: const TextTheme( + titleSmall: TextStyle( + fontSize: 16, fontWeight: FontWeight.w500, letterSpacing: 0), + titleMedium: TextStyle( + fontSize: 18, fontWeight: FontWeight.w700, letterSpacing: 0), + titleLarge: TextStyle( + fontSize: 20, fontWeight: FontWeight.w700, letterSpacing: 0), + bodyMedium: TextStyle(fontSize: 14, letterSpacing: 0), + labelSmall: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Colors.white, + letterSpacing: 0), + labelLarge: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: Colors.white, + letterSpacing: 0), + )); + + static final darkThemeData = ThemeData( + brightness: Brightness.dark, + colorScheme: ColorScheme.dark( + primary: Colors.green, + secondary: Colors.grey.shade600, + background: Colors.grey.shade900, + onBackground: Colors.white, + outline: Colors.grey.shade700, + primaryContainer: const Color(0xFF7FA864), + ), + textTheme: const TextTheme( + titleSmall: TextStyle( + fontSize: 16, fontWeight: FontWeight.w500, letterSpacing: 0), + titleMedium: TextStyle( + fontSize: 18, fontWeight: FontWeight.w700, letterSpacing: 0), + titleLarge: TextStyle( + fontSize: 20, fontWeight: FontWeight.w700, letterSpacing: 0), + bodyMedium: TextStyle(fontSize: 14, letterSpacing: 0), + labelSmall: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Colors.white, + letterSpacing: 0), + labelLarge: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: Colors.white, + letterSpacing: 0), + )); + + ExampleAppTheme._(); +} + +ColorScheme getColorTheme(BuildContext context) => + Theme.of(context).colorScheme; + +TextTheme getTextTheme(BuildContext context) => Theme.of(context).textTheme; diff --git a/example/lib/design/ui_widgets.dart b/example/lib/design/ui_widgets.dart new file mode 100644 index 000000000..e69de29bb diff --git a/example/lib/main.dart b/example/lib/main.dart index 48dccd295..bd824cf10 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,16 +1,19 @@ -import 'dart:developer'; - import 'package:flutter/material.dart'; import 'package:flutter_naver_map/flutter_naver_map.dart'; +import 'package:flutter_naver_map_example/design/custom_widget.dart'; +import 'package:flutter_naver_map_example/pages/examples/overlay_example.dart'; + +import 'pages/bottom_drawer.dart'; +import 'pages/new_window_page.dart'; +import 'design/map_function_item.dart'; +import 'design/theme.dart'; + +import 'pages/examples/options_example.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - await NaverMapSdk.instance.initialize( - clientId: 'client id를 여기에 입력해주세요.', - onAuthFailed: (ex) { - log("Client ID를 세팅해주세요.", name: "NaverMap", error: ex); - }); + await NaverMapSdk.instance.initialize(clientId: '2vkiu8dsqb'); runApp(const MyApp()); } @@ -20,7 +23,11 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return const MaterialApp(home: FNMapPage()); + return MaterialApp( + debugShowCheckedModeBanner: false, + home: const FNMapPage(), + theme: ExampleAppTheme.lightThemeData, + darkTheme: ExampleAppTheme.darkThemeData); } } @@ -32,8 +39,144 @@ class FNMapPage extends StatefulWidget { } class _FNMapPageState extends State { + /* ----- UI Size ----- */ + late EdgeInsets safeArea; + double drawerHeight = 0; + @override Widget build(BuildContext context) { - return const NaverMap(); + safeArea = MediaQuery.of(context).padding; + return WillPopScope( + onWillPop: () async => drawerTool.processWillPop(), + child: Stack(children: [ + Positioned.fill(child: mapWidget()), + drawerTool.bottomDrawer, + ]), + ); + } + + /* + --- Naver Map Widget --- + */ + + late NaverMapController mapController; + NaverMapViewOptions options = const NaverMapViewOptions(); + + Widget mapWidget() { + final mapPadding = EdgeInsets.only(bottom: drawerHeight - safeArea.bottom); + return NaverMap( + options: options.copyWith(contentPadding: mapPadding), + onMapReady: onMapReady, + onMapTapped: onMapTapped, + onSymbolTapped: onSymbolTapped, + onCameraChange: onCameraChange, + onCameraIdle: onCameraIdle, + onSelectedIndoorChanged: onSelectedIndoorChanged, + ); + } + + /* ----- Events ----- */ + + void onMapReady(NaverMapController controller) { + mapController = controller; + } + + void onMapTapped(NPoint point, NLatLng latLng) { + // ... + } + + void onSymbolTapped(NSymbolInfo symbolInfo) { + // ... + } + + void onCameraChange(NCameraUpdateReason reason, bool isGesture) { + // ... + } + + void onCameraIdle() { + // ... + } + + void onSelectedIndoorChanged(NSelectedIndoor? selectedIndoor) { + // ... + } + + /* + --- Bottom Drawer Widget --- + */ + + late final drawerTool = ExampleAppBottomDrawer( + context: context, + onDrawerHeightChanged: (height) => setState(() => drawerHeight = height), + rebuild: () => setState(() {}), + // onPageDispose: () {}, + pages: pages); + + late final List pages = [ + ExampleAppBottomDrawer.makeDefault( + title: "NaverMapViewOptions 변경", + description: "지도의 옵션을 변경할 수 있어요", + page: (canScroll) => NaverMapViewOptionsExample( + canScroll: canScroll, + options: options, + onOptionsChanged: (changed) { + if (changed != options) setState(() => options = changed); + })), + ExampleAppBottomDrawer.makeDefault( + title: "오버레이 추가 / 제거", + description: "마커, 경로 등의 각종 오버레이들을 추가하고 제거할 수 있어요", + isScrollPage: false, + page: (canScroll) => NOverlayExample( + isClosed: !canScroll, mapController: mapController)), + ExampleAppBottomDrawer.makeDefault( + title: "카메라 이동", + description: "지도에 보이는 영역을 카메라를 이동하여 바꿀 수 있어요", + page: (canScroll) => _cameraMoveTestPage()), + ExampleAppBottomDrawer.makeDefault( + title: "기타 컨트롤러 기능", + description: "컨트롤러로 여러가지 기능을 조작합니다.", + page: (canScroll) => _controllerTestPage()), + ExampleAppBottomDrawer.makeDefault( + title: "주변 심볼 및 오버레이 가져오기", + description: "특정 영역 주변의 심볼 및 오버레이를 가져올 수 있어요", + page: (canScroll) => _pickTestPage()), + MapFunctionItem( + title: "새 페이지에서 지도 보기", + description: "새 페이지에서 지도를 봅니다. (메모리 누수 확인용)", + onTap: (_) => Navigator.of(context).push( + MaterialPageRoute(builder: (context) => const NewWindowTestPage())), + ), + ]; + + Widget _cameraMoveTestPage() { + return Padding( + padding: const EdgeInsets.all(24), + child: Column(children: const [ + // + // // todo + // Text("_cameraMoveTestPage"), + // Text("카메라 이동"), + // + ])); + } + + Widget _controllerTestPage() { + return Padding( + padding: const EdgeInsets.all(24), + child: Column(children: const [ + // todo + Text("_etcControllerTestPage"), + Text("기타 컨트롤러 기능"), + ])); + } + + Widget _pickTestPage() { + return Padding( + padding: const EdgeInsets.all(24), + child: Column(children: const [ + // todo + Text("_pickTestPage"), + Text("주변 심볼 및 오버레이 가져오기"), + ])); } } diff --git a/example/lib/main_test.dart b/example/lib/main_test.dart index 3fee92d01..5b4cd4d03 100644 --- a/example/lib/main_test.dart +++ b/example/lib/main_test.dart @@ -4,6 +4,8 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_naver_map/flutter_naver_map.dart'; +// 통합 테스트를 위한 main 입니다. + @visibleForTesting Future mainWithTest(int testId) async { print("---------------- NEW TEST : $testId ----------------"); diff --git a/example/lib/pages/bottom_drawer.dart b/example/lib/pages/bottom_drawer.dart new file mode 100644 index 000000000..f11097c22 --- /dev/null +++ b/example/lib/pages/bottom_drawer.dart @@ -0,0 +1,141 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bottom_drawer/flutter_bottom_drawer.dart'; + +import '../design/map_function_item.dart'; +import '../design/theme.dart'; + +class ExampleAppBottomDrawer { + final BuildContext context; + final Function(double height) onDrawerHeightChanged; + final Function() rebuild; + final List pages; + final void Function()? onPageDispose; + + ExampleAppBottomDrawer({ + required this.context, + required this.onDrawerHeightChanged, + required this.rebuild, + required this.pages, + this.onPageDispose, + }); + + ColorScheme get colorTheme => getColorTheme(context); + + TextTheme get textTheme => getTextTheme(context); + + late DrawerMoveController drawerController; + late DrawerState drawerState; + late Function(Function()) drawerSetState; + final scrollController = ScrollController(); + + MapFunctionItem? nowItem; + + bool get hasPage => nowItem != null; + + static MapFunctionItem makeDefault({ + required String title, + required String description, + required Widget Function(bool canScroll) page, + bool isScrollPage = true, + }) => + MapFunctionItem( + title: title, + description: description, + page: page, + onTap: null, + isScrollPage: isScrollPage); + + void go(MapFunctionItem item) { + nowItem = item; + rebuildDrawerAndPage(); + } + + void back() { + nowItem = null; + onPageDispose?.call(); + rebuildDrawerAndPage(); + } + + void rebuildDrawerAndPage() { + drawerSetState(() {}); // drawer rebuild + rebuild(); // page rebuild (height changed) + } + + BottomDrawer get bottomDrawer => BottomDrawer( + height: nowItem?.isScrollPage == false ? null : 200, + expandedHeight: 480, + handleSectionHeight: 20, + handleColor: colorTheme.secondary, + backgroundColor: colorTheme.background, + onReady: (controller) => drawerController = controller, + onStateChanged: (state) => drawerState = state, + onHeightChanged: onDrawerHeightChanged, + builder: (state, setState, context) { + drawerSetState = setState; + return hasPage + ? selectedPage(state == DrawerState.opened) + : innerListView(state == DrawerState.opened); + }); + + Widget innerListView(bool canScroll) => Column(children: [ + innerListViewHeader(), + Expanded( + child: ScrollConfiguration( + behavior: const ScrollBehavior().copyWith(overscroll: false), + child: Scrollbar( + controller: scrollController, + child: ListView.builder( + controller: scrollController, + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).padding.bottom), + physics: canScroll + ? const ClampingScrollPhysics() + : const NeverScrollableScrollPhysics(), + itemCount: pages.length, + itemBuilder: (context, index) { + MapFunctionItem page = pages[index]; + if (page.needItemOnTap) { + page = page.copyWith(onTap: go, onBack: back); + } + return page.getItemWidget(context); + }, + )))), + ]); + + Widget innerListViewHeader() => Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: colorTheme.onBackground.withOpacity(0.28), width: 0.2)), + ), + padding: const EdgeInsets.fromLTRB(24, 12, 24, 16), + child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [ + Text("지도 기능 둘러보기", style: textTheme.titleLarge), + const Spacer(), + Container( + padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 4), + decoration: BoxDecoration( + color: kReleaseMode ? colorTheme.primary : colorTheme.secondary, + borderRadius: BorderRadius.circular(4), + ), + child: Text(kReleaseMode ? "Release Mode" : "Debug Mode", + style: textTheme.labelSmall)), + ])); + + Widget selectedPage(bool canScroll) { + assert(nowItem != null); + return nowItem!.getPageWidget(context, canScroll: canScroll); + } + + bool processWillPop() { + if (hasPage) { + back(); + return false; + } else if (drawerState != DrawerState.closed) { + drawerController.close(); + return false; + } + return true; + } +} diff --git a/example/lib/pages/examples/options_example.dart b/example/lib/pages/examples/options_example.dart new file mode 100644 index 000000000..0a80c21e7 --- /dev/null +++ b/example/lib/pages/examples/options_example.dart @@ -0,0 +1,350 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_naver_map/flutter_naver_map.dart'; + +import '../../design/custom_widget.dart'; +import '../../util/location_util.dart'; +import '../../util/alert_util.dart'; + +class NaverMapViewOptionsExample extends StatelessWidget { + final NaverMapViewOptions options; + final Function(NaverMapViewOptions options) onOptionsChanged; + final bool canScroll; + + const NaverMapViewOptionsExample({ + Key? key, + required this.options, + required this.onOptionsChanged, + required this.canScroll, + }) : super(key: key); + + set options(NaverMapViewOptions value) { + final changedOptions = prepareOptionChange(value); + onOptionsChanged(changedOptions); + } + + void clearOptions() => onOptionsChanged(const NaverMapViewOptions()); + + NaverMapViewOptions prepareOptionChange(NaverMapViewOptions newOptions) { + if (!indoorAvailable) { + newOptions = newOptions.copyWith( + indoorEnable: false, indoorLevelPickerEnable: false); + } + if (!liteModeAvailable) { + newOptions = newOptions.copyWith(liteModeEnable: false); + } + if (!nightModeAvailable) { + newOptions = newOptions.copyWith(nightModeEnable: false); + } + + return newOptions; + } + + /// 실내 지도는 지도 유형이 [basic, terrain]만 가능합니다. + bool get indoorAvailable => + options.mapType == NMapType.basic || options.mapType == NMapType.terrain; + + /// 경량 모드는 지도 유형이 navi가 아닌 경우만 가능합니다. + bool get liteModeAvailable => + options.mapType != NMapType.navi && options.mapType != NMapType.none; + + /// 야간 모드는 지도 유형이 navi인 경우만 가능합니다. + bool get nightModeAvailable => options.mapType == NMapType.navi; + + @override + Widget build(BuildContext context) { + return Expanded( + child: ReLoader( + text: "옵션을 모두", + reload: clearOptions, + child: CustomScrollView( + primary: true, + physics: canScroll + ? const BouncingScrollPhysics() + : const NeverScrollableScrollPhysics(), + slivers: [ + SliverColumn([ + SelectorWithTitle("지도 유형", + description: ".mapType", + selector: (context) => EasyDropdown( + items: NMapType.values, + value: options.mapType, + onChanged: (v) => + options = options.copyWith(mapType: v))), + SelectorWithTitle("로고 위치", + description: ".logoAlign", + selector: (context) => EasyDropdown( + items: NLogoAlign.values, + value: options.logoAlign, + onChanged: (v) => + options = options.copyWith(logoAlign: v))) + ]), + sliverMultiSwitcherGrid([ + TextSwitcher( + title: "축척 바", + description: ".scaleBarEnable", + value: options.scaleBarEnable, + onChanged: (v) => + options = options.copyWith(scaleBarEnable: v)), + TextSwitcher( + title: "내 위치 버튼", + description: ".locationButtonEnable", + value: options.locationButtonEnable, + onChanged: (enable) { + void buttonEnable(bool v) => + options = options.copyWith(locationButtonEnable: v); + if (!enable) { + buttonEnable(false); + return; + } + requestLocationPermission(context, + onGranted: () => buttonEnable(true)); + }), + if (indoorAvailable) + TextSwitcher( + title: "실내 지도", + description: ".indoorEnable", + value: options.indoorEnable, + onChanged: (v) => + options = options.copyWith(indoorEnable: v)), + if (indoorAvailable) + TextSwitcher( + title: "실내 지도 레벨 피커", + description: ".indoorLevelPickerEnable", + value: options.indoorLevelPickerEnable, + onChanged: (v) => + options = options.copyWith(indoorLevelPickerEnable: v)), + if (liteModeAvailable) + TextSwitcher( + title: "경량 모드", + description: ".liteModeEnable", + value: options.liteModeEnable, + onChanged: (v) => + options = options.copyWith(liteModeEnable: v)), + if (nightModeAvailable) + TextSwitcher( + title: "야간 모드", + description: ".nightModeEnable", + value: options.nightModeEnable, + onChanged: (v) => + options = options.copyWith(nightModeEnable: v)), + ]), + SliverColumn([ + if (options.indoorEnable) + SelectorWithTitle("실내 지도 유지 반경", + description: ".indoorFocusRadius", + selector: (context) => EasySlider( + value: options.indoorFocusRadius, + onChanged: (v) => + options = options.copyWith(indoorFocusRadius: v))), + SelectorWithTitle("지도 명도", + description: ".lightness", + selector: (context) => EasySlider( + min: -1, + max: 1, + floatingPoint: 1, + value: options.lightness, + onChanged: (v) => + options = options.copyWith(lightness: v))), + SelectorWithTitle("건물 3D 높이", + description: ".buildingHeight", + selector: (context) => EasySlider( + max: 1, + floatingPoint: 1, + value: options.buildingHeight, + onChanged: (v) => + options = options.copyWith(buildingHeight: v))), + SelectorWithTitle("심볼 크기", + description: ".symbolScale", + selector: (context) => EasySlider( + max: 2, + floatingPoint: 1, + value: options.symbolScale, + onChanged: (v) => + options = options.copyWith(symbolScale: v))), + SelectorWithTitle("심볼 원근 계수", + description: ".symbolPerspectiveRatio", + selector: (context) => EasySlider( + max: 1, + floatingPoint: 1, + value: options.symbolPerspectiveRatio, + onChanged: (v) => options = + options.copyWith(symbolPerspectiveRatio: v))), + ]), + const SliverTitle("제스처 제어"), + sliverMultiSwitcherGrid([ + TextSwitcher( + title: "스크롤 제스처", + description: ".scrollGesturesEnable", + value: options.scrollGesturesEnable, + onChanged: (v) => + options = options.copyWith(scrollGesturesEnable: v)), + TextSwitcher( + title: "줌 제스처", + description: ".zoomGesturesEnable", + value: options.zoomGesturesEnable, + onChanged: (v) => + options = options.copyWith(zoomGesturesEnable: v)), + TextSwitcher( + title: "회전 제스처", + description: ".rotationGesturesEnable", + value: options.rotationGesturesEnable, + onChanged: (v) => + options = options.copyWith(rotationGesturesEnable: v)), + TextSwitcher( + title: "기울임 제스처", + description: ".tiltGesturesEnable", + value: options.tiltGesturesEnable, + onChanged: (v) => + options = options.copyWith(tiltGesturesEnable: v)), + TextSwitcher( + title: "멈춤 제스처", + description: ".stopGesturesEnable", + value: options.stopGesturesEnable, + onChanged: (v) => + options = options.copyWith(stopGesturesEnable: v)), + TextSwitcher( + title: "심볼 터치 소비", + description: ".consumeSymbolTapEvents", + value: options.consumeSymbolTapEvents, + onChanged: (v) => + options = options.copyWith(consumeSymbolTapEvents: v)), + TextSwitcher( + title: "로고 클릭", + description: ".logoClickEnable", + value: options.logoClickEnable, + onChanged: (v) => + options = options.copyWith(logoClickEnable: v)), + ]), + SliverColumn([ + SelectorWithTitle("오버레이 및 심볼\n터치 반경", + description: ".pickTolerance", + selector: (context) => EasySlider( + max: 8, + divisions: 8, + value: options.pickTolerance, + onChanged: (v) => + options = options.copyWith(pickTolerance: v))), + SelectorWithTitle("스크롤 마찰 계수", + description: ".scrollGesturesFriction", + selector: (context) => EasySlider( + max: 1, + divisions: null, + floatingPoint: 3, + value: options.scrollGesturesFriction, + defaultValue: + NaverMapViewOptions.defaultScrollGesturesFriction, + onChanged: (v) => options = + options.copyWith(scrollGesturesFriction: v))), + SelectorWithTitle("줌 마찰 계수", + description: ".zoomGesturesFriction", + selector: (context) => EasySlider( + max: 1, + divisions: null, + floatingPoint: 3, + value: options.zoomGesturesFriction, + defaultValue: + NaverMapViewOptions.defaultZoomGesturesFriction, + onChanged: (v) => + options = options.copyWith(zoomGesturesFriction: v))), + SelectorWithTitle("회전 마찰 계수", + description: ".rotationGesturesFriction", + selector: (context) => EasySlider( + max: 1, + divisions: null, + floatingPoint: 3, + value: options.rotationGesturesFriction, + defaultValue: + NaverMapViewOptions.defaultRotationGesturesFriction, + onChanged: (v) => options = + options.copyWith(rotationGesturesFriction: v))), + ]), + const SliverTitle("표시할 정보 레이어", description: ".activeLayerGroups"), + sliverMultiSwitcherGrid([ + layerGroupTextSwitcher("건물", NLayerGroup.building), + layerGroupTextSwitcher("교통정보", NLayerGroup.traffic), + layerGroupTextSwitcher("대중교통", NLayerGroup.transit, + enable: options.mapType != NMapType.navi), + layerGroupTextSwitcher("자전거", NLayerGroup.bicycle, + enable: options.mapType != NMapType.navi), + layerGroupTextSwitcher("등산정보", NLayerGroup.mountain, + enable: options.mapType != NMapType.navi), + layerGroupTextSwitcher("지적편집도", NLayerGroup.cadastral, + enable: options.mapType != NMapType.navi), + ], small: true), + const SliverTitle("이동 제한"), + SliverColumn([ + SelectorWithTitle("최소 줌 제한", + description: ".minZoom", + selector: (context) => EasySlider( + max: 21, + divisions: 21, + value: options.minZoom, + onChanged: (v) => + options = options.copyWith(minZoom: v))), + SelectorWithTitle("최대 줌 제한", + description: ".maxZoom", + selector: (context) => EasySlider( + max: 21, + divisions: 21, + value: options.maxZoom, + onChanged: (v) => + options = options.copyWith(maxZoom: v))), + SelectorWithTitle("최대 기울임 제한", + description: ".maxTilt", + selector: (context) => EasySlider( + max: 63, + divisions: 63, + value: options.maxTilt, + onChanged: (v) => + options = options.copyWith(maxTilt: v))), + ]), + const SliverBottomPadding(), + ]), + )); + } + + Widget sliverMultiSwitcherGrid(List switchers, + {bool small = false}) { + return SliverPadding( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24), + sliver: SliverGrid.count( + crossAxisCount: small ? 3 : 2, + mainAxisSpacing: small ? 6 : 8, + crossAxisSpacing: small ? 6 : 8, + childAspectRatio: small ? 2 : 2.6, + children: switchers, + )); + } + + void layerGroupChange(bool enable, {required NLayerGroup layer}) { + options = options.copyWith( + activeLayerGroups: enable + ? [layer, ...options.activeLayerGroups] + : options.activeLayerGroups.where((e) => e != layer).toList()); + } + + TextSwitcher layerGroupTextSwitcher(String title, NLayerGroup layer, + {bool enable = true}) { + return TextSwitcher( + title: title, + description: layer.name, + enable: enable && options.mapType != NMapType.none, + value: layerGroupContains(layer), + onChanged: (enable) => layerGroupChange(enable, layer: layer)); + } + + bool layerGroupContains(NLayerGroup layer) { + return options.activeLayerGroups.contains(layer); + } + + void requestLocationPermission(BuildContext context, + {required void Function() onGranted}) { + AlertUtil.openAlertIfResultTrue("위치 권한이 없습니다.\n위치를 가져오려면 권한을 허용해주세요.", + context: context, callback: () async { + final isGranted = await ExampleLocationUtil.requestAndGrantedCheck(); + if (isGranted) onGranted(); + final needShowAlert = !isGranted; + return needShowAlert; + }); + } +} diff --git a/example/lib/pages/examples/overlay_example.dart b/example/lib/pages/examples/overlay_example.dart new file mode 100644 index 000000000..0b108caa1 --- /dev/null +++ b/example/lib/pages/examples/overlay_example.dart @@ -0,0 +1,159 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_naver_map/flutter_naver_map.dart'; + +import '../../design/custom_widget.dart'; + +class NOverlayExample extends StatefulWidget { + final bool isClosed; + final NaverMapController mapController; + + const NOverlayExample({ + Key? key, + required this.mapController, + required this.isClosed, + }) : super(key: key); + + @override + State createState() => _NOverlayExampleState(); +} + +class _NOverlayExampleState extends State { + NOverlayType willCreateOverlayType = NOverlayType.marker; + NAddableOverlay? willCreateOverlay; + + NaverMapController get mapController => widget.mapController; + + void attachOverlay() async { + final point = await mapController.getCameraPosition().then((p) => p.target); + final marker = NMarker(id: "1", position: point); + marker.setOnTapListener((m) { + print(m); + mapController.latLngToScreenLocation(m.position).then((point) { + print("화면상 위치 : $point"); + addFlutterFloatingOverlay(point); + }); + }); + mapController.addOverlay(marker); + } + + OverlayEntry? entry; + + void addFlutterFloatingOverlay(NPoint point) { + final screenSize = MediaQuery.of(context).size; + + entry = OverlayEntry(builder: (context) { + return Positioned.fill( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + removeFlutterFloatingOverlay(); + }, + child: Stack(children: [ + Positioned( + left: (point.x) - 28, + top: point.y, + child: Balloon( + size: const Size(200, 400), + padding: const EdgeInsets.all(8), + child: Container( + // color: Colors.grey, + child: Column(children: const [ + Text("마커를 클릭하셨습니다."), + Text("이 오버레이는 Flutter 위젯으로 생성되었습니다."), + ]), + ))) + ]))); + }); + Overlay.of(context).insert(entry!); + } + + void removeFlutterFloatingOverlay() { + entry?.remove(); + entry = null; + } + + @override + Widget build(BuildContext context) { + return Column(children: [ + const SimpleTitle("화면 중앙에 오버레이가 생성됩니다.", + description: "생성된 오버레이를 터치하면 속성을 변경할 수 있어요.", + direction: Axis.vertical, + padding: EdgeInsets.symmetric(vertical: 8, horizontal: 24)), + SelectorWithTitle("오버레이 유형", + description: "NOverlayType", + selector: (context) => EasyDropdown( + items: NOverlayType.values + .where((t) => t != NOverlayType.locationOverlay) + .toList(), + value: willCreateOverlayType, + onChanged: (v) => setState(() => willCreateOverlayType = v))), + SimpleButton( + text: "${getOverlayKoreanName(willCreateOverlayType)} 생성", + margin: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + action: attachOverlay), + if (!widget.isClosed) + Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 12), + child: Row(children: [ + Expanded( + child: SimpleButton( + text: + "${getOverlayKoreanName(willCreateOverlayType)}만 모두 지우기", + color: Colors.orange, + margin: EdgeInsets.zero, + action: () => mapController.clearOverlays( + type: willCreateOverlayType)), + ), + const SizedBox(width: 12), + Expanded( + child: SimpleButton( + text: "모두 지우기", + color: Colors.red, + margin: EdgeInsets.zero, + action: () => mapController.clearOverlays()), + ) + ])), + const BottomPadding(), + ]); + } + + List markerOptions() => []; + + String get timeBasedId => "${DateTime.now().millisecondsSinceEpoch}"; + static const mockLatLng = NLatLng(0, 0); + static const mockLatLngBounds = + NLatLngBounds(southWest: NLatLng(0, 0), northEast: NLatLng(0, 0)); + static const assetImage = + NOverlayImage.fromAssetImage('assets/images/overlay.png'); // todo + + String getOverlayKoreanName(NOverlayType type) { + switch (type) { + case NOverlayType.marker: + return "마커"; + case NOverlayType.infoWindow: + return "정보창"; + case NOverlayType.circleOverlay: + return "원 오버레이"; + case NOverlayType.groundOverlay: + return "지상 오버레이"; + case NOverlayType.polygonOverlay: + return "다각형 오버레이"; + case NOverlayType.polylineOverlay: + return "선 오버레이"; + case NOverlayType.pathOverlay: + return "경로 오버레이"; + case NOverlayType.multipartPathOverlay: + return "경로(멀티파트) 오버레이"; + case NOverlayType.arrowheadPathOverlay: + return "경로(화살표) 오버레이"; + case NOverlayType.locationOverlay: + throw Exception("locationOverlay is not supported"); + } + } + + @override + void dispose() { + removeFlutterFloatingOverlay(); + super.dispose(); + } +} diff --git a/example/lib/pages/new_window_page.dart b/example/lib/pages/new_window_page.dart new file mode 100644 index 000000000..1262e7c7d --- /dev/null +++ b/example/lib/pages/new_window_page.dart @@ -0,0 +1,81 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_naver_map/flutter_naver_map.dart'; + +import '../main.dart'; + +class DoubleMapTestPage extends StatefulWidget { + const DoubleMapTestPage({Key? key}) : super(key: key); + + @override + State createState() => _DoubleMapTestPageState(); +} + +class _DoubleMapTestPageState extends State { + bool map2Enabled = false; + int count = 0; + Timer? timer; + final maxCount = 500; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Column( + children: [ + const Expanded(child: NaverMap()), + Padding( + padding: const EdgeInsets.all(16), + child: ElevatedButton( + onPressed: () { + if (count == 0) { + timer = Timer.periodic( + const Duration(milliseconds: 1200), (timer) { + count++; + WidgetsBinding.instance.addPostFrameCallback( + (_) => setState(() => map2Enabled = true)); + setState(() => map2Enabled = false); + if (count == maxCount) { + timer.cancel(); + count = 0; + } + }); + } else { + timer?.cancel(); + count = 0; + map2Enabled = false; + setState(() {}); + } + }, + child: Text(count == 0 + ? "start run" + : "stop (running, ${Duration(milliseconds: (maxCount - count) * 1200)} left)"))), + Expanded( + child: map2Enabled + ? const NaverMap() + : Container(color: Colors.green), + ), + ], + ), + ), + ); + } +} + +class NewWindowTestPage extends StatelessWidget { + const NewWindowTestPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: ElevatedButton( + onPressed: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => const DoubleMapTestPage())); + }, + child: const Text('openMap'))), + ); + } +} diff --git a/example/lib/util/alert_util.dart b/example/lib/util/alert_util.dart new file mode 100644 index 000000000..e90d8b7bc --- /dev/null +++ b/example/lib/util/alert_util.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_styled_toast/flutter_styled_toast.dart'; + +import '../design/theme.dart'; + +class AlertUtil { + static void openErrorAlert(String title, {required BuildContext context}) { + showToast( + title, + context: context, + textStyle: + getTextTheme(context).titleSmall?.copyWith(color: Colors.white), + backgroundColor: Colors.red.shade700.withOpacity(0.92), + toastHorizontalMargin: 24, + position: + const StyledToastPosition(align: Alignment.bottomCenter, offset: 36), + animation: StyledToastAnimation.fade, + reverseAnimation: StyledToastAnimation.fade, + animDuration: const Duration(milliseconds: 300), + ); + } + + static void openAlertIfResultTrue(String title, + {required BuildContext context, + required Future Function() callback}) { + void openToast() => openErrorAlert(title, context: context); + + callback.call().then((open) { + if (open) openToast(); + }); + } + + AlertUtil._(); +} diff --git a/example/lib/util/location_util.dart b/example/lib/util/location_util.dart new file mode 100644 index 000000000..f21d25854 --- /dev/null +++ b/example/lib/util/location_util.dart @@ -0,0 +1,29 @@ +import 'dart:io'; + +import 'package:permission_handler/permission_handler.dart'; + +class ExampleLocationUtil { + static const locationPermission = Permission.locationWhenInUse; + + static Future get _hasPermission async { + final denied = await locationPermission.isDenied; + return !denied; + } + + static Future _requestPermission() async { + final status = await locationPermission.request(); + return status.isGranted; + } + + static Future requestAndGrantedCheck() async { + if (Platform.isAndroid) { + final enabled = await _hasPermission; // 권한이 있는지 확인합니다. + if (enabled) return true; // 있다면, true를 반환합니다. + } + + final granted = await _requestPermission(); // 권한이 없으므로, 권한 요청을 합니다. + if (granted) return true; // 권한이 허용되었다면, true를 반환합니다. + + return false; // 거부되었음을 알리기 위해, false를 반환합니다. + } +} diff --git a/example/pubspec.lock b/example/pubspec.lock index bb612dbdd..49e47b988 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -86,6 +86,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_bottom_drawer: + dependency: "direct main" + description: + name: flutter_bottom_drawer + sha256: "7e128d80fc5d8e5f5d0a1054bd39fda2f5843e2d6af7c16b90a17ea511bb5092" + url: "https://pub.dev" + source: hosted + version: "0.0.6" flutter_driver: dependency: transitive description: flutter @@ -99,97 +107,49 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_naver_map: dependency: "direct main" description: path: ".." relative: true source: path - version: "1.0.0-dev.9" + version: "1.0.0-dev.10" + flutter_styled_toast: + dependency: "direct main" + description: + name: flutter_styled_toast + sha256: cc32aed2a49ce77a1ed5844073c6c0f5e381c81fd6d694e0ba3c5dc2a645963d + url: "https://pub.dev" + source: hosted + version: "2.1.3" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" - flutter_web_plugins: + fuchsia_remote_debug_protocol: dependency: transitive description: flutter source: sdk version: "0.0.0" - fuchsia_remote_debug_protocol: - dependency: transitive + integration_test: + dependency: "direct dev" description: flutter source: sdk version: "0.0.0" - geolocator: - dependency: "direct main" - description: - name: geolocator - sha256: "5c23f3613f50586c0bbb2b8f970240ae66b3bd992088cf60dd5ee2e6f7dde3a8" - url: "https://pub.dev" - source: hosted - version: "9.0.2" - geolocator_android: - dependency: transitive - description: - name: geolocator_android - sha256: "2ba24690aee0a3e1b6b7bd47c2711a50c874e95e4c758346589d35194adf6d6a" - url: "https://pub.dev" - source: hosted - version: "4.1.7" - geolocator_apple: - dependency: transitive - description: - name: geolocator_apple - sha256: "22b60ca3b8c0f58e6a9688ff855ee39ab813ca3f0c0609a48d282f6631266f2e" - url: "https://pub.dev" - source: hosted - version: "2.2.5" - geolocator_platform_interface: - dependency: transitive - description: - name: geolocator_platform_interface - sha256: af4d69231452f9620718588f41acc4cb58312368716bfff2e92e770b46ce6386 - url: "https://pub.dev" - source: hosted - version: "4.0.7" - geolocator_web: - dependency: transitive - description: - name: geolocator_web - sha256: f68a122da48fcfff68bbc9846bb0b74ef651afe84a1b1f6ec20939de4d6860e1 - url: "https://pub.dev" - source: hosted - version: "2.1.6" - geolocator_windows: + intl: dependency: transitive description: - name: geolocator_windows - sha256: f5911c88e23f48b598dd506c7c19eff0e001645bdc03bb6fecb9f4549208354d + name: intl + sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" url: "https://pub.dev" source: hosted - version: "0.1.1" - http: - dependency: "direct main" - description: - name: http - sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" - url: "https://pub.dev" - source: hosted - version: "0.13.5" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" - url: "https://pub.dev" - source: hosted - version: "4.0.2" - integration_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" + version: "0.17.0" js: dependency: transitive description: @@ -286,6 +246,46 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "33c6a1253d1f95fd06fa74b65b7ba907ae9811f9d5c1d3150e51417d04b8d6a8" + url: "https://pub.dev" + source: hosted + version: "10.2.0" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "8028362b40c4a45298f1cbfccd227c8dd6caf0e27088a69f2ba2ab15464159e2" + url: "https://pub.dev" + source: hosted + version: "10.2.0" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: "9c370ef6a18b1c4b2f7f35944d644a56aa23576f23abee654cf73968de93f163" + url: "https://pub.dev" + source: hosted + version: "9.0.7" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: "68abbc472002b5e6dfce47fe9898c6b7d8328d58b5d2524f75e277c07a97eb84" + url: "https://pub.dev" + source: hosted + version: "3.9.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: f67cab14b4328574938ecea2db3475dad7af7ead6afab6338772c5f88963e38b + url: "https://pub.dev" + source: hosted + version: "0.1.2" platform: dependency: transitive description: @@ -310,6 +310,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.2.4" + pull_to_refresh_flutter3: + dependency: "direct main" + description: + name: pull_to_refresh_flutter3 + sha256: "223a6241067162dc15cf8c46c05af998ce7aa85e0703d8f696101eb1b5629d76" + url: "https://pub.dev" + source: hosted + version: "2.0.1" sky_engine: dependency: transitive description: flutter @@ -420,5 +428,5 @@ packages: source: hosted version: "0.2.0+3" sdks: - dart: ">=2.18.4 <3.0.0" + dart: ">=2.19.2 <3.0.0" flutter: ">=3.0.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 94b964b3c..51948a9f4 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -15,8 +15,10 @@ dependencies: flutter_naver_map: path: ../ - geolocator: ^9.0.2 - http: ^0.13.5 + flutter_bottom_drawer: 0.0.6 + permission_handler: 10.2.0 + flutter_styled_toast: 2.1.3 + pull_to_refresh_flutter3: 2.0.1 dev_dependencies: flutter_test: diff --git a/ios/Classes/controller/overlay/OverlayController.swift b/ios/Classes/controller/overlay/OverlayController.swift index 9a4cb0c9d..c8cc92d0b 100644 --- a/ios/Classes/controller/overlay/OverlayController.swift +++ b/ios/Classes/controller/overlay/OverlayController.swift @@ -1,6 +1,6 @@ import NMapsMap -internal class OverlayController: OverlayHandler, ArrowheadPathOverlayHandler, CircleOverlayHandler, GroundOverlayHandler, InfoWindowHandler, LocationOverlayHandler, MarkerHandler, MultipartPathOverlayHandler, PathOverlayHandler, PolygonOverlayHandler, PolylineOverlayHandler { +internal class OverlayController: OverlaySender, OverlayHandler, ArrowheadPathOverlayHandler, CircleOverlayHandler, GroundOverlayHandler, InfoWindowHandler, LocationOverlayHandler, MarkerHandler, MultipartPathOverlayHandler, PathOverlayHandler, PolygonOverlayHandler, PolylineOverlayHandler { private let channel: FlutterMethodChannel @@ -8,6 +8,12 @@ internal class OverlayController: OverlayHandler, ArrowheadPathOverlayHandler, C self.channel = channel channel.setMethodCallHandler(handler) } + + /* ----- sender ----- */ + func onOverlayTapped(info: NOverlayInfo) { + let query = NOverlayQuery(info: info, methodName: onTapName).query + channel.invokeMethod(query, arguments: nil) + } /* ----- overlay storage ----- */ @@ -16,9 +22,8 @@ internal class OverlayController: OverlayHandler, ArrowheadPathOverlayHandler, C func saveOverlay(overlay: NMFOverlay, info: NOverlayInfo) { info.saveAtOverlay(overlay) detachOverlay(info: info) - let query = NOverlayQuery(info: info, methodName: onTapName).query overlay.touchHandler = { [weak self] overlay in - self?.channel.invokeMethod(query, arguments: nil) + self?.onOverlayTapped(info: info) return true } overlays[info] = overlay @@ -592,4 +597,4 @@ internal class OverlayController: OverlayHandler, ArrowheadPathOverlayHandler, C func removeChannel() { channel.setMethodCallHandler(nil) } -} \ No newline at end of file +} diff --git a/ios/Classes/controller/overlay/OverlaySender.swift b/ios/Classes/controller/overlay/OverlaySender.swift new file mode 100644 index 000000000..39d299bc7 --- /dev/null +++ b/ios/Classes/controller/overlay/OverlaySender.swift @@ -0,0 +1,3 @@ +internal protocol OverlaySender { + func onOverlayTapped(info: NOverlayInfo) +} diff --git a/lib/src/controller/map/controller.dart b/lib/src/controller/map/controller.dart index 59c5f9f53..9267bfa0e 100644 --- a/lib/src/controller/map/controller.dart +++ b/lib/src/controller/map/controller.dart @@ -1,12 +1,13 @@ part of flutter_naver_map; abstract class NaverMapController extends _NaverMapControlSender { - static NaverMapController createController(MethodChannel controllerChannel, + static NaverMapController _createController(MethodChannel controllerChannel, {required int viewId}) { - final overlayController = _NOverlayControllerImpl( - NChannel.overlayChannelName.createChannel(viewId)); + final overlayController = _NOverlayControllerImpl(viewId: viewId); return _NaverMapControllerImpl(controllerChannel, overlayController); } + + void dispose(); } class _NaverMapControllerImpl @@ -137,7 +138,7 @@ class _NaverMapControllerImpl Future deleteOverlay(NOverlayInfo info) async { assert(info.type != NOverlayType.locationOverlay); await invokeMethod("deleteOverlay", info); - overlayController.disposeWithInfo(info); + overlayController.deleteWithInfo(info); } @override @@ -161,4 +162,9 @@ class _NaverMapControllerImpl @override String toString() => "NaverMapController(channel: ${channel.name})"; + + @override + void dispose() { + overlayController.disposeChannel(); + } } diff --git a/lib/src/controller/overlay/overlay_controller.dart b/lib/src/controller/overlay/overlay_controller.dart index d8ec21ad3..c5def93a2 100644 --- a/lib/src/controller/overlay/overlay_controller.dart +++ b/lib/src/controller/overlay/overlay_controller.dart @@ -5,7 +5,7 @@ abstract class _NOverlayController with NChannelWrapper { void add(NOverlayInfo info, NOverlay overlay); - void disposeWithInfo(NOverlayInfo info); + void deleteWithInfo(NOverlayInfo info); void clear(NOverlayType? type); } diff --git a/lib/src/controller/overlay/overlay_controller_impl.dart b/lib/src/controller/overlay/overlay_controller_impl.dart index 7fae1cc8b..b7a8b6064 100644 --- a/lib/src/controller/overlay/overlay_controller_impl.dart +++ b/lib/src/controller/overlay/overlay_controller_impl.dart @@ -1,16 +1,17 @@ part of flutter_naver_map; -class _NOverlayControllerImpl extends _NOverlayController { +class _NOverlayControllerImpl extends _NOverlayController with NChannelWrapper { final Map overlayFunctionMap = {}; @override - final MethodChannel channel; + late final MethodChannel channel; NOverlayInfo get _locationOverlayInfo => NLocationOverlay._locationOverlayInfo; - _NOverlayControllerImpl(this.channel) { - channel.setMethodCallHandler(_handleMethodCall); + _NOverlayControllerImpl({required int viewId}) { + createChannel(NChannel.overlayChannelName, + id: viewId, handler: _handleMethodCall); } Future _handleMethodCall(MethodCall call) async { @@ -32,7 +33,7 @@ class _NOverlayControllerImpl extends _NOverlayController { } @override - void disposeWithInfo(NOverlayInfo info) { + void deleteWithInfo(NOverlayInfo info) { overlayFunctionMap.remove(info); } diff --git a/lib/src/messaging/channel_maker.dart b/lib/src/messaging/channel_types.dart similarity index 75% rename from lib/src/messaging/channel_maker.dart rename to lib/src/messaging/channel_types.dart index 2c65579c9..ed6ec6473 100644 --- a/lib/src/messaging/channel_maker.dart +++ b/lib/src/messaging/channel_types.dart @@ -8,10 +8,9 @@ enum NChannel { const NChannel(this.str); - static const separateString = "#"; + static const _separateString = "#"; - MethodChannel createChannel(int id) => - MethodChannel("$str$separateString$id"); + MethodChannel _create(int id) => MethodChannel("$str$_separateString$id"); @override String toString() => name; diff --git a/lib/src/messaging/channel_wrapper.dart b/lib/src/messaging/channel_wrapper.dart index b2f6c9f94..007b5bebe 100644 --- a/lib/src/messaging/channel_wrapper.dart +++ b/lib/src/messaging/channel_wrapper.dart @@ -3,6 +3,21 @@ part of flutter_naver_map_messaging; mixin NChannelWrapper { MethodChannel get channel; + // optional implementation + set channel(MethodChannel channel) {} + + void createChannel( + NChannel channelType, { + required int id, + Future Function(MethodCall)? handler, + }) { + channel = channelType._create(id)..setMethodCallHandler(handler); + } + + void disposeChannel() { + channel.setMethodCallHandler(null); + } + Future invokeMethod(String funcName, [NMessageable? arg]) { return channel.invokeMethod(funcName, arg?.payload); } diff --git a/lib/src/messaging/messaging.dart b/lib/src/messaging/messaging.dart index 69e34a6e0..ea8c522b8 100644 --- a/lib/src/messaging/messaging.dart +++ b/lib/src/messaging/messaging.dart @@ -6,7 +6,7 @@ import 'package:flutter/services.dart'; import "package:flutter/widgets.dart"; import "package:flutter_naver_map/flutter_naver_map.dart"; -part 'channel_maker.dart'; +part 'channel_types.dart'; part 'channel_wrapper.dart'; part 'messageable.dart'; part 'messaging_util.dart'; diff --git a/lib/src/type/map/overlay/overlay/addable/info_window.dart b/lib/src/type/map/overlay/overlay/addable/info_window.dart index 91edfa59b..08e97dd31 100644 --- a/lib/src/type/map/overlay/overlay/addable/info_window.dart +++ b/lib/src/type/map/overlay/overlay/addable/info_window.dart @@ -94,7 +94,7 @@ class NInfoWindow extends NAddableOverlay { void close() { _runAsync(_closeName); - _overlayController!.disposeWithInfo(info); + _overlayController!.deleteWithInfo(info); } @override diff --git a/lib/src/widget/map_widget.dart b/lib/src/widget/map_widget.dart index 749c028f8..37f7cf284 100644 --- a/lib/src/widget/map_widget.dart +++ b/lib/src/widget/map_widget.dart @@ -31,8 +31,10 @@ class NaverMap extends StatefulWidget { State createState() => _NaverMapState(); } -class _NaverMapState extends State with _NaverMapControlHandler { - late final MethodChannel methodChannel; +class _NaverMapState extends State + with _NaverMapControlHandler, NChannelWrapper { + @override + late final MethodChannel channel; late final NaverMapController controller; final controllerCompleter = Completer(); late NaverMapViewOptions nowViewOptions = widget.options; @@ -63,10 +65,9 @@ class _NaverMapState extends State with _NaverMapControlHandler { } void _onPlatformViewCreated(int id) { - methodChannel = NChannel.naverMapNativeView.createChannel(id); - controller = NaverMapController.createController(methodChannel, viewId: id); + createChannel(NChannel.naverMapNativeView, id: id, handler: handle); + controller = NaverMapController._createController(channel, viewId: id); controllerCompleter.complete(); - methodChannel.setMethodCallHandler(handle); } Set> _createGestureRecognizers( @@ -104,4 +105,11 @@ class _NaverMapState extends State with _NaverMapControlHandler { @override void onCameraIdle() => widget.onCameraIdle?.call(); + + @override + void dispose() { + controller.dispose(); + disposeChannel(); + super.dispose(); + } } diff --git a/pubspec.yaml b/pubspec.yaml index aa8c9199b..fe006fb9e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_naver_map description: Naver Map plugin for Flutter, which provides map service of Korea. -version: 1.0.0-dev.9 +version: 1.0.0-dev.10 homepage: https://github.com/note11g/flutter_naver_map environment: