SlideShare una empresa de Scribd logo
1 de 31
Descargar para leer sin conexión
怎樣在 Flutter App 中整合
Google Maps
Weizhong Yang a.k.a zonble
zonble@gmail.com
關於我
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
還記得我去年的題⽬?
總之,今年我們把去年題⽬裡頭的那個 App 商品化了
Cerence
Link
這個 App 有哪些功能?
• 顯示 ODB 傳來的讀數(速度、溫度等)
• 在地圖上顯示⽬前⾞⼦與⼿機的位置
• 顯示 Journey(每次⾞⼦上⽕/熄⽕之間的駕駛紀錄)
• 路徑規劃:
• 怎樣⾛到我的⾞⼦?
• 怎樣去加油站/維修站/⾃訂地點?
• Geo Fence 規劃(在某個區域開⾞時發出警告)
• Curfew 規劃(在某個時間開⾞發出警告)
• 各種警告—急彎、急停、撞擊、進⼊ Geo Fence
這個 App ⼤量需要顯示地圖
Agenda
• 放置地圖 Widget
• 新增 Marker
• 新增 Polyline
• 計算 zoom level
• 顯示 information window
• 移動地圖、設定樣式
• 其他…
會⽤到哪些 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
基本導⼊
• 更新 pubspec.yaml
• 加⼊ google_maps_
fl
utter
•
fl
utter pub get
• 需要⼀些平台權限,例如使⽤ GPS 等,需要在 iOS 的 Info Plist 以及
Android 的 Manifest 裡頭加上對應設定
• Android 上需要安裝 Google Maps App
基本⽤法
GoogleMap(
minMaxZoomPreference: MinMaxZoomPreference(0, 16),
initialCameraPosition: LatLng(…..),
mapType: MapType.normal,
mapToolbarEnabled: false,
zoomControlsEnabled: false,
rotateGesturesEnabled: false,
scrollGesturesEnabled: false,
zoomGesturesEnabled: false,
myLocationButtonEnabled: false,
)
Platform View
• Google Maps Widget 是⼀個 Platform View
• 將 Native View 包進 Flutter 中
• 現在我們也可以在 Platform View 上重疊任意的 Flutter Widget
加上 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)
放置 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;
}
加上 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);
根據 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];
}
Custom Info Window
custom_info_window
https://pub.dev/packages/custom_info_window
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();
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
)
如何更新地圖內容
• 可以透過 setState() 更新包含 GoogleMap Widget 的 Widget
• 如果使⽤ Bloc,可以將 GoogleMap Widget 放在 BlocBuilder 中
• 以上⽅式可以修改有哪些 Marker、Polyline 與 Circle,不會改變地圖的
zoom level 與中央位置
移動地圖
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;
},
),
進階設置
• liteModeEnabled:⽤在完全靜態的地圖上
• indoorViewEnabled:顯示室內導航
• tra
ffi
cEnabled:顯示交通狀況
• buildingsEnabled:顯示建築物模型
設定地圖樣式
• GoogleMap Widget 本身
沒有 light/dark mode
• ⽽是設置整個 Map 樣式
的 JSON
• https://
console.cloud.google.co
m/projectselector2/
google/maps-apis/studio/
styles?pli=1
設定地圖樣式
rootBundle.loadString('assets/map_style.txt').then((string) {
_mapStyle = string;
});
GoogleMap(
onMapCreated: (GoogleMapController controller) {
mapController = controller;
mapController.setMapStyle(_mapStyle);
}
);
這個 Future 也可以考慮放在 FutureBuilder 裡頭
疑難雜症
• Android 上,如果 App 放在背景再回到前景,之後 GoogleMap Widget 可
能畫⾯花掉,或是有奇怪的狀況
• 可以⽤ WidgetsBindingObserver 偵測回到前景重繪
(didChangeAppLifecycleState)
• 重新 Build GoogleMap Widget 也不⾒得會重繪
• 但是呼叫 setMapStyle ⼀定可以重繪
地點搜尋
地點搜尋
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),
);
}
路徑規劃
路徑規劃
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';
路徑偏移(Route deviation)
• 判斷⽬前的 GPS 位置是否偏移規劃的路徑
• 可以使⽤ google_map_polyutil
• 呼叫 PolyUtils.isLocationOnEdgeTolerance,判斷座標是否在路徑上
• 還有其他⼯具
Recap
• 放置地圖 Widget
• 新增 Marker
• 新增 Polyline
• 計算 zoom level
• 顯示 information window
• 移動地圖、設定樣式
• 其他…
That’s all
謝謝⼤家

Más contenido relacionado

La actualidad más candente

知って得するUnity エディタ拡張編
知って得するUnity エディタ拡張編知って得するUnity エディタ拡張編
知って得するUnity エディタ拡張編
Shota Baba
 
initとプロセス再起動
initとプロセス再起動initとプロセス再起動
initとプロセス再起動
Takashi Takizawa
 
複雑ネットワーク勉強会 二部グラフの基礎と応用 20120208
複雑ネットワーク勉強会  二部グラフの基礎と応用 20120208複雑ネットワーク勉強会  二部グラフの基礎と応用 20120208
複雑ネットワーク勉強会 二部グラフの基礎と応用 20120208
Hiroko Onari
 

La actualidad más candente (20)

Flutter session 01
Flutter session 01Flutter session 01
Flutter session 01
 
「再代入なんて、あるわけない」 ~ふつうのプログラマが関数型言語を知るべき理由~ (Gunma.web #5 2011/05/14)
「再代入なんて、あるわけない」 ~ふつうのプログラマが関数型言語を知るべき理由~ (Gunma.web #5 2011/05/14)「再代入なんて、あるわけない」 ~ふつうのプログラマが関数型言語を知るべき理由~ (Gunma.web #5 2011/05/14)
「再代入なんて、あるわけない」 ~ふつうのプログラマが関数型言語を知るべき理由~ (Gunma.web #5 2011/05/14)
 
MapReduce入門
MapReduce入門MapReduce入門
MapReduce入門
 
Flutter
FlutterFlutter
Flutter
 
知って得するUnity エディタ拡張編
知って得するUnity エディタ拡張編知って得するUnity エディタ拡張編
知って得するUnity エディタ拡張編
 
Hadoop導入事例 in クックパッド
Hadoop導入事例 in クックパッドHadoop導入事例 in クックパッド
Hadoop導入事例 in クックパッド
 
[Cloud OnAir] BigQuery の仕組みからベストプラクティスまでのご紹介 2018年9月6日 放送
[Cloud OnAir] BigQuery の仕組みからベストプラクティスまでのご紹介 2018年9月6日 放送[Cloud OnAir] BigQuery の仕組みからベストプラクティスまでのご紹介 2018年9月6日 放送
[Cloud OnAir] BigQuery の仕組みからベストプラクティスまでのご紹介 2018年9月6日 放送
 
計算機を用いて数学の問題を解くということ
計算機を用いて数学の問題を解くということ計算機を用いて数学の問題を解くということ
計算機を用いて数学の問題を解くということ
 
initとプロセス再起動
initとプロセス再起動initとプロセス再起動
initとプロセス再起動
 
OSSを活用したIaCの実現
OSSを活用したIaCの実現OSSを活用したIaCの実現
OSSを活用したIaCの実現
 
たのしい高階関数
たのしい高階関数たのしい高階関数
たのしい高階関数
 
Alpine.jsハンズオン
Alpine.jsハンズオンAlpine.jsハンズオン
Alpine.jsハンズオン
 
DDD 20121106 SEA Forum November
DDD 20121106 SEA Forum NovemberDDD 20121106 SEA Forum November
DDD 20121106 SEA Forum November
 
Flutter Intro
Flutter IntroFlutter Intro
Flutter Intro
 
Kendoui
KendouiKendoui
Kendoui
 
ゼロ幅スペースという悪夢
ゼロ幅スペースという悪夢ゼロ幅スペースという悪夢
ゼロ幅スペースという悪夢
 
Flutter workshop
Flutter workshopFlutter workshop
Flutter workshop
 
GDSC Flutter Forward Workshop.pptx
GDSC Flutter Forward Workshop.pptxGDSC Flutter Forward Workshop.pptx
GDSC Flutter Forward Workshop.pptx
 
複雑ネットワーク勉強会 二部グラフの基礎と応用 20120208
複雑ネットワーク勉強会  二部グラフの基礎と応用 20120208複雑ネットワーク勉強会  二部グラフの基礎と応用 20120208
複雑ネットワーク勉強会 二部グラフの基礎と応用 20120208
 
NextGen Server/Client Architecture - gRPC + Unity + C#
NextGen Server/Client Architecture - gRPC + Unity + C#NextGen Server/Client Architecture - gRPC + Unity + C#
NextGen Server/Client Architecture - gRPC + Unity + C#
 

Similar a 怎樣在 Flutter app 中使用 Google Maps

Google map api接口整理
Google map api接口整理Google map api接口整理
Google map api接口整理
lileinba
 
行動商務實務 - PhoneGap Advance
行動商務實務 - PhoneGap Advance行動商務實務 - PhoneGap Advance
行動商務實務 - PhoneGap Advance
My own sweet home!
 
Introduction to corona sdk
Introduction to corona sdkIntroduction to corona sdk
Introduction to corona sdk
馬 萬圳
 

Similar a 怎樣在 Flutter app 中使用 Google Maps (20)

再接再勵學 Swift 程式設計
再接再勵學 Swift 程式設計再接再勵學 Swift 程式設計
再接再勵學 Swift 程式設計
 
I os 07
I os 07I os 07
I os 07
 
2016輕鬆開發自有網路地圖工作坊 進階班 0701
2016輕鬆開發自有網路地圖工作坊 進階班 07012016輕鬆開發自有網路地圖工作坊 進階班 0701
2016輕鬆開發自有網路地圖工作坊 進階班 0701
 
Google map api接口整理
Google map api接口整理Google map api接口整理
Google map api接口整理
 
Responsive Web UI Design
Responsive Web UI DesignResponsive Web UI Design
Responsive Web UI Design
 
Html5移动web应用开发(PhoneGap)
Html5移动web应用开发(PhoneGap)Html5移动web应用开发(PhoneGap)
Html5移动web应用开发(PhoneGap)
 
Html5移动web应用开发(PhoneGap)
Html5移动web应用开发(PhoneGap)Html5移动web应用开发(PhoneGap)
Html5移动web应用开发(PhoneGap)
 
Study mapapi v0.1
Study mapapi v0.1Study mapapi v0.1
Study mapapi v0.1
 
I os 02
I os 02I os 02
I os 02
 
AngularJS training in Luster
AngularJS training in LusterAngularJS training in Luster
AngularJS training in Luster
 
Android 智慧型手機程式設計
Android 智慧型手機程式設計Android 智慧型手機程式設計
Android 智慧型手機程式設計
 
Behind Tetris5
Behind Tetris5Behind Tetris5
Behind Tetris5
 
Anroid development part.1
Anroid development part.1Anroid development part.1
Anroid development part.1
 
行動商務實務 - PhoneGap Advance
行動商務實務 - PhoneGap Advance行動商務實務 - PhoneGap Advance
行動商務實務 - PhoneGap Advance
 
I os 01
I os 01I os 01
I os 01
 
Introduction to corona sdk
Introduction to corona sdkIntroduction to corona sdk
Introduction to corona sdk
 
HTML5移动WEB应用程序开发(PhoneGap)
HTML5移动WEB应用程序开发(PhoneGap)HTML5移动WEB应用程序开发(PhoneGap)
HTML5移动WEB应用程序开发(PhoneGap)
 
HTML5移动应用开发分享会(PhoneGap)
HTML5移动应用开发分享会(PhoneGap)HTML5移动应用开发分享会(PhoneGap)
HTML5移动应用开发分享会(PhoneGap)
 
2021laravelconftwslides12
2021laravelconftwslides122021laravelconftwslides12
2021laravelconftwslides12
 
I os 16
I os 16I os 16
I os 16
 

Más de Weizhong Yang

苦集滅道:透過開發客製 Sketch Plug-in 改善產品設計流程
苦集滅道:透過開發客製 Sketch Plug-in  改善產品設計流程苦集滅道:透過開發客製 Sketch Plug-in  改善產品設計流程
苦集滅道:透過開發客製 Sketch Plug-in 改善產品設計流程
Weizhong Yang
 
Aspect Oriented Programming
Aspect Oriented ProgrammingAspect Oriented Programming
Aspect Oriented Programming
Weizhong Yang
 
Python 的文件系統
Python 的文件系統Python 的文件系統
Python 的文件系統
Weizhong Yang
 

Más de Weizhong Yang (20)

Flutter BLE
Flutter BLEFlutter BLE
Flutter BLE
 
關於延長役期這件事情
關於延長役期這件事情關於延長役期這件事情
關於延長役期這件事情
 
Dart null safety
Dart null safetyDart null safety
Dart null safety
 
Github Actions
Github ActionsGithub Actions
Github Actions
 
iPlayground: CarPlay and MFI Hearing Aids
iPlayground: CarPlay and MFI Hearing AidsiPlayground: CarPlay and MFI Hearing Aids
iPlayground: CarPlay and MFI Hearing Aids
 
CocoaPods private repo
CocoaPods private repoCocoaPods private repo
CocoaPods private repo
 
那些年被蘋果 Ban 掉的 API
那些年被蘋果 Ban 掉的 API那些年被蘋果 Ban 掉的 API
那些年被蘋果 Ban 掉的 API
 
給 iOS 工程師的 Vue.js 開發
給 iOS 工程師的 Vue.js 開發給 iOS 工程師的 Vue.js 開發
給 iOS 工程師的 Vue.js 開發
 
苦集滅道:透過開發客製 Sketch Plug-in 改善產品設計流程
苦集滅道:透過開發客製 Sketch Plug-in  改善產品設計流程苦集滅道:透過開發客製 Sketch Plug-in  改善產品設計流程
苦集滅道:透過開發客製 Sketch Plug-in 改善產品設計流程
 
使用 switch/case 重構程式碼
使用 switch/case 重構程式碼使用 switch/case 重構程式碼
使用 switch/case 重構程式碼
 
怎樣寫出比較沒有問題的 Code
怎樣寫出比較沒有問題的 Code怎樣寫出比較沒有問題的 Code
怎樣寫出比較沒有問題的 Code
 
貪食蛇
貪食蛇貪食蛇
貪食蛇
 
Aspect Oriented Programming
Aspect Oriented ProgrammingAspect Oriented Programming
Aspect Oriented Programming
 
Mac OS X 與 iOS 的 Audio API
Mac OS X 與 iOS 的 Audio APIMac OS X 與 iOS 的 Audio API
Mac OS X 與 iOS 的 Audio API
 
Html 5 native drag
Html 5 native dragHtml 5 native drag
Html 5 native drag
 
Retina mac
Retina macRetina mac
Retina mac
 
Python 的文件系統
Python 的文件系統Python 的文件系統
Python 的文件系統
 
Input Method Kit
Input Method KitInput Method Kit
Input Method Kit
 
Refactoring
RefactoringRefactoring
Refactoring
 
Core animation
Core animationCore animation
Core animation
 

怎樣在 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
  • 5. 這個 App 有哪些功能? • 顯示 ODB 傳來的讀數(速度、溫度等) • 在地圖上顯示⽬前⾞⼦與⼿機的位置 • 顯示 Journey(每次⾞⼦上⽕/熄⽕之間的駕駛紀錄) • 路徑規劃: • 怎樣⾛到我的⾞⼦? • 怎樣去加油站/維修站/⾃訂地點? • Geo Fence 規劃(在某個區域開⾞時發出警告) • Curfew 規劃(在某個時間開⾞發出警告) • 各種警告—急彎、急停、撞擊、進⼊ Geo Fence
  • 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]; }
  • 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 ⼀定可以重繪
  • 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';
  • 29. 路徑偏移(Route deviation) • 判斷⽬前的 GPS 位置是否偏移規劃的路徑 • 可以使⽤ google_map_polyutil • 呼叫 PolyUtils.isLocationOnEdgeTolerance,判斷座標是否在路徑上 • 還有其他⼯具
  • 30. Recap • 放置地圖 Widget • 新增 Marker • 新增 Polyline • 計算 zoom level • 顯示 information window • 移動地圖、設定樣式 • 其他…