Más contenido relacionado La actualidad más candente (20) Similar a 怎樣在 Flutter app 中使用 Google Maps (20) Más de Weizhong Yang (20) 怎樣在 Flutter app 中使用 Google Maps1. 怎樣在 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
5. 這個 App 有哪些功能?
• 顯示 ODB 傳來的讀數(速度、溫度等)
• 在地圖上顯示⽬前⾞⼦與⼿機的位置
• 顯示 Journey(每次⾞⼦上⽕/熄⽕之間的駕駛紀錄)
• 路徑規劃:
• 怎樣⾛到我的⾞⼦?
• 怎樣去加油站/維修站/⾃訂地點?
• Geo Fence 規劃(在某個區域開⾞時發出警告)
• Curfew 規劃(在某個時間開⾞發出警告)
• 各種警告—急彎、急停、撞擊、進⼊ Geo Fence
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];
}
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;
},
),
22. 設定地圖樣式
• GoogleMap Widget 本身
沒有 light/dark mode
• ⽽是設置整個 Map 樣式
的 JSON
• https://
console.cloud.google.co
m/projectselector2/
google/maps-apis/studio/
styles?pli=1
24. 疑難雜症
• Android 上,如果 App 放在背景再回到前景,之後 GoogleMap Widget 可
能畫⾯花掉,或是有奇怪的狀況
• 可以⽤ WidgetsBindingObserver 偵測回到前景重繪
(didChangeAppLifecycleState)
• 重新 Build GoogleMap Widget 也不⾒得會重繪
• 但是呼叫 setMapStyle ⼀定可以重繪
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),
);
}
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';
30. Recap
• 放置地圖 Widget
• 新增 Marker
• 新增 Polyline
• 計算 zoom level
• 顯示 information window
• 移動地圖、設定樣式
• 其他…