Publicité
Publicité

Contenu connexe

Publicité

Dernier(20)

Publicité

怎樣在 Flutter app 中使用 Google Maps

  1. 怎樣在 Flutter App 中整合 Google Maps Weizhong Yang a.k.a zonble zonble@gmail.com
  2. 關於我 Weizhong Yang a.k.a zonble • 最近⼗年都在做 App 開發 • Flutter GDE (2019-) • Developer Manager at Cerence Inc (2020-) • iOS Developer Lead at KKBOX (2011-2020) • 今年做的事:講了⼀場 Flutter Desktop plugin 開發,下半年常跑⾦⾨,開始重 寫⼆⼗年前的《防區狀況三⽣效》的新章… • Twitter @zonble
  3. 還記得我去年的題⽬? 總之,今年我們把去年題⽬裡頭的那個 App 商品化了
  4. Cerence Link
  5. 這個 App 有哪些功能? • 顯示 ODB 傳來的讀數(速度、溫度等) • 在地圖上顯示⽬前⾞⼦與⼿機的位置 • 顯示 Journey(每次⾞⼦上⽕/熄⽕之間的駕駛紀錄) • 路徑規劃: • 怎樣⾛到我的⾞⼦? • 怎樣去加油站/維修站/⾃訂地點? • Geo Fence 規劃(在某個區域開⾞時發出警告) • Curfew 規劃(在某個時間開⾞發出警告) • 各種警告—急彎、急停、撞擊、進⼊ Geo Fence
  6. 這個 App ⼤量需要顯示地圖
  7. Agenda • 放置地圖 Widget • 新增 Marker • 新增 Polyline • 計算 zoom level • 顯示 information window • 移動地圖、設定樣式 • 其他…
  8. 會⽤到哪些 package? • google_maps_ fl utter: The o ffi cial package • custom_info_window: Show custom window in the map • google_place: Google Place API • google_maps_utils: 地圖相關計算功能 • fl utter_polyline_points: encode/decode Google polyline string
  9. 基本導⼊ • 更新 pubspec.yaml • 加⼊ google_maps_ fl utter • fl utter pub get • 需要⼀些平台權限,例如使⽤ GPS 等,需要在 iOS 的 Info Plist 以及 Android 的 Manifest 裡頭加上對應設定 • Android 上需要安裝 Google Maps App
  10. 基本⽤法 GoogleMap( minMaxZoomPreference: MinMaxZoomPreference(0, 16), initialCameraPosition: LatLng(…..), mapType: MapType.normal, mapToolbarEnabled: false, zoomControlsEnabled: false, rotateGesturesEnabled: false, scrollGesturesEnabled: false, zoomGesturesEnabled: false, myLocationButtonEnabled: false, )
  11. Platform View • Google Maps Widget 是⼀個 Platform View • 將 Native View 包進 Flutter 中 • 現在我們也可以在 Platform View 上重疊任意的 Flutter Widget
  12. 加上 Marker fi nal startPoint = await BitmapDescriptor.fromAssetImage( ImageCon fi guration(size: Size(100, 100)), ‘asset/image.png’); var markers = []; fi nal startPoint = Marker( markerId: MarkerId('pin-start'), icon: _startPoint, anchor: O ff set(0.5, 0.5), position: LatLng(….)), zIndex: 10, ); markers.add(startPoint); GoogleMap(markers:markers); Image 的載⼊可以使⽤ Future Builder 需要注意載⼊的解析度(@2x、@3x)
  13. 放置 SVG Marker import 'dart:ui' as ui; import ‘package: fl utter_svg/ fl utter_svg.dart'; static Future<BitmapDescriptor?> bitmapDescriptorFromSvgAsset( BuildContext context, String assetName, Size size) async { // Read SVG fi le as String String svgString = await DefaultAssetBundle.of(context).loadString(assetNa me); // Create DrawableRoot from SVG String DrawableRoot svgDrawableRoot = await svg.fromSvgString(svgString, 'a'); // toPicture() and toImage() don't seem to be pixel ratio aware, so we calculate the actual sizes here MediaQueryData queryData = MediaQuery.of(context); double devicePixelRatio = queryData.devicePixelRatio; double width = size.width * devicePixelRatio; // where 32 is your SVG's original width double height = size.height * devicePixelRatio; // same thing // Convert to ui.Picture ui.Picture picture = svgDrawableRoot.toPicture(size: Size(width, height)); ui.Image image = await picture.toImage(width.toInt(), height.toInt()); ByteData? bytes = await image.toByteData(format: ui.ImageByteFormat.png); fi nal bu ff er = bytes?.bu ff er; if (bu ff er != null) { return BitmapDescriptor.fromBytes(bu ff er.asUint8List()); } return null; }
  14. 加上 Polyline fi nal points = <LatLng>[……]; var lines = <Polyline>{}; fi nal line = Polyline( polylineId: PolylineId('planning_route'), color: planningRouteColor, width: 6, points: points, ); lines.add(line); GoogleMap(polylines: polylines);
  15. 根據 route 計算 zoom level num latRad(lat) { var sin = math.sin(lat * math.pi / 180); var radX2 = math.log((1 + sin) / (1 - sin)) / 2; return math.max(math.min(radX2, math.pi), -math.pi) / 2; } num zoom(mapPx, worldPx, fraction) { if (fraction == 0) { return 21; } fi nal left = math.log(mapPx / worldPx / fraction); fi nal result = (left. fl oor() / math.ln2); return result; } List<num> _getRegion(num screenWidth, num screenHeight) { num zoomMax = 16.0; num? minLong = ….; num? maxLong = ….; num? minLat = ….; num? maxLat = ….; fi nal latFraction = (latRad(maxLat) - latRad(minLat)) / math.pi; fi nal lngDi ff = maxLong - minLong; fi nal lngFraction = ((lngDi ff < 0) ? (lngDi ff + 360) : lngDi ff ) / 360; num latZoom = zoom(screenHeight, 256, latFraction); num lngZoom = zoom(screenWidth, 256, lngFraction); fi nal zoomLevel = math.min(math.min(latZoom, lngZoom) * 0.99, zoomMax); fi nal centerLat = (maxLat + minLat) / 2; fi nal centerLong = (maxLong + minLong) / 2; return [centerLat, centerLong, zoomLevel]; }
  16. Custom Info Window custom_info_window https://pub.dev/packages/custom_info_window
  17. Custom Info Window Stack( children: [ PreloadMapWrapper( child: GoogleMap( onCameraMove: (position) { _customInfoWindowController.onCameraMove?.call(); }, onMapCreated: (GoogleMapController controller) { _customInfoWindowController.googleMapController = controller; _mapController = controller; }, ), ), CustomInfoWindow( controller: _customInfoWindowController, width: 274, height: 189, o ff set: 30, ), ], ); var _customInfoWindowController = CustomInfoWindowController(); _customInfoWindowController.addInfoWindow?.call(window, LatLng(…)); _customInfoWindowController.hideInfoWindow?.call();
  18. Circle fi nal pin = Circle( circleId: CircleId('circle'), radius: radius.toDouble(), fi llColor: …., strokeColor: …., center: location, ); fi nal circles = <Circle>{}; circles.add(pin); GoogleMap( circles: circles )
  19. 如何更新地圖內容 • 可以透過 setState() 更新包含 GoogleMap Widget 的 Widget • 如果使⽤ Bloc,可以將 GoogleMap Widget 放在 BlocBuilder 中 • 以上⽅式可以修改有哪些 Marker、Polyline 與 Circle,不會改變地圖的 zoom level 與中央位置
  20. 移動地圖 Future<void> goTo(num latitude, num longitude) async { fi nal position = CameraPosition( target: LatLng(latitude.toDouble(), longitude.toDouble()), zoom: zoomLevel, ); _mapController?.animateCamera(CameraUpdate.newCameraPosition(position)); } GoogleMapController? _mapController GoogleMap( onMapCreated: (GoogleMapController controller) { _mapController = controller; }, ),
  21. 進階設置 • liteModeEnabled:⽤在完全靜態的地圖上 • indoorViewEnabled:顯示室內導航 • tra ffi cEnabled:顯示交通狀況 • buildingsEnabled:顯示建築物模型
  22. 設定地圖樣式 • GoogleMap Widget 本身 沒有 light/dark mode • ⽽是設置整個 Map 樣式 的 JSON • https:// console.cloud.google.co m/projectselector2/ google/maps-apis/studio/ styles?pli=1
  23. 設定地圖樣式 rootBundle.loadString('assets/map_style.txt').then((string) { _mapStyle = string; }); GoogleMap( onMapCreated: (GoogleMapController controller) { mapController = controller; mapController.setMapStyle(_mapStyle); } ); 這個 Future 也可以考慮放在 FutureBuilder 裡頭
  24. 疑難雜症 • Android 上,如果 App 放在背景再回到前景,之後 GoogleMap Widget 可 能畫⾯花掉,或是有奇怪的狀況 • 可以⽤ WidgetsBindingObserver 偵測回到前景重繪 (didChangeAppLifecycleState) • 重新 Build GoogleMap Widget 也不⾒得會重繪 • 但是呼叫 setMapStyle ⼀定可以重繪
  25. 地點搜尋
  26. 地點搜尋 fi nal _googlePlace = GooglePlace(kGooglePlacesApikey); Future<TextSearchResponse?> _callGooglePlaceAPI(String keyword, { required double lat, required double lng }) async { return await _googlePlace.search.getTextSearch(keyword, location: Location(lat: lat, lng: lng), ); }
  27. 路徑規劃
  28. 路徑規劃 PolylineResult result = await PolylinePoints().getRouteBetweenCoordinates( kGoogleDirectionsApiKey, PointLatLng(lat1, lng1), PointLatLng(lat2, lng2), travelMode: TravelMode.driving, ); import 'package: fl utter_polyline_points/ fl utter_polyline_points.dart';
  29. 路徑偏移(Route deviation) • 判斷⽬前的 GPS 位置是否偏移規劃的路徑 • 可以使⽤ google_map_polyutil • 呼叫 PolyUtils.isLocationOnEdgeTolerance,判斷座標是否在路徑上 • 還有其他⼯具
  30. Recap • 放置地圖 Widget • 新增 Marker • 新增 Polyline • 計算 zoom level • 顯示 information window • 移動地圖、設定樣式 • 其他…
  31. That’s all 謝謝⼤家
Publicité